探索用户控件中依赖属性的使用






4.44/5 (11投票s)
本文介绍了如何访问用于构成用户控件的 WPF 依赖属性。
引言
假设你有一个 WPF 用户控件,它由其他 WPF 控件组成,其中一些控件具有你希望初始化和数据绑定的依赖属性 (DP)。关键在于这些 DP 已经定义在子控件中。本文探讨了利用这些 DP 的各种方法,并强调了每种方法的各种局限性。
背景
我最初开始写另一篇文章,探索实现自定义 ComboBox
的不同方法。其中一种方法是用户控件。我一直在使用各种方法的 DP,并且在使用 XAML 从用户控件的子控件中初始化 DP 时遇到了一个问题。这需要通过用户控件的 Content
属性来访问子控件。这并不吸引人,因为它意味着使用该控件的任何人都会对其结构有深入的了解,这似乎违反了封装规则。因此,我考虑尝试其他方法,看看 CodeProject 和 Internet 上还有什么。结果就是这篇文章。
使用代码
各种方法通过检查不同的代码片段来展示。这些代码都遵循一个基本 WPF 应用程序的格式,该应用程序包含一个带有默认代码隐藏页的 MainWindow.xaml 和一个名为 UserControl1
的用户控件。用户控件对于每个版本都是相同的,它是一个 ComboBox
,专门用于允许选择红色、绿色或蓝色画笔。然后将其绑定到一个标签,该标签以选定的颜色显示文本“看着我”。根据示例,代码隐藏将是默认的或包含附加内容。附带的 zip 文件是 VS2010 解决方案,其中每个版本都包含一个单独的项目。应用程序的屏幕截图显示在顶部。
首先,我们来看用户控件的基本实现。这是一个非常简单的自定义,它使用 ComboBox
显示三种颜色:红色、绿色和蓝色。ComboBox
是用户控件的 Content
,并且 ComboBox
用于显示 SolidColorBrush
实例的每个项目的模板被重写为 20x20 的矩形。然后将 ComobBox
绑定到作为用户控件内资源创建的 SolidColorBrush
数组。这绝对没有什么令人兴奋的地方。除默认生成的代码外,没有其他代码隐藏。在解决方案中,这是 Initial 项目。
<UserControl x:Class="Initial.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<x:Array x:Key="SomeBrushes" Type="{x:Type SolidColorBrush}">
<SolidColorBrush>Red</SolidColorBrush>
<SolidColorBrush>Green</SolidColorBrush>
<SolidColorBrush>Blue</SolidColorBrush>
</x:Array>
</UserControl.Resources>
<ComboBox Name="combo" ItemsSource="{StaticResource SomeBrushes}">
<ComboBox.ItemTemplate>
<DataTemplate DataType="SolidColorBrush">
<Rectangle Width="20" Height="20" Fill="{Binding}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</UserControl>
接下来要看的是主窗口的 XAML 部分。它有一个 UserControl1
的实例和一个 Label
,该 Label
的前景颜色绑定到嵌入在用户控件中的 ComboBox
的 SelectedItem
DP。
<Window x:Class="Initial.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:Initial"
Title="MainWindow" Height="350" Width="525">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Left" VerticalAlignment="Top">
<src:UserControl1 x:Name="UC1"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Label Content="Look at me"
Foreground="{Binding ElementName=UC1,Path=Content.SelectedItem}"/>
</StackPanel>
</Window>
上一句话的最后一部分是令人不满意的。为了实现绑定,Label
必须知道嵌入在用户控件中的控件类型。这违反了封装原则。在某些方面,如果 WPF/XAML 允许用户控件的内容不透明或私有,那就好了,就像 C++ 和 C# 允许类作者将实现细节声明为私有但提供公共接口一样;很可能只有依赖属性占了公共接口的大部分。
运行项目时,ComoBox
以空白状态初始化。这是因为 SelectedIndex
属性未设置。考虑到可以建立与 SelectedItem
属性的绑定,添加 Content.SelectedIndex="0"
到 src:UserControl
似乎会起作用。但是,事实并非如此。最可能的原因是,虽然绑定发生在运行时,并且指定的路径是一个字符串,用于通过反射查找底层属性,但在设置属性时,必须在编译时已知。这是导致我寻找访问用户控件内依赖属性的其他方法的另一个问题。
接下来是第一个替代实现,即 MakeDPInherit 项目。主要区别在于对 UserControl1
的代码隐藏文件的修改,如下所示
public partial class UserControl1 : UserControl
{
public static readonly DependencyProperty SelectedIndexProperty;
static UserControl1()
{
// The DP must be set to inherit at the UserControl1 type level.
SelectedIndexProperty = ComboBox.SelectedIndexProperty.AddOwner(
typeof(UserControl1), new FrameworkPropertyMetadata() { Inherits = true });
// This line is necessary
ComboBox.SelectedIndexProperty.OverrideMetadata(typeof(ComboBox),
new FrameworkPropertyMetadata() { Inherits = true });
// NOTE: The metadata at both levels needs to be set
// to inherit so that the DP value is inherited
}
public int SelectedIndex
{
get { return (int)GetValue(SelectedIndexProperty); }
set { SetValue(SelectedIndexProperty, value); }
}
public UserControl1()
{
InitializeComponent();
}
}
更改是为了允许设置 SelectedIndex
属性。由于此依赖属性已存在于底层的 ComboBox
上,因此我们希望直接从控件可见。通过让控件注册为所有者来实现这一点。然而,仅这样做本身并不能正确设置 SelectedIndex
。这是因为默认情况下,SelectedIndex
属性不会继承在更高级别上设置的相同 DP 的值。为了启用此功能,以及在 UserControl
级别注册时设置 FrameworPropertyMetadata
的 Inherits
属性,还需要覆盖 ComboBox
级别上现有的元数据;否则,设置将不会向下传播。这些更改意味着 MainWindow.xaml 中的以下内容现在可以正常工作。
<src:UserControl1 x:Name="UC1" HorizontalAlignment="Center"
VerticalAlignment="Center" SelectedIndex="0"/>
启用 SelectedIndex
所需的代码量,加上修改底层依赖属性元数据的需求,促使我寻找更简单的机制。这包含在 Passthru 项目中。此项目对 MainWindow.xaml 和 UserControl1.xaml 没有进一步的更改,但 UserControl1
的代码隐藏变为
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
}
public int SelectedIndex
{
get { return ((ComboBox)Content).SelectedIndex; }
set { ((ComboBox)Content).SelectedIndex = value; }
}
}
这种方法只是为底层依赖属性提供了一个公共可访问的 setter 和 getter。这本质上是代理模式的应用。这明显更简单,并且有一会儿似乎是完美的解决方案。然而,虽然它解决了设置 SelectedIndex
的问题,并允许 UserControl
作为数据绑定的目标,但在它作为数据绑定的源时会出现问题。
在到目前为止的示例中,UserControl1
实际上并不是数据绑定的源。这一行
<Label Content="Look at me"
Foreground="{Binding ElementName=UC1,Path=Content.SelectedItem}"/>
建立的选定颜色与 Label
前景的绑定,位于嵌入在 UserControl
中的 ComboBox
的依赖属性之间;即,正是我们试图避免的情况。
如果向 MainWindow.xaml 添加一个额外的控件,在本例中是一个滑块,它创建了一个绑定在其自身当前值和 SelectedIndex
属性之间,那么事情就不太顺利了。发生的情况是,对 Slider
的任何更新都会反映在 ComboBox
中,但尽管是双向绑定,对 ComboBox
的更改也不会被 reciprocate。这是因为 SelectedIndex
访问器不是一个真正的依赖属性,所以当修改底层依赖属性时,这些更新不会传播到绑定,因为绑定到 UserControl1
上的公共属性,而后者没有实现 INotifyPropertyChanged
。
<Slider Height="23" Name="slider1" Width="100" Minimum="0" Maximum="2"
Value="{Binding ElementName=UC1, Path=SelectedIndex, Mode=TwoWay}"/>
一种可能的解决方法可能是将绑定目标移动到 UserControl1
,将其保留为双向,例如
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Top">
<src:UserControl1 x:Name="UC1" HorizontalAlignment="Center" VerticalAlignment="Center"
SelectedIndex="{Binding ElementName=slider1, Path=Value, Mode=TwoWay}"/>
<Label Content="Look at me"
Foreground="{Binding ElementName=UC1,Path=Content.SelectedItem}"/>
<Slider Height="23" Name="slider1" Width="100" Minimum="0" Maximum="2" Value="0"/>
</StackPanel>
请注意初始化是如何通过在 slider1
上设置 Value="0"
来完成的.
然而,这会导致编译错误,因为绑定只能针对依赖(和附加)属性建立,而在本例中,SelectedIndex
只是一个标准属性。因此,为了正确支持绑定,别无选择,只能使用某种形式的实际依赖属性。因此,MakeDPInherit 示例值得重新审视。
将上述更改应用于此示例不会导致任何编译错误,但也不会起作用!这次的问题是,即使在元数据中设置了继承属性,对于实现依赖属性(通过 AddOwner
为除实际注册它的类型之外的所有类型)的层次结构中的类型实例,如果值在层次结构中设置得更低,那么它将覆盖在更高级别设置的任何值。在本例中,当在 ComboBox
中更改选择时,这只会更改 ComboBox
的依赖属性实例的值,而值不会向上传播到 UserControl1
。由于绑定是在 UserControl1
上的依赖属性上建立的,因此没有更改可以报告。接近了,但还不是完全成功!
在研究此问题时,发现了以下 CodeProject 文章。这与前一种机制类似,但它没有为依赖属性添加额外的所有者,而是创建了一个新的依赖属性,但名称相同。额外的步骤是创建一个内部绑定,将新注册的依赖属性与子控件上的依赖属性绑定起来。此技术如下所示。这包含在 EnableBinding 示例项目中。
public partial class UserControl1 : UserControl
{
public static readonly DependencyProperty SelectedIndexProperty;
static UserControl1()
{
SelectedIndexProperty = DependencyProperty.RegisterAttached("SelectedIndex",
typeof(int), typeof(UserControl1),
new FrameworkPropertyMetadata() { BindsTwoWayByDefault = true });
}
public int SelectedIndex
{
get { return ((ComboBox)Content).SelectedIndex; }
set { ((ComboBox)Content).SelectedIndex = value; }
}
public UserControl1()
{
InitializeComponent();
Binding b = new Binding("SelectedIndex");
b.Source = this;
b.Mode = BindingMode.TwoWay;
combo.SetBinding(ComboBox.SelectedIndexProperty, b);
}
public IEnumerable ItemsSource
{
get { return ((ComboBox)Content).ItemsSource; }
set { ((ComboBox)Content).ItemsSource = value; }
}
}
与之前的示例相比,另一个变化是为 ItemsSource
依赖属性添加了 Passthru 样式实现。这是为了表明这个相当重要的属性可以这样使用。目前,它不能被绑定,但对于加载静态资源来说,它很好。因此,SolidColorBrush
数组已移至 MainWindow.xaml,并且 ItemsSource
在此处引用,如下所示
<Window x:Class="EnableBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:EnableBinding"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<x:Array x:Key="SomeBrushes" Type="{x:Type SolidColorBrush}">
<SolidColorBrush>Red</SolidColorBrush>
<SolidColorBrush>Green</SolidColorBrush>
<SolidColorBrush>Blue</SolidColorBrush>
</x:Array>
</Window.Resources>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Left" VerticalAlignment="Top">
<src:UserControl1 x:Name="UC1" HorizontalAlignment="Center"
VerticalAlignment="Center"
ItemsSource="{StaticResource SomeBrushes}" SelectedIndex="0"/>
<Label Content="Look at me"
Foreground="{Binding ElementName=UC1,Path=Content.SelectedItem}"/>
<Slider Height="23" Name="slider1" Width="100" Minimum="0"
Maximum="2"
Value="{Binding ElementName=UC1, Path=SelectedIndex, Mode=TwoWay}"/>
</StackPanel>
</Window>
为了在 Slider
和 UserControl1
之间建立双向绑定,绑定在哪一个控件上并不重要,因为两个字段都是依赖属性。在项目中,提供了这两种版本。
请注意,在解决方案中,还有一个额外的项目,它只是显示了原始示例,该示例已修改为使用同步依赖属性机制。该项目名为 MakeDPSync.
关注点
使用 UserControl
来提供自定义控件可能完全是错误的。在 WPF 提供的所有机制中,这是最不适合此特定应用程序的,我实际上不会使用它。
这些机制都不是完美的,因为它们都涉及某种妥协,无论是功能缺乏还是添加了似乎复制了底层依赖属性实现的重复代码。此外,如果需要支持多个依赖属性,那么由于所需的代码与依赖属性的实际名称紧密相关,因此很难实现通用解决方案。
如果必须使用 UserControl
并且需要修改子控件的依赖属性,那么正如本文所示,有多种选项可用。如果需要完整的绑定支持,那么唯一的真正选择是使用最后一种方法,但如果要求仅仅是设置依赖属性(无论是在编译时还是作为绑定目标),那么 Passthru 示例中所示的机制就足够了。
历史
- 2010 年 7 月 9 日 - 第一个版本。