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

WPF 教程 - 依赖属性

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (85投票s)

2010年12月28日

CPOL

12分钟阅读

viewsIcon

425441

downloadIcon

9608

WPF 引入了一个由依赖属性增强的新属性系统。与 CLR 属性相比,依赖属性有许多改进。在本文中,我将讨论如何创建自己的依赖属性以及如何使用它的各种功能。

目录

引言

WPF 提供了许多普通 Windows 应用程序没有的新功能和替代方案。在我们已经讨论了 WPF 的一些功能之后,现在是时候进一步介绍其他新功能了。阅读完本教程的前几篇文章后,我希望您对 WPF 架构、边框、效果、变换、标记扩展等已经或多或少地熟悉了。如果不熟悉,请通过以下系列文章进行学习:

因此,在本文中,我将向您介绍支撑 WPF 属性系统的新的属性系统。我们将进一步介绍如何轻松地使用这些属性来生成自定义回调、创建附加属性、应用动画和样式等,所有这些都通过全新的依赖属性实现。最后,我们将讨论上一篇文章中遗留的绑定替代方案,以完成整个主题。希望您喜欢这篇文章,就像您喜欢我提供的所有教程一样。

新的属性系统

好吧,您看到这个标题一定很惊讶。是的,WPF 有一种全新的定义控件属性的技术。新属性系统的单位是依赖属性,创建依赖属性的包装类称为 DependencyObject。我们使用它将依赖属性注册到属性系统中,以确保该对象包含该属性,并且我们可以随时轻松地获取或设置这些属性的值。我们甚至可以使用普通的 CLR 属性来包装依赖属性,并使用 GetValueSetValue 来获取和设置其中传递的值。

这与 CLR 属性系统几乎相同。那么,新属性系统有什么优点呢?让我们看看依赖属性和 CLR 属性的区别。

要使用依赖属性,您必须从 DependencyObject 派生类,因为包含新属性系统的整个观察者都定义在 DependencyObject 中。

依赖属性和 CLR 属性的区别

CLR 属性只是一个包装器,用于包装 private 变量。它使用 Get/Set 方法来检索和存储变量的值。所以,老实说,CLR 属性只为您提供了一个块,您可以在其中编写代码,在获取或设置属性时调用。

另一方面,依赖属性系统的功能非常强大。依赖属性的理念是基于其他外部输入的计算属性值。外部输入可能是样式、主题、系统属性、动画等。所以,您可以说依赖属性与 WPF 引入的大部分内置功能一起工作。

mbos.JPG

依赖属性的优点

事实上,依赖属性比普通的 CLR 属性有许多优点。在创建我们自己的依赖属性之前,让我们先讨论一下它的优点。

  1. 属性值继承:通过属性值继承,我们是指依赖属性的值可以在层次结构中被覆盖,以便最终设置具有最高优先级的那个值。
  2. 数据验证:我们可以强制在属性值被修改时自动触发数据验证。
  3. 参与动画:依赖属性可以进行动画处理。WPF 动画在一定时间间隔内改变值的能力很强。通过定义依赖属性,您可以最终为该属性支持动画。
  4. 参与样式:样式是定义控件的元素。我们可以在依赖属性上使用 Style Setters
  5. 参与模板:模板是定义元素整体结构的元素。通过定义依赖属性,我们可以在模板中使用它。
  6. 数据绑定:由于每个依赖属性在属性值被修改时都会自己调用 INotifyPropertyChanged,因此 DataBinding 是内部支持的。要了解更多关于 INotifyPropertyChanged 的信息,请阅读 [^]
  7. 回调:您可以为依赖属性设置回调,以便在属性更改时会引发回调。
  8. 资源:依赖属性可以获取资源。因此,在 XAML 中,您可以为依赖属性的定义定义一个资源。
  9. 元数据覆盖:您可以使用 PropertyMetaData 为依赖属性定义特定行为。因此,覆盖派生属性的元数据不需要您重新定义或重新实现整个属性定义。
  10. 设计器支持:依赖属性获得 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 属性中编写逻辑,因为它不会在每次调用属性时都被调用。它将内部调用 GetValueSetValue。因此,如果您希望在获取依赖属性时编写自己的逻辑,可以使用回调来实现。

定义属性的元数据

在定义了最简单的依赖属性之后,让我们对其进行一些增强。要为 DependencyProperty 添加元数据,我们使用 PropertyMetaData 类的对象。如果您在一个 FrameworkElement 中(例如我正在一个 UserControlWindow 中),您可以使用 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 之前被调用,所以这个事件确保传入属性的值是否有效。根据有效性,您需要返回 truefalse。如果值为 false,运行时将生成一个错误。

因此,在上面的应用程序中,运行代码后,您会看到以下 MessageBox

  • ValidateCallback:您需要在此处放置逻辑来验证作为 Value 参数传入的数据。True 使其接受该值,false 将抛出错误。
  • CoerceValue:可以根据作为参数传递的值来修改或更改值。它还接收 DependencyObject 作为参数。您可以通过与 DependencyProperty 关联的 CoerceValue 方法调用 CoerceValueCallback
  • PropertyChanged:这是您看到的最后一个 Messagebox,它将在值完全修改后被调用。您可以从 DependencyPropertyChangedEventArgs 中获取 OldValueNewValue

关于 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 方法声明了一个 DependencyPropertyKeyObservableCollection 实际上是一个 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

这与普通对象相同。当您指定 WindowForeground 时,它会自动继承到所有子元素,因此每个控件的 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));

您可以看到,DockPropertyDockPanel 内部被定义为 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 方法来单独 SetGet 对象的值。这将在代码中使用,以确保我们从适当的对象传递值。例如,如果您添加一个按钮并希望从代码中传递值,在这种情况下,static 方法将非常有用。

private void Button_Click(object sender, RoutedEventArgs e)
{
     Window1.SetIsValuePassed(this, !(bool)this.GetValue(IsValuePassedProperty));
}

同样,DockPanel 定义了 SetDock 方法。

结论

总之,DependencyProperty 是您在处理 WPF 之前必须了解的最重要和最有趣的概念之一。在某些情况下,您会想要声明一个 DependencyProperty。从本文开始,我基本上回顾了您可以使用的 DependencyProperty 的每个部分。希望这篇文章对您有所帮助。感谢您的阅读。期待您的反馈。

© . All rights reserved.