WPF 教程 - 依赖属性






4.87/5 (85投票s)
WPF 引入了一个由依赖属性增强的新属性系统。与 CLR 属性相比,依赖属性有许多改进。在本文中,我将讨论如何创建自己的依赖属性以及如何使用它的各种功能。
目录
引言
WPF 提供了许多普通 Windows 应用程序没有的新功能和替代方案。在我们已经讨论了 WPF 的一些功能之后,现在是时候进一步介绍其他新功能了。阅读完本教程的前几篇文章后,我希望您对 WPF 架构、边框、效果、变换、标记扩展等已经或多或少地熟悉了。如果不熟悉,请通过以下系列文章进行学习:
- WPF 教程:入门 [^]
- WPF 教程:布局面板、容器和布局变换 [^]
- WPF 教程:边框和画笔的乐趣 [^]
- WPF 教程 - TypeConverter 和 Markup Extension [^]
- WPF 教程 - 依赖属性 [^]
- WPF 教程 - 概念绑定 [^]
- WPF 教程 - 样式、触发器和动画 [^]
因此,在本文中,我将向您介绍支撑 WPF 属性系统的新的属性系统。我们将进一步介绍如何轻松地使用这些属性来生成自定义回调、创建附加属性、应用动画和样式等,所有这些都通过全新的依赖属性实现。最后,我们将讨论上一篇文章中遗留的绑定替代方案,以完成整个主题。希望您喜欢这篇文章,就像您喜欢我提供的所有教程一样。
新的属性系统
好吧,您看到这个标题一定很惊讶。是的,WPF 有一种全新的定义控件属性的技术。新属性系统的单位是依赖属性,创建依赖属性的包装类称为 DependencyObject
。我们使用它将依赖属性注册到属性系统中,以确保该对象包含该属性,并且我们可以随时轻松地获取或设置这些属性的值。我们甚至可以使用普通的 CLR 属性来包装依赖属性,并使用 GetValue
和 SetValue
来获取和设置其中传递的值。
这与 CLR 属性系统几乎相同。那么,新属性系统有什么优点呢?让我们看看依赖属性和 CLR 属性的区别。
要使用依赖属性,您必须从 DependencyObject
派生类,因为包含新属性系统的整个观察者都定义在 DependencyObject
中。
依赖属性和 CLR 属性的区别
CLR 属性只是一个包装器,用于包装 private
变量。它使用 Get
/Set
方法来检索和存储变量的值。所以,老实说,CLR 属性只为您提供了一个块,您可以在其中编写代码,在获取或设置属性时调用。
另一方面,依赖属性系统的功能非常强大。依赖属性的理念是基于其他外部输入的计算属性值。外部输入可能是样式、主题、系统属性、动画等。所以,您可以说依赖属性与 WPF 引入的大部分内置功能一起工作。

