将虚拟分支附加到 WPF 的逻辑树





5.00/5 (32投票s)
介绍了一种能为数据绑定带来新可能性的模式。
引言
本文演示了如何在 WPF 用户界面中创建一个逻辑树的“虚拟分支”。所谓“虚拟分支”,我指的是一组并未实际存在于逻辑树中的元素,但它们可以利用向下传播的 DataContext
。虚拟分支中的元素不被 LogicalTreeHelper
识别,也不会参与路由事件和命令系统。将虚拟分支附加到逻辑树的目的是为了实现一些否则无法实现的数据绑定场景。
在直接深入介绍创建虚拟分支的代码和标记之前,我们将先回顾一个可以使用虚拟分支来解决的问题。一旦我们将“虚拟分支”这个相当晦涩的概念置于一个更易于理解的语境中,我们再来回顾它们所解决的普遍问题以及实现的细节。
问题所在
假设我们创建一个简单的应用程序,允许用户在 Slider
上选择一个数字,然后在一个 TextBox
中输入该数字的倍数。当 TextBox
中的数字不是 Slider
上选择的数字的倍数时,应用程序应该提供视觉提示。也许用户界面看起来像这样
用户输入的两个数字存储在一个名为 DivisionNumbers
的简单类的实例中,该类实现了此接口
interface IDivisionNumbers : INotifyPropertyChanged
{
int Dividend { get; set; }
int Divisor { get; set; }
}
在 Window
的构造函数中,我们配置了一个 DivisionNumbers
实例,并将其设置为 Window
的 DataContext
,以便应用程序中的所有元素都可以绑定到它,如下所示
public Window1()
{
InitializeComponent();
DivisionNumbers nums = new DivisionNumbers();
nums.Divisor = 1;
nums.Dividend = 1;
this.DataContext = nums;
}
用于验证 TextBox
中数字的逻辑被放入了一个 ValidationRule
的子类中。该类的实例被添加到与 TextBox
的 Text
属性关联的 Binding
的 ValidationRules
属性中。当用户在 TextBox
中输入数字时,我们的 ValidationRule
会检查该数字是否有效。此 XAML 看起来类似于
<TextBox x:Name="dividendTextBox">
<TextBox.Text>
<Binding Path="Dividend" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:IsMultipleOfValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
我们的自定义 ValidationRule
的不完整版本实现如下
public class IsMultipleOfValidationRule : ValidationRule
{
public override ValidationResult Validate(
object value, CultureInfo cultureInfo )
{
try
{
int dividend = (int)Convert.ChangeType( value, typeof( int ) );
int divisor = // Get the divisor somehow...
if( dividend % divisor == 0 )
return ValidationResult.ValidResult;
return new ValidationResult(
false,
"The number is not a multiple of " + divisor );
}
catch
{
return new ValidationResult( false, "Not a number." );
}
}
}
现在出现了一个问题。我们如何在 Validate
方法中获取用户选择的除数的值?
我们不能在 IsMultipleOfValidationRule
中添加一个名为 Divisor
的整数属性并绑定到它,因为只有依赖属性才能绑定。我们也无法向该类添加依赖属性,因为它们只能存在于派生自 DependencyObject
的类中。ValidationRule
不派生自该类。
即使我们设法向 IsMultipleOfValidationRule
添加了一个名为 Divisor
的整数依赖属性,也无济于事。由 Binding
拥有的 ValidationRule
对象并未添加到逻辑树中。由于 DataContext
仅由逻辑树中的元素继承,我们的自定义 ValidationRule
将无法访问该值。最糟糕的是,ValidationRule
甚至没有 DataContext
属性,因为它不派生自 FrameworkElement
!
同样,ValidationRule
从未被添加到命名空间中。由于 ValidationRule
不在命名空间中,您将无法通过 Binding
的 ElementName
属性绑定到 Slider
。用于将 ElementName
解析为元素的查找过程需要 Binding
绑定到已添加到与目标元素相同命名空间中的 DependencyObject
子类的实例。
如何优雅地克服这个看似不可能的技术障碍,以实现我们温和的目标?
解决方案
将虚拟分支附加到逻辑树可以为不在树中的对象提供它们所需的来自树的数据。从概念上讲,将虚拟分支附加到逻辑树类似于窃听邻居的电话线,以便您可以收听他们关于您感兴趣的话题的电话交谈。我们将改为接入用户界面的逻辑树,并利用向下传播的 DataContext
值。唯一涉及的“线路拼接”技术是向逻辑树中的某个特定元素添加一些 XAML。
简而言之,虚拟分支中的元素可以绑定到分支所附加的逻辑树的 DataContext
。可以将虚拟分支附加到逻辑树中的任何元素,以防某些子树被分配了不同的 DataContext
来进行绑定。如果需要,您可以将多个虚拟分支应用于同一个逻辑树。
在不深入实现细节的情况下,让我们回顾一下什么是虚拟分支,什么不是。本文的下一节将介绍如何实现该模式。
“虚拟分支”一词既不是 WPF 的官方术语,也不是由任何程序化构造表示(即,没有 VirtualBranch
类)。这是一个我们可以用来指代我设计的一种可重用模式的术语,该模式旨在克服 WPF 数据绑定模型所施加的上述技术限制。
我所说的“物理分支”是实际存在于逻辑树中的元素的层次结构分组。LogicalTreeHelper
方法返回物理分支中的元素。虚拟分支中的元素则不返回。物理分支中的元素参与路由事件和命令系统。虚拟分支中的元素则不参与。虚拟分支中的元素实际上并不在分支所附加的逻辑树中。
实现虚拟分支
创建虚拟分支非常容易。只需要三个组件。有关我们即将研究的技术的视觉解释,请参阅下图
第一步 – 构建一座桥梁
将一个 FrameworkElement
添加到您希望附加虚拟分支的元素的 Resources
集合中。该元素充当逻辑树和虚拟分支之间的桥梁。逻辑树的 DataContext
通过数据绑定推送到桥梁上。桥梁元素成为虚拟分支的根节点。在本文的演示应用程序中,桥梁元素被添加到 Window
的 Resources
中,如下所示
<Window.Resources>
<!-- This is the "root node" in the virtual branch
attached to the logical tree. It has its
DataContext set by the Binding applied to the
Window's DataContext property. -->
<FrameworkElement x:Key="DataContextBridge" />
</Window.Resources>
第二步 – 将 DataContext 推过桥梁
此时,我们已经准备好一个元素,可以将其作为逻辑树的 DataContext
暴露给虚拟分支中的元素。现在我们需要确保桥梁元素的 DataContext
始终与逻辑树中的某个特定元素具有相同的值。我们将通过使用很少使用的“OneWayToSource
”绑定模式来实现此目的,如下所示
<Window.DataContext>
<!-- This Binding sets the DataContext on the "root node"
of the virtual logical tree branch. This Binding
must be applied to the DataContext of the element
which is actually assigned the data context value. -->
<Binding
Mode="OneWayToSource"
Path="DataContext"
Source="{StaticResource DataContextBridge}"
/>
</Window.DataContext>
上面的 Binding
确保每当 Window
的 DataContext
属性被设置时,新值也会被推送到桥梁元素的 DataContext
中。理想情况下,Window
不需要将其 DataContext
应用此 Binding
。如果桥梁元素包含该 Binding
(为了封装),那会更简洁。但是,必须在 Window
的 DataContext
上建立此 Binding
,因为桥梁元素是存在于 ResourceDictionary
中的资源,因此无法绑定到元素树中的元素。
需要注意的是,上面看到的 Binding
必须应用于您实际为其 DataContext
属性赋值的逻辑树中的元素,而不是某个恰好继承了该值的其他元素。在演示应用程序中,DataContext
设置在 Window
上,因此 Binding
应用于 Window
的 DataContext
属性。
第三步 – 从桥梁上拉取 DataContext
最后一步是利用已“走私”到虚拟分支的 DataContext
。这是我们终于有机会从 IsMultipleOfValidationRule
中获取除数的值,如前所述。此步骤可分为三个任务
第三步 a – 创建数据容器类
虚拟分支中的所有节点都应派生自 FrameworkElement
,即使它们仅用于保存简单值。派生自 FrameworkElement
使对象具有 DataContext
依赖属性,并允许我们创建自己的依赖属性(FrameworkElement
派生自 DependencyObject
)。
以下是用于我们的验证规则的除数保存类
/// <summary>
/// Stores an integer in the Value property. Derives from
/// FrameworkElement so that it gets the DataContext property.
/// </summary>
public class IntegerContainer : FrameworkElement
{
public int Value
{
get { return (int)GetValue( ValueProperty ); }
set { SetValue( ValueProperty, value ); }
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(
"Value",
typeof( int ),
typeof( IntegerContainer ),
new UIPropertyMetadata( 0 ) );
}
第三步 b – 使用数据容器
现在是将 IntegerContainer
用于 IsMultipleOfValidationRule
类的时候了。我们将公开该类的实例作为 public
读写属性。这是我们验证规则的完整版本
public class IsMultipleOfValidationRule : ValidationRule
{
private IntegerContainer divisorContainer;
public IntegerContainer DivisorContainer
{
get { return divisorContainer; }
set { divisorContainer = value; }
}
public override ValidationResult Validate(
object value, CultureInfo cultureInfo )
{
try
{
int dividend = (int)Convert.ChangeType( value, typeof( int ) );
int divisor = this.DivisorContainer.Value;
if( dividend % divisor == 0 )
return ValidationResult.ValidResult;
return new ValidationResult(
false,
"The number is not a multiple of " + divisor );
}
catch
{
return new ValidationResult( false, "Not a number." );
}
}
}
第三步 c – 绑定到桥梁
最后一步是将 IntegerContainer
的 DataContext
绑定到桥梁元素的 DataContext
。一旦 IntegerContainer
的 DataContext
被绑定,它将引用与逻辑树中的所有元素相同的 DivisionNumbers
对象。届时,我们可以轻松地绑定到其 Divisor
属性。以下 XAML 配置了 TextBox
和我们的自定义验证规则
<TextBox x:Name="dividendTextBox">
<TextBox.Text>
<Binding Path="Dividend" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:IsMultipleOfValidationRule>
<local:IsMultipleOfValidationRule.DivisorContainer>
<!-- This IntegerContainer is the "child node" of
the DataContextBridge element, in the virtual
branch attached to the Window's logical tree. -->
<local:IntegerContainer
DataContext="{Binding
Source={StaticResource DataContextBridge},
Path=DataContext}"
Value="{Binding Divisor}"
/>
</local:IsMultipleOfValidationRule.DivisorContainer>
</local:IsMultipleOfValidationRule>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
结束语
我相信这种模式将被以我从未想象过的方式使用。很可能存在一些对应用程序有害或破坏稳定的使用虚拟分支的方法,所以要小心使用它们。如果您使用虚拟分支来解决与此处所示不同的问题,请在本篇文章的消息板上留言,解释您的情况。
历史
- 2007 年 5 月 6 日 - 创建文章