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

WPF 中的人工继承上下文

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (29投票s)

2008年7月2日

CPOL

8分钟阅读

viewsIcon

162672

downloadIcon

1010

回顾并比较了三种技术,这些技术能够让你将数据绑定到任何元素树。

引言

本文讨论并演示了三种方法来模拟元素树外部的 DependencyObject 的继承上下文。在本文中,我们研究了如何使用人工继承上下文来在通常无法实现的场景中启用数据绑定。本文的演示程序展示了如何使用这三种技术。在本文的最后,有一个部分比较了每种技术的优缺点。

背景

你可以向 WPF 属性系统注册一个依赖属性,以便其值可以继承到元素树(技术上是逻辑树)中。典型的例子是 FontSize 属性。如果你在 Window 上设置了 FontSize,则该 Window 中的所有元素都将以该字体大小显示文本。如果你在 Window 中的 GroupBox 上指定了不同的 FontSize,则 GroupBox 中的所有元素将继承并使用新的 FontSize,而不是 WindowFontSize。这类似于 Windows Forms 中的环境属性。

FrameworkElementFrameworkContentElement 类上另一个可继承的依赖属性是 DataContext,它充当元素树中所有数据绑定的环境隐式数据源。当元素的属性被绑定,并且 BindingSourceRelativeSourceElementName 属性未设置时,Binding 会自动绑定到元素的 DataContext。这是 WPF 框架的一个强大功能,因为在实践中,大多数绑定都发生在元素属性和数据上下文对象属性之间。这也是 WPF 成为出色的数据驱动用户界面平台的原因之一。

当你尝试绑定元素树中不存在的对象的属性时,此系统就会失效。一个例子是尝试绑定元素 Tag 属性引用的对象上的属性。你可以将元素的 Tag 设置为视觉元素,但该元素不在元素树中。WPF 不会将该元素添加到元素树中,因为它实际上不是用户界面的一部分;它只是内存中的一个视觉元素。由于该元素不在元素树中,它就没有继承上下文,这意味着它无法绑定到继承的 DataContext。这里的关键是,当元素不在元素树中时,它无法继承依赖属性的值。值继承依赖于所谓的“继承上下文”,这是用于将值向下传播到元素树的内部基础设施。此外,没有继承上下文还会阻止绑定使用 ElementName 属性来指定其源。

本文展示了三种方法来解决数据绑定时没有继承上下文的问题。每种技术都设法将元素树的 DataContext “导出”到树外部的对象。

演示

在本文的开头,你可以下载本文附带的演示项目。该应用程序以三种方式展示了完成相同的任务。任务本身非常简单,你绝对可以在不需要人工继承上下文的情况下完成它。但是,和我的许多文章一样,我努力寻找一个足够简单的编程任务,以免“妨碍”,但又足够复杂,能够让我演示正在审查的技术。

演示应用程序允许用户选择一个历史文件(例如《美国宪法》),并查看其名称以大文本显示。该文件名称以绘制的画笔显示文件的照片。UI 还包含一个带有两个选项的 ListBox。如果选择“完全不透明”,则照片显示为完全不透明;否则,如果选择“半透明”,则显示为半透明。

正如我在下面的截图中看到的,在查看《美国宪法》时,文本以《美国宪法》的照片以完全不透明的方式绘制。

screenshot.png

正如我之前提到的,这个简单(且奇怪!)的应用程序可以轻松创建,而无需使用人工继承上下文。大多数需要人工继承上下文的实际情况都比这个愚蠢的小编程任务更复杂和晦涩。

资源注入

从元素树外部的元素访问 DataContext 或其他任何对象的最快捷、最简单的方法是将其注入资源系统。这里的技巧是依赖这样一个事实:DynamicResource 引用会在创建时检查 ApplicationResources 集合,查找具有匹配资源键的资源。即使 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 日 - 创建了本文。
© . All rights reserved.