依赖属性的优点
事实上,依赖属性比普通的 CLR 属性有许多优点。在创建我们自己的依赖属性之前,让我们先讨论一下它的优点。
- 属性值继承:通过属性值继承,我们是指依赖属性的值可以在层次结构中被覆盖,以便最终设置具有最高优先级的那个值。
- 数据验证:我们可以强制在属性值被修改时自动触发数据验证。
- 参与动画:依赖属性可以进行动画处理。WPF 动画在一定时间间隔内改变值的能力很强。通过定义依赖属性,您可以最终为该属性支持动画。
- 参与样式:样式是定义控件的元素。我们可以在依赖属性上使用
Style
Setters
。 - 参与模板:模板是定义元素整体结构的元素。通过定义依赖属性,我们可以在模板中使用它。
- 数据绑定:由于每个依赖属性在属性值被修改时都会自己调用
INotifyPropertyChanged
,因此DataBinding
是内部支持的。要了解更多关于INotifyPropertyChanged
的信息,请阅读 [^] - 回调:您可以为依赖属性设置回调,以便在属性更改时会引发回调。
- 资源:依赖属性可以获取资源。因此,在 XAML 中,您可以为依赖属性的定义定义一个资源。
- 元数据覆盖:您可以使用
PropertyMetaData
为依赖属性定义特定行为。因此,覆盖派生属性的元数据不需要您重新定义或重新实现整个属性定义。 - 设计器支持:依赖属性获得 Visual Studio 设计器的支持。您可以在设计器的属性窗口中看到控件的所有依赖属性列表。
其中,一些功能仅受依赖属性支持。动画、样式、Templates
、属性值继承等只能通过依赖属性参与。如果您在这种情况下使用 CLR 属性,编译器将生成错误。
如何定义依赖属性
现在来看实际代码,让我们看看如何定义依赖属性。
public static readonly DependencyProperty MyCustomProperty =
DependencyProperty.Register("MyCustom", typeof(string), typeof(Window1));
public string MyCustom
{
get
{
return this.GetValue(MyCustomProperty) as string;
}
set
{
this.SetValue(MyCustomProperty, value);
}
}
在上面的代码中,我只定义了一个依赖属性。您可能会对为什么依赖属性要声明为 static
感到惊讶。是的,就像您一样,我第一次看到时也很惊讶。但后来,在阅读了关于依赖属性的知识后,我了解到依赖属性是在类级别维护的,所以您可以说类 A 拥有属性 B。所以属性 B 将被维护到类 A 的所有对象中。依赖属性因此为类 A 维护的所有属性创建了一个观察者并存储在那里。因此,重要的是要注意,依赖属性应该被维护为 static
。
依赖属性的命名约定规定,它应该具有与作为第一个参数传递的包装器属性相同的名称。因此,在本例中,我们将在程序中使用包装器 "MyCustom"
的名称,并将其作为 Register
方法的第一个参数传递,并且依赖属性的名称始终应在原始包装器键后面加上 Property
。因此,在本例中,依赖属性的名称是 MyCustomProperty
。如果您不遵循此规则,某些功能将在您的程序中异常运行。
还应该注意的是,您不应该在 Wrapper
属性中编写逻辑,因为它不会在每次调用属性时都被调用。它将内部调用 GetValue
和 SetValue
。因此,如果您希望在获取依赖属性时编写自己的逻辑,可以使用回调来实现。
定义属性的元数据
在定义了最简单的依赖属性之后,让我们对其进行一些增强。要为 DependencyProperty
添加元数据,我们使用 PropertyMetaData
类的对象。如果您在一个 FrameworkElement
中(例如我正在一个 UserControl
或 Window
中),您可以使用 FrameworkMetaData
而不是 PropertyMetaData
。让我们看看如何编码。
static FrameworkPropertyMetadata propertymetadata =
new FrameworkPropertyMetadata("Comes as Default",
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault |
FrameworkPropertyMetadataOptions.Journal,new
PropertyChangedCallback(MyCustom_PropertyChanged),
new CoerceValueCallback(MyCustom_CoerceValue),
false, UpdateSourceTrigger.PropertyChanged);
public static readonly DependencyProperty MyCustomProperty =
DependencyProperty.Register("MyCustom", typeof(string), typeof(Window1),
propertymetadata, new ValidateValueCallback(MyCustom_Validate));
private static void MyCustom_PropertyChanged(DependencyObject dobj,
DependencyPropertyChangedEventArgs e)
{
//To be called whenever the DP is changed.
MessageBox.Show(string.Format(
"Property changed is fired : OldValue {0} NewValue : {1}", e.OldValue, e.NewValue));
}
private static object MyCustom_CoerceValue(DependencyObject dobj, object Value)
{
//called whenever dependency property value is reevaluated. The return value is the
//latest value set to the dependency property
MessageBox.Show(string.Format("CoerceValue is fired : Value {0}", Value));
return Value;
}
private static bool MyCustom_Validate(object Value)
{
//Custom validation block which takes in the value of DP
//Returns true / false based on success / failure of the validation
MessageBox.Show(string.Format("DataValidation is Fired : Value {0}", Value));
return true;
}
public string MyCustom
{
get
{
return this.GetValue(MyCustomProperty) as string;
}
set
{
this.SetValue(MyCustomProperty, value);
}
}
这样就更加详细了。我们定义了一个 FrameworkMetaData
,其中我们为 Dependency
属性指定了 DefaultValue
为“Comes as Default”,因此,如果我们不重置 DependencyProperty
的值,该对象将具有此默认值。FrameworkPropertyMetaDataOption
让您有机会评估依赖属性的各种元数据选项。让我们看看枚举的各种选项。
AffectsMeasure
:为对象放置的Layout
元素调用AffectsMeasure
。AffectsArrange
:为布局元素调用AffectsArrange
。AffectsParentMeasure
:为父元素调用AffectsMeasure
。AffectsParentArrange
:为父控件调用AffectsArrange
。AffectsRender
:当值被修改时重新渲染控件。NotDataBindable
:可以禁用Databinding
。BindsTwoWayByDefault
:默认情况下,数据绑定是OneWay
。如果您希望您的属性具有TwoWay
默认绑定,可以使用此选项。Inherits
:它确保子控件继承其基类的值。
您可以使用 | 分隔符组合使用多个选项,就像我们处理标志一样。
PropertyChangedCallback
在属性值更改时被调用。因此,它将在实际值被修改后被调用。CoerceValue
在实际值被修改之前被调用。这意味着在调用 CoerceValue
之后,我们将从它返回的值将分配给属性。验证块将在 CoerceValue
之前被调用,所以这个事件确保传入属性的值是否有效。根据有效性,您需要返回 true
或 false
。如果值为 false
,运行时将生成一个错误。
因此,在上面的应用程序中,运行代码后,您会看到以下 MessageBox
:
ValidateCallback
:您需要在此处放置逻辑来验证作为 Value 参数传入的数据。True
使其接受该值,false
将抛出错误。CoerceValue
:可以根据作为参数传递的值来修改或更改值。它还接收DependencyObject
作为参数。您可以通过与DependencyProperty
关联的CoerceValue
方法调用CoerceValueCallback
。PropertyChanged
:这是您看到的最后一个Messagebox
,它将在值完全修改后被调用。您可以从DependencyPropertyChangedEventArgs
中获取OldValue
和NewValue
。
关于 CollectionType 依赖属性的说明
CollectionType
依赖属性用于当您想将 DependencyObject
的集合保存在一个集合中时。在我们的项目中,我们经常需要这个。通常,当您创建一个依赖对象并向其中传递默认值时,该值不会是您创建的每个对象的默认值。相反,它将是该类型注册的依赖属性的初始值。因此,如果您想创建一个 Dependency
对象集合,并希望您的对象拥有自己的默认值,您需要为依赖集合的每个单独项分配此值,而不是使用 Metadata
定义。例如:
public static readonly DependencyPropertyKey ObserverPropertyKey =
DependencyProperty.RegisterReadOnly("Observer", typeof(ObservableCollection<Button>),
typeof(MyCustomUC),new FrameworkPropertyMetadata(new ObservableCollection<Button>()));
public static readonly DependencyProperty ObserverProperty =
ObserverPropertyKey.DependencyProperty;
public ObservableCollection<Button> Observer
{
get
{
return (ObservableCollection<Button>)GetValue(ObserverProperty);
}
}
在上面的代码中,您可以看到我们使用 RegisterReadonly
方法声明了一个 DependencyPropertyKey
。ObservableCollection
实际上是一个 Button
的集合,而 Button
最终是一个 DependencyObject
。
现在,如果您使用此集合,您会发现当您创建 Usercontrol
对象时,每个对象都引用相同的 Dependency
Property,而不是拥有自己的依赖属性。根据定义,每个 dependencyproperty
都使用其类型分配内存,因此如果对象 2 创建 DependencyProperty
的新实例,它将覆盖对象 1 的集合。因此,该对象将充当 Singleton
类。要克服这种情况,您需要在创建类的每个新对象时使用新实例重置集合。由于属性是 readonly
的,您需要使用 SetValue
来使用 DependencyPropertyKey
创建新实例。
public MyCustomUC()
{
InitializeComponent();
SetValue(ObserverPropertyKey, new ObservableCollection<Button>());
}
因此,对于每个实例,集合都会重置,因此您将看到为每个创建的 UserControl
提供的唯一集合。
属性值继承
DependencyProperty
支持 Property
值继承。根据定义,在您创建 DependencyObject
后,您可以通过与 DependencyProperty
关联的 AddOwner
方法轻松地将 DependencyProperty
继承到其所有子控件。
每个 DependencyProperty
都有 AddOwner
方法,该方法创建一个链接到另一个已定义的 DependencyProperty
。假设您有一个 DependencyObject
A,它有一个名为 Width
的属性。您希望 DependencyObject
B 的值继承 A 的值。
public class A :DependencyObject
{
public static readonly DependencyProperty HeightProperty =
DependencyProperty.Register("Height", typeof(int), typeof(A),
new FrameworkPropertyMetadata(0,
FrameworkPropertyMetadataOptions.Inherits));
public int Height
{
get
{
return (int)GetValue(HeightProperty);
}
set
{
SetValue(HeightProperty, value);
}
}
public B BObject { get; set; }
}
public class B : DependencyObject
{
public static readonly DependencyProperty HeightProperty;
static B()
{
HeightProperty = A.HeightProperty.AddOwner(typeof(B),
new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.Inherits));
}
public int Height
{
get
{
return (int)GetValue(HeightProperty);
}
set
{
SetValue(HeightProperty, value);
}
}
}
在上面的代码中,您可以看到类 B
使用 AddOwner
继承了 Height
DependencyProperty
,而无需在类中重新声明它。因此,当声明 A
时,如果您指定 A
的高度,它将自动传输到继承的子对象 B
。
这与普通对象相同。当您指定 Window
的 Foreground
时,它会自动继承到所有子元素,因此每个控件的 Foreground
都将表现相同。
更新
尽管 PropertyValueInhertence
对任何依赖属性都存在,但它实际上适用于 AttachedProperties
。我了解到属性值继承仅在属性被视为附加属性时才有效。如果您为附加属性设置了默认值,并且还设置了 FrameworkMetaData.Inherits
,属性值将自动从父级继承到子级,并且还允许子级修改内容。有关更多详细信息,请查看 MSDN [^]。因此,我上面提供的示例不适合属性值继承,但您可以在阅读下一部分后轻松创建一个来查看。
附加属性
附加属性是另一个有趣的概念。附加属性允许您将一个属性附加到一个与该对象完全无关的对象上,从而允许您使用该对象为其定义值。听起来有点令人困惑,是吗?是的,让我们看一个例子。
假设您已声明了一个 DockPanel
,您希望在其中显示控件。现在 DockPanel
注册了一个 AttachedProperty
。
public static readonly DependencyProperty DockProperty =
DependencyProperty.RegisterAttached("Dock", typeof(Dock), typeof(DockPanel),
new FrameworkPropertyMetadata(Dock.Left,
new PropertyChangedCallback(DockPanel.OnDockChanged)),
new ValidateValueCallback(DockPanel.IsValidDock));
您可以看到,DockProperty
在 DockPanel
内部被定义为 Attached
。我们使用 RegisterAttached
方法来注册附加的 DependencyProperty
。因此,DockPanel
的任何 UIElement
子级都将附加 Dock
属性,因此它可以定义其值,该值会自动传播到 DockPanel
。
让我们声明一个 Attached DependencyProperty
。
public static readonly DependencyProperty IsValuePassedProperty =
DependencyProperty.RegisterAttached("IsValuePassed", typeof(bool), typeof(Window1),
new FrameworkPropertyMetadata(new PropertyChangedCallback(IsValuePassed_Changed)));
public static void SetIsValuePassed(DependencyObject obj, bool value)
{
obj.SetValue(IsValuePassedProperty, value);
}
public static bool GetIsValuePassed(DependencyObject obj)
{
return (bool)obj.GetValue(IsValuePassedProperty);
}
在这里,我声明了一个 DependencyObject
,它持有一个值 IsValuePassed
。该对象绑定到 Window1
,因此您可以从任何 UIElement
将值传递给 Window1
。
因此,在我的代码中,UserControl
可以将属性值传递给 Window
。
<local:MyCustomUC x:Name="ucust" Grid.Row="0" local:Window1.IsValuePassed="true"/>
您可以看到上面的 IsValuePassed
可以从外部 UserControl
设置,并且相同的值将传递到实际的窗口。如您所见,我添加了两个 static
方法来单独 Set
或 Get
对象的值。这将在代码中使用,以确保我们从适当的对象传递值。例如,如果您添加一个按钮并希望从代码中传递值,在这种情况下,static
方法将非常有用。
private void Button_Click(object sender, RoutedEventArgs e)
{
Window1.SetIsValuePassed(this, !(bool)this.GetValue(IsValuePassedProperty));
}
同样,DockPanel
定义了 SetDock
方法。
结论
总之,DependencyProperty
是您在处理 WPF 之前必须了解的最重要和最有趣的概念之一。在某些情况下,您会想要声明一个 DependencyProperty
。从本文开始,我基本上回顾了您可以使用的 DependencyProperty
的每个部分。希望这篇文章对您有所帮助。感谢您的阅读。期待您的反馈。