65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (32投票s)

2007 年 5 月 6 日

CPOL

8分钟阅读

viewsIcon

265484

downloadIcon

1237

介绍了一种能为数据绑定带来新可能性的模式。

引言

本文演示了如何在 WPF 用户界面中创建一个逻辑树的“虚拟分支”。所谓“虚拟分支”,我指的是一组并未实际存在于逻辑树中的元素,但它们可以利用向下传播的 DataContext。虚拟分支中的元素不被 LogicalTreeHelper 识别,也不会参与路由事件和命令系统。将虚拟分支附加到逻辑树的目的是为了实现一些否则无法实现的数据绑定场景。

在直接深入介绍创建虚拟分支的代码和标记之前,我们将先回顾一个可以使用虚拟分支来解决的问题。一旦我们将“虚拟分支”这个相当晦涩的概念置于一个更易于理解的语境中,我们再来回顾它们所解决的普遍问题以及实现的细节。

问题所在

假设我们创建一个简单的应用程序,允许用户在 Slider 上选择一个数字,然后在一个 TextBox 中输入该数字的倍数。当 TextBox 中的数字不是 Slider 上选择的数字的倍数时,应用程序应该提供视觉提示。也许用户界面看起来像这样

用户输入的两个数字存储在一个名为 DivisionNumbers 的简单类的实例中,该类实现了此接口

interface IDivisionNumbers : INotifyPropertyChanged
{
 int Dividend { get; set; }
 int Divisor { get; set; }
}

Window 的构造函数中,我们配置了一个 DivisionNumbers 实例,并将其设置为 WindowDataContext,以便应用程序中的所有元素都可以绑定到它,如下所示

public Window1()
{
 InitializeComponent();

 DivisionNumbers nums = new DivisionNumbers();
 nums.Divisor = 1;
 nums.Dividend = 1;
 this.DataContext = nums;
}

用于验证 TextBox 中数字的逻辑被放入了一个 ValidationRule 的子类中。该类的实例被添加到与 TextBoxText 属性关联的 BindingValidationRules 属性中。当用户在 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 不在命名空间中,您将无法通过 BindingElementName 属性绑定到 Slider。用于将 ElementName 解析为元素的查找过程需要 Binding 绑定到已添加到与目标元素相同命名空间中的 DependencyObject 子类的实例。

如何优雅地克服这个看似不可能的技术障碍,以实现我们温和的目标?

解决方案

将虚拟分支附加到逻辑树可以为不在树中的对象提供它们所需的来自树的数据。从概念上讲,将虚拟分支附加到逻辑树类似于窃听邻居的电话线,以便您可以收听他们关于您感兴趣的话题的电话交谈。我们将改为接入用户界面的逻辑树,并利用向下传播的 DataContext 值。唯一涉及的“线路拼接”技术是向逻辑树中的某个特定元素添加一些 XAML。

简而言之,虚拟分支中的元素可以绑定到分支所附加的逻辑树的 DataContext。可以将虚拟分支附加到逻辑树中的任何元素,以防某些子树被分配了不同的 DataContext 来进行绑定。如果需要,您可以将多个虚拟分支应用于同一个逻辑树。

在不深入实现细节的情况下,让我们回顾一下什么是虚拟分支,什么不是。本文的下一节将介绍如何实现该模式。

“虚拟分支”一词既不是 WPF 的官方术语,也不是由任何程序化构造表示(即,没有 VirtualBranch 类)。这是一个我们可以用来指代我设计的一种可重用模式的术语,该模式旨在克服 WPF 数据绑定模型所施加的上述技术限制。

我所说的“物理分支”是实际存在于逻辑树中的元素的层次结构分组。LogicalTreeHelper 方法返回物理分支中的元素。虚拟分支中的元素则不返回。物理分支中的元素参与路由事件和命令系统。虚拟分支中的元素则不参与。虚拟分支中的元素实际上并不在分支所附加的逻辑树中。

实现虚拟分支

创建虚拟分支非常容易。只需要三个组件。有关我们即将研究的技术的视觉解释,请参阅下图

第一步 – 构建一座桥梁

将一个 FrameworkElement 添加到您希望附加虚拟分支的元素的 Resources 集合中。该元素充当逻辑树和虚拟分支之间的桥梁。逻辑树的 DataContext 通过数据绑定推送到桥梁上。桥梁元素成为虚拟分支的根节点。在本文的演示应用程序中,桥梁元素被添加到 WindowResources 中,如下所示

<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 确保每当 WindowDataContext 属性被设置时,新值也会被推送到桥梁元素的 DataContext 中。理想情况下,Window 不需要将其 DataContext 应用此 Binding。如果桥梁元素包含该 Binding(为了封装),那会更简洁。但是,必须在 WindowDataContext 上建立此 Binding,因为桥梁元素是存在于 ResourceDictionary 中的资源,因此无法绑定到元素树中的元素。

需要注意的是,上面看到的 Binding 必须应用于您实际为其 DataContext 属性赋值的逻辑树中的元素,而不是某个恰好继承了该值的其他元素。在演示应用程序中,DataContext 设置在 Window 上,因此 Binding 应用于 WindowDataContext 属性。

第三步 – 从桥梁上拉取 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 – 绑定到桥梁

最后一步是将 IntegerContainerDataContext 绑定到桥梁元素的 DataContext。一旦 IntegerContainerDataContext 被绑定,它将引用与逻辑树中的所有元素相同的 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 日 - 创建文章
© . All rights reserved.