WPF 中的人工继承上下文






4.97/5 (29投票s)
回顾并比较了三种技术,这些技术能够让你将数据绑定到任何元素树。
引言
本文讨论并演示了三种方法来模拟元素树外部的 DependencyObject
的继承上下文。在本文中,我们研究了如何使用人工继承上下文来在通常无法实现的场景中启用数据绑定。本文的演示程序展示了如何使用这三种技术。在本文的最后,有一个部分比较了每种技术的优缺点。
背景
你可以向 WPF 属性系统注册一个依赖属性,以便其值可以继承到元素树(技术上是逻辑树)中。典型的例子是 FontSize
属性。如果你在 Window
上设置了 FontSize
,则该 Window
中的所有元素都将以该字体大小显示文本。如果你在 Window
中的 GroupBox
上指定了不同的 FontSize
,则 GroupBox
中的所有元素将继承并使用新的 FontSize
,而不是 Window
的 FontSize
。这类似于 Windows Forms 中的环境属性。
FrameworkElement
和 FrameworkContentElement
类上另一个可继承的依赖属性是 DataContext
,它充当元素树中所有数据绑定的环境隐式数据源。当元素的属性被绑定,并且 Binding
的 Source
、RelativeSource
和 ElementName
属性未设置时,Binding
会自动绑定到元素的 DataContext
。这是 WPF 框架的一个强大功能,因为在实践中,大多数绑定都发生在元素属性和数据上下文对象属性之间。这也是 WPF 成为出色的数据驱动用户界面平台的原因之一。
当你尝试绑定元素树中不存在的对象的属性时,此系统就会失效。一个例子是尝试绑定元素 Tag
属性引用的对象上的属性。你可以将元素的 Tag
设置为视觉元素,但该元素不在元素树中。WPF 不会将该元素添加到元素树中,因为它实际上不是用户界面的一部分;它只是内存中的一个视觉元素。由于该元素不在元素树中,它就没有继承上下文,这意味着它无法绑定到继承的 DataContext
。这里的关键是,当元素不在元素树中时,它无法继承依赖属性的值。值继承依赖于所谓的“继承上下文”,这是用于将值向下传播到元素树的内部基础设施。此外,没有继承上下文还会阻止绑定使用 ElementName
属性来指定其源。
本文展示了三种方法来解决数据绑定时没有继承上下文的问题。每种技术都设法将元素树的 DataContext
“导出”到树外部的对象。
演示
在本文的开头,你可以下载本文附带的演示项目。该应用程序以三种方式展示了完成相同的任务。任务本身非常简单,你绝对可以在不需要人工继承上下文的情况下完成它。但是,和我的许多文章一样,我努力寻找一个足够简单的编程任务,以免“妨碍”,但又足够复杂,能够让我演示正在审查的技术。
演示应用程序允许用户选择一个历史文件(例如《美国宪法》),并查看其名称以大文本显示。该文件名称以绘制的画笔显示文件的照片。UI 还包含一个带有两个选项的 ListBox
。如果选择“完全不透明”,则照片显示为完全不透明;否则,如果选择“半透明”,则显示为半透明。
正如我在下面的截图中看到的,在查看《美国宪法》时,文本以《美国宪法》的照片以完全不透明的方式绘制。
正如我之前提到的,这个简单(且奇怪!)的应用程序可以轻松创建,而无需使用人工继承上下文。大多数需要人工继承上下文的实际情况都比这个愚蠢的小编程任务更复杂和晦涩。
资源注入
从元素树外部的元素访问 DataContext
或其他任何对象的最快捷、最简单的方法是将其注入资源系统。这里的技巧是依赖这样一个事实:DynamicResource
引用会在创建时检查 Application
的 Resources
集合,查找具有匹配资源键的资源。即使 DynamicResource
位于不在元素树中的元素的属性上,也会发生这种情况。但请注意,与普通的 DynamicResource
引用不同,对 App.Resources
中资源的后续更新将**不会**被注意到和遵守。本质上,在这种情况下,它们的作用类似于 StaticResource
引用。
另一个需要了解的技巧是,资源必须在 DynamicResource
创建之前添加到 App.Resources
中。在实践中,这通常意味着你必须在 Window
/Page
/UserControl
构造函数调用 InitializeComponent
之前加载你的数据上下文对象并将其放入 App.Resources
。这确保了每个 DynamicResource
引用在 App.Resources
中查找匹配资源的单次检查都会成功。
以下是 ResourceInjectionDemo
UserControl
的代码隐藏文件。
public partial class ResourceInjectionDemo : UserControl
{
public ResourceInjectionDemo()
{
// After setting our DataContext, inject it into the App's
// Resources so that it is visible to all DynamicResource references.
// NOTE: This must be done *before* the call to InitializeComponent
// since DynamicResource references for objects not in the logical tree
// only check the App's Resources once, upon creation.
// This only works once. After the call to InitializeComponent, updating
// the resource value to a new datacontext object will have no effect.
base.DataContext = HistoricDocument.GetDocuments();
App.Current.Resources["DATA_HistoricDocuments"] = base.DataContext;
this.InitializeComponent();
}
}
资源键“DATA_HistoricDocuments
”是我随意设置的一个标识符。你会在控件的 XAML 文件中看到它的用法。
<UserControl
x:Class="ArtificialInheritanceContextDemo.ResourceInjectionDemo"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ComboBox
Grid.Row="0"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding}"
Margin="4"
/>
<ListBox x:Name="_listBox" Grid.Row="1" Margin="4">
<ListBoxItem Content="Fully Opaque" IsSelected="True">
<ListBoxItem.Tag>
<!--
Get a reference to the DataContext by grabbing it from
the Application's Resources via a DynamicResource reference.
-->
<Image
DataContext="{DynamicResource DATA_HistoricDocuments}"
Opacity="1"
Source="{Binding PhotoUri}"
Width="300" Height="350"
/>
</ListBoxItem.Tag>
</ListBoxItem>
<ListBoxItem Content="Semi-Transparent">
<ListBoxItem.Tag>
<Image
DataContext="{DynamicResource DATA_HistoricDocuments}"
Opacity="0.5"
Source="{Binding PhotoUri}"
Width="300" Height="350"
/>
</ListBoxItem.Tag>
</ListBoxItem>
</ListBox>
<Viewbox Grid.Row="2" Stretch="Fill">
<TextBlock
FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Text="{Binding Path=Name}"
>
<TextBlock.Foreground>
<VisualBrush
Visual="{Binding ElementName=_listBox, Path=SelectedItem.Tag}"
/>
</TextBlock.Foreground>
</TextBlock>
</Viewbox>
</Grid>
</UserControl>
DataContextSpy
间谍是指秘密调查他人行动和信息并将其报告给外部一方的人。我的 DataContextSpy
类做的正是这一点,它只观察元素树的 DataContext
,并且外部元素可以绑定到它以访问 DataContext
。你只需将 DataContextSpy
添加到任何元素的 Resources
集合中(除了设置 DataContext
的那个元素),它的 DataContext
属性就会自动暴露该元素的 DataContext
。
这是 DataContextSpy
类。
public class DataContextSpy
: Freezable // Enable ElementName and DataContext bindings
{
public DataContextSpy()
{
// This binding allows the spy to inherit a DataContext.
BindingOperations.SetBinding(this, DataContextProperty, new Binding());
this.IsSynchronizedWithCurrentItem = true;
}
/// <summary>
/// Gets/sets whether the spy will return the CurrentItem of the
/// ICollectionView that wraps the data context, assuming it is
/// a collection of some sort. If the data context is not a
/// collection, this property has no effect.
/// The default value is true.
/// </summary>
public bool IsSynchronizedWithCurrentItem { get; set; }
public object DataContext
{
get { return (object)GetValue(DataContextProperty); }
set { SetValue(DataContextProperty, value); }
}
// Borrow the DataContext dependency property from FrameworkElement.
public static readonly DependencyProperty DataContextProperty =
FrameworkElement.DataContextProperty.AddOwner(
typeof(DataContextSpy),
new PropertyMetadata(null, null, OnCoerceDataContext));
static object OnCoerceDataContext(DependencyObject depObj, object value)
{
DataContextSpy spy = depObj as DataContextSpy;
if (spy == null)
return value;
if (spy.IsSynchronizedWithCurrentItem)
{
ICollectionView view = CollectionViewSource.GetDefaultView(value);
if (view != null)
return view.CurrentItem;
}
return value;
}
protected override Freezable CreateInstanceCore()
{
// We are required to override this abstract method.
throw new NotImplementedException();
}
}
这个类利用了 Hillberg Freezable Trick 来访问宿主元素的 DataContext
。该技巧利用了 WPF 的 Freezable
类内置了获取继承上下文的支持,即使它不在元素树中。有关这方面工作的更多信息,我建议你查看 Dr. WPF 的 此处的详细解释。
DataContextSpyDemo
UserControl
的代码隐藏文件没有任何逻辑,所以让我们直接跳到 XAML 来看看它是如何工作的。
<UserControl
x:Class="ArtificialInheritanceContextDemo.DataContextSpyDemo"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ArtificialInheritanceContextDemo"
>
<UserControl.DataContext>
<ObjectDataProvider
MethodName="GetDocuments"
ObjectType="{x:Type local:HistoricDocument}"
/>
</UserControl.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ComboBox
Grid.Row="0"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding}"
Margin="4"
/>
<ListBox x:Name="_listBox" Grid.Row="1" Margin="4">
<ListBox.Resources>
<!--
This object 'spies' on the DataContext for other elements.
-->
<local:DataContextSpy x:Key="spy" />
</ListBox.Resources>
<ListBoxItem Content="Fully Opaque" IsSelected="True">
<ListBoxItem.Tag>
<Image
DataContext="{Binding Source={StaticResource spy}, Path=DataContext}"
Opacity="1"
Source="{Binding PhotoUri}"
Width="300" Height="350"
/>
</ListBoxItem.Tag>
</ListBoxItem>
<ListBoxItem Content="Semi-Transparent">
<ListBoxItem.Tag>
<Image
DataContext="{Binding Source={StaticResource spy}, Path=DataContext}"
Opacity="0.5"
Source="{Binding PhotoUri}"
Width="300" Height="350"
/>
</ListBoxItem.Tag>
</ListBoxItem>
</ListBox>
<Viewbox Grid.Row="2" Stretch="Fill">
<TextBlock
FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Text="{Binding Path=Name}"
>
<TextBlock.Foreground>
<VisualBrush
Visual="{Binding ElementName=_listBox, Path=SelectedItem.Tag}"
/>
</TextBlock.Foreground>
</TextBlock>
</Viewbox>
</Grid>
</UserControl>
逻辑树的虚拟分支
我们介绍的最后一个技术是我在 2007 年 5 月写过的 一篇文章。为了完整起见,我在本文中包含了一个使用虚拟分支的演示。创建逻辑树的虚拟分支类似于使用 DataContextSpy
,只是元素树推送 DataContext
,而不是从元素树拉取 DataContext
。基本思想是通过静态资源引用和 OneWayToSource
绑定导出 DataContext
属性(或任何属性)。
虚拟分支非常灵活,你可以轻松地将它们用于导出元素树的 DataContext
属性以外的属性。但最大的缺点是,你必须为应用于具有该属性设置的元素上的导出属性设置 OneWayToSource
绑定。而 DataContextSpy
可以添加到除设置 DataContext
的元素之外的任何元素的 Resources
集合中,虚拟分支只能由设置 DataContext
的元素建立。
以下是 VirtualBranchDemo
UserControl
的代码隐藏文件。
public partial class VirtualBranchDemo : UserControl
{
public VirtualBranchDemo()
{
this.InitializeComponent();
base.DataContext = HistoricDocument.GetDocuments();
}
}
控件的 DataContext
是在代码中设置的,因为 XAML 中有一个绑定将属性值推送到虚拟分支。XAML 文件如下所示。
<UserControl
x:Class="ArtificialInheritanceContextDemo.VirtualBranchDemo"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ArtificialInheritanceContextDemo"
>
<UserControl.Resources>
<FrameworkElement x:Key="bridge" />
</UserControl.Resources>
<UserControl.DataContext>
<Binding
Mode="OneWayToSource"
Path="DataContext"
Source="{StaticResource bridge}"
/>
</UserControl.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ComboBox
Grid.Row="0"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding}"
Margin="4"
/>
<ListBox x:Name="_listBox" Grid.Row="1" Margin="4">
<ListBoxItem Content="Fully Opaque" IsSelected="True">
<ListBoxItem.Tag>
<Image
DataContext="{Binding Source={StaticResource bridge}, Path=DataContext}"
Opacity="1"
Source="{Binding PhotoUri}"
Width="300" Height="350"
/>
</ListBoxItem.Tag>
</ListBoxItem>
<ListBoxItem Content="Semi-Transparent">
<ListBoxItem.Tag>
<Image
DataContext="{Binding Source={StaticResource bridge}, Path=DataContext}"
Opacity="0.5"
Source="{Binding PhotoUri}"
Width="300" Height="350"
/>
</ListBoxItem.Tag>
</ListBoxItem>
</ListBox>
<Viewbox Grid.Row="2" Stretch="Fill">
<TextBlock
FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Text="{Binding Path=Name}"
>
<TextBlock.Foreground>
<VisualBrush
Visual="{Binding ElementName=_listBox, Path=SelectedItem.Tag}"
/>
</TextBlock.Foreground>
</TextBlock>
</Viewbox>
</Grid>
</UserControl>
每种技术的优缺点
这些技术各有优劣。我在这里列出了我能想到的所有优点和缺点,以帮助你更容易地决定使用哪种方法。我确信肯定还有我没有列出的其他考虑因素,所以如果你发现了,请在本篇文章中发表评论。
资源注入
优点
- 易于实现
- 易于理解
缺点
- 污染
App.Resources
中的全局变量 - 无法将
DataContext
设置为新值 - 要求
DataContext
对象在InitializeComponent
调用之前存在
DataContextSpy
优点
- 易于实现
- 新的
DataContext
将被遵守 - 如有必要,
DataContext
可以通过ElementName
进行绑定 - 对元素树的影响非常小,几乎没有影响
缺点
- 可能令人困惑
- 不能添加到设置
DataContext
的元素的Resources
中 - 监视其他属性需要
DataContextSpy
类的新属性
虚拟分支
优点
- 新的
DataContext
将被遵守 - 通过数据绑定轻松将
DataContext
以外的属性导出到虚拟分支
缺点
- 可能令人困惑
- 需要由设置
DataContext
的元素来绑定DataContext
桥接器 - 需要元素树导出
DataContext
(对元素树有影响)
修订历史
- 2008 年 7 月 2 日 - 创建了本文。