自定义依赖属性 (WPF .NET)

Windows Presentation Foundation (WPF) 应用程序开发人员和组件创建者可以创建自定义依赖属性来扩展其属性的功能。 与公共语言运行时 (CLR) 属性不同,依赖属性添加了对样式、数据绑定、继承、动画和默认值的支持。 BackgroundWidthText 是 WPF 类中现有依赖属性的示例。 本文介绍如何实现自定义依赖属性,并提供用于提高性能、可用性和多样性的选项。

重要

面向 .NET 7 和 .NET 6 的桌面指南文档正在撰写中。

先决条件

本文假定你对依赖属性有基本的了解,并且已阅读依赖属性概述。 若要理解本文中的示例,还应当熟悉 Extensible Application Markup Language (XAML) 并知道如何编写 WPF 应用程序。

依赖属性标识符

依赖属性是通过 RegisterRegisterReadOnly 调用向 WPF 属性系统注册的属性。 Register 方法返回 DependencyProperty 实例,该实例保存依赖属性的已注册名称和特征。 你会将 DependencyProperty 实例分配到一个静态只读字段(称为依赖属性标识符),该字段按约定命名为 <property name>Property。 例如,Background 属性的标识符字段始终为 BackgroundProperty

依赖属性标识符用作用于获取或设置属性值的支持字段,而不是通过私有字段支持属性的标准模式。 属性系统不仅使用标识符,XAML 处理器也可以使用它,代码(并且可能是外部代码)可以通过标识符访问依赖属性。

依赖属性只能应用于派生自 DependencyObject 类型的类。 大多数 WPF 类都支持依赖属性,因为 DependencyObject 靠近 WPF 类层次结构的根。 有关依赖属性以及用于描述它们的术语和约定,请参阅依赖属性概述

依赖属性包装器

不是附加属性的 WPF 依赖属性通过实现 getset 访问器的 CLR 包装器进行公开。 通过使用属性包装器,依赖属性的使用者可以获取或设置依赖属性值,就像获取或设置任何其他 CLR 属性一样。 getset 访问器通过 DependencyObject.GetValueDependencyObject.SetValue 调用与底层属性系统交互,并以参数的形式传入依赖属性标识符。 依赖属性的使用者通常不会直接调用 GetValueSetValue,但如果要实现自定义依赖属性,则会在包装器中使用这些方法。

何时实现依赖属性

对派生自 DependencyObject 的类实现属性时,可以通过使用 DependencyProperty 标识符来支持属性使其成为依赖属性。 创建依赖属性是否有益取决于你的方案。 尽管通过私有字段支持属性足够用于某些方案,但如果希望属性支持以下一个或多个 WPF 功能,请考虑实现依赖属性:

  • 可在样式中设置的属性。 有关详细信息,请参阅样式和模板

  • 支持数据绑定的属性。 有关数据绑定依赖属性的详细信息,请参阅绑定两个控件的属性

  • 可通过动态资源引用设置的属性。 有关详细信息,请参阅 XAML 资源

  • 自动从元素树中的父元素继承其值的属性。 为此,即使还会创建属性包装器以进行 CLR 访问,也需要使用 RegisterAttached 进行注册。 有关详细信息,请参阅属性值继承

  • 可进行动画处理的属性。 有关详细信息,请参阅动画概述

  • 当属性值发生更改时,WPF 属性系统进行的通知。 更改可能是由属性系统、环境、用户或样式执行的操作造成的。 属性可以在属性元数据中指定回调方法,每次属性系统确定属性值已更改时会调用此回调方法。 与此相关的一个概念是属性值强制转换。 有关详细信息,请参阅依赖属性回调和验证

  • 对 WPF 进程读取的依赖属性元数据的访问。 例如,可以使用属性元数据:

    • 指定更改的依赖属性值是否应使布局系统重新安排元素的视觉对象。

    • 通过替代派生类的元数据来设置依赖属性的默认值。

  • Visual Studio WPF 设计器支持,例如在“属性”窗口中编辑自定义控件的属性。 有关详细信息,请参阅控件创作概述

对于某些方案,替代现有依赖属性的元数据是比实现新依赖属性更好的选项。 元素据替代是否可行取决于方案以及方案与现有 WPF 依赖属性和类的实现的相似度。 有关替代现有依赖属性上的元素据的详细信息,请参阅依赖属性元素据

创建依赖属性的检查清单

按照以下步骤创建依赖属性。 某些步骤可以合并,在单行代码中并实现。

  1. (可选)创建依赖属性元数据。

  2. 将依赖属性注册到属性系统,指定属性名称、所有者类型、属性值类型以及(可选)属性元数据。

  3. 在所有者类型上将 DependencyProperty 标识符定义为 public static readonly 字段。 标识符字段名称是追加了 Property 后缀的属性名称。

  4. 定义名称与依赖属性名称相同的 CLR 包装器属性。 在 CLR 包装器中,实现 getset 访问器,它们与支持包装器的依赖属性连接。

注册属性

若要使属性成为依赖属性,必须将它注册到属性系统。 若要注册属性,请在类的主体中(但在任何成员定义之外)调用 Register 方法。 Register 方法会返回调用属性系统 API 时将使用的唯一依赖属性标识符。 在成员定义之外进行 Register 调用的原因是会将返回值分配给 DependencyProperty 类型的 public static readonly 字段。 将在类中创建的此字段是依赖属性的标识符。 在以下示例中,Register 的第一个参数将依赖属性命名为 AquariumGraphic

// Register a dependency property with the specified property name,
// property type, owner type, and property metadata. Store the dependency
// property identifier as a public static readonly member of the class.
public static readonly DependencyProperty AquariumGraphicProperty =
    DependencyProperty.Register(
      name: "AquariumGraphic",
      propertyType: typeof(Uri),
      ownerType: typeof(Aquarium),
      typeMetadata: new FrameworkPropertyMetadata(
          defaultValue: new Uri("http://www.contoso.com/aquarium-graphic.jpg"),
          flags: FrameworkPropertyMetadataOptions.AffectsRender,
          propertyChangedCallback: new PropertyChangedCallback(OnUriChanged))
    );
' Register a dependency property with the specified property name,
' property type, owner type, and property metadata. Store the dependency
' property identifier as a public static readonly member of the class.
Public Shared ReadOnly AquariumGraphicProperty As DependencyProperty =
    DependencyProperty.Register(
        name:="AquariumGraphic",
        propertyType:=GetType(Uri),
        ownerType:=GetType(Aquarium),
        typeMetadata:=New FrameworkPropertyMetadata(
            defaultValue:=New Uri("http://www.contoso.com/aquarium-graphic.jpg"),
            flags:=FrameworkPropertyMetadataOptions.AffectsRender,
            propertyChangedCallback:=New PropertyChangedCallback(AddressOf OnUriChanged)))

注意

在类的主体中定义依赖属性是典型的实现,但也可以在类静态构造函数中定义依赖属性。 需要多行代码来初始化依赖属性时,此方法会很有用。

依赖属性命名

对于属性系统的正常行为,为依赖属性建立的命名约定是必需的。 创建的标识符字段的名称必须是属性的已注册名称并且具有后缀 Property

依赖属性名称在注册类中必须是唯一的。 通过基类型继承的依赖属性已注册,无法由派生类型注册。 但是,可以通过将你的类添加为依赖属性的所有者,使用通过不同类型(甚至是你的类不继承的类型)注册的依赖属性。 有关将类添加为所有者的详细信息,请参阅依赖属性元数据

实现属性包装器

按照约定,包装器属性的名称必须与 Register 调用的第一个参数(这是依赖属性名称)相同。 包装器实现会在 get 访问器中调用 GetValue,并在 set 访问器中调用 SetValue(用于读写属性)。 以下示例演示一个包装器(在注册调用和标识符字段声明后面)。 WPF 类上的所有公共依赖属性都使用类似的包装器模型。

// Register a dependency property with the specified property name,
// property type, owner type, and property metadata. Store the dependency
// property identifier as a public static readonly member of the class.
public static readonly DependencyProperty AquariumGraphicProperty =
    DependencyProperty.Register(
      name: "AquariumGraphic",
      propertyType: typeof(Uri),
      ownerType: typeof(Aquarium),
      typeMetadata: new FrameworkPropertyMetadata(
          defaultValue: new Uri("http://www.contoso.com/aquarium-graphic.jpg"),
          flags: FrameworkPropertyMetadataOptions.AffectsRender,
          propertyChangedCallback: new PropertyChangedCallback(OnUriChanged))
    );

// Declare a read-write property wrapper.
public Uri AquariumGraphic
{
    get => (Uri)GetValue(AquariumGraphicProperty);
    set => SetValue(AquariumGraphicProperty, value);
}
' Register a dependency property with the specified property name,
' property type, owner type, and property metadata. Store the dependency
' property identifier as a public static readonly member of the class.
Public Shared ReadOnly AquariumGraphicProperty As DependencyProperty =
    DependencyProperty.Register(
        name:="AquariumGraphic",
        propertyType:=GetType(Uri),
        ownerType:=GetType(Aquarium),
        typeMetadata:=New FrameworkPropertyMetadata(
            defaultValue:=New Uri("http://www.contoso.com/aquarium-graphic.jpg"),
            flags:=FrameworkPropertyMetadataOptions.AffectsRender,
            propertyChangedCallback:=New PropertyChangedCallback(AddressOf OnUriChanged)))

' Declare a read-write property wrapper.
Public Property AquariumGraphic As Uri
    Get
        Return CType(GetValue(AquariumGraphicProperty), Uri)
    End Get
    Set
        SetValue(AquariumGraphicProperty, Value)
    End Set
End Property

除了在极少数情况下,包装器实现应仅包含 GetValueSetValue 代码。 有关这背后的原因,请参阅自定义依赖属性的影响

如果属性未遵循建立的命名约定,则可能会遇到以下问题:

  • 样式和模板的某些方面不起作用。

  • 大多数工具和设计器依赖命名约定来正确序列化 XAML 并在每个属性级别提供设计器环境帮助。

  • WPF XAML 加载程序的当前实现会完全跳过包装器,并依赖于命名约定来处理特性值。 有关详细信息,请参阅 XAML 加载和依赖属性

依赖属性元数据

注册依赖属性时,属性系统会创建一个存储属性特征的元数据对象。 通过 Register 方法的重载可以在注册期间指定属性元数据,例如 Register(String, Type, Type, PropertyMetadata)。 属性元数据的常见用途是为使用依赖属性的新实例应用自定义默认值。 如果未提供属性元数据,则属性系统会将默认值分配给许多依赖属性特征。

如果要对派生自 FrameworkElement 的类创建依赖属性,则可以使用更专业的元数据类 FrameworkPropertyMetadata,而不是其基类 PropertyMetadata。 通过多个 FrameworkPropertyMetadata 构造函数签名可指定元数据特征的不同组合。 如果只想指定默认值,请使用 FrameworkPropertyMetadata(Object) 并将默认值传递给 Object 参数。 确保值类型与 Register 调用中指定的 propertyType 匹配。

通过某些 FrameworkPropertyMetadata 重载可以为属性指定元数据选项标志。 属性系统会将这些标志转换为离散属性,标志值会由 WPF 进程(如布局引擎)使用。

设置元数据标志

设置元数据标志时,请考虑以下事项:

  • 如果属性值(或对其进行的更改)会影响布局系统呈现 UI 元素的方式,则设置以下一个或多个标志:

    • AffectsMeasure,指示属性值的更改需要更改 UI 呈现,尤其是对象在其父级内占用的空间。 例如,为 Width 属性设置此元数据标志。

    • AffectsArrange,指示属性值的更改需要更改 UI 呈现,尤其是对象在其父级内的位置。 通常,对象不会也更改大小。 例如,为 Alignment 属性设置此元数据标志。

    • AffectsRender,指出发生了不影响布局和度量值的更改,但仍需要其他呈现方式。 例如,为 Background 属性或影响元素颜色的任何其他属性设置此标志。

    还可以将这些标志用作属性系统(或布局)回调的替代实现的输入。 例如,当实例的属性报告值更改并在元数据中设置了 AffectsArrange 时,可以使用 OnPropertyChanged 回调调用 InvalidateArrange

  • 某些属性会以其他方式影响其父元素的呈现特征。 例如,对 MinOrphanLines 属性的更改可以更改流文档的整体呈现。 可在自己的属性中使用 AffectsParentArrangeAffectsParentMeasure 表示父操作。

  • 默认情况下,依赖属性支持数据绑定。 但是,当数据绑定没有现实方案时,或者数据绑定性能有问题(例如针对大型对象)时,可以使用 IsDataBindingAllowed 禁用数据绑定。

  • 尽管依赖属性的默认数据绑定模式OneWay,但可以将特定绑定的绑定模式更改为 TwoWay。 有关详细信息,请参阅绑定方向。 作为依赖属性作者,你甚至可以选择将双向绑定设置为默认模式。 使用双向数据绑定的现有依赖属性的示例是 MenuItem.IsSubmenuOpen,它具有基于其他属性和方法调用的状态。 IsSubmenuOpen 的应用场景为:其设置逻辑和 MenuItem 合成与默认主题样式交互。 TextBox.Text 是默认情况下使用双向绑定的另一个 WPF 依赖属性。

  • 还可以通过设置 Inherits 标志为依赖属性启用属性继承。 属性继承对于父元素和子元素具有共同属性的情况非常有用,它使子元素可以继承共同属性的父值。 可继承属性的示例是 DataContext,它支持使用主-从方案进行数据呈现的绑定操作。 通过属性值继承可以在页面或应用程序根处指定数据上下文,从而不必为子元素绑定指定数据上下文。 尽管继承的属性值会替代默认值,但可以在任何子元素上本地设置属性值。 请谨慎使用属性值继承,因为它具有性能成本。 有关详细信息,请参阅属性值继承

  • 设置 Journal 标志,以指示导航日志服务是否应该检测或使用依赖属性。 例如,SelectedIndex 属性设置 Journal 标志,以建议应用程序保留所选项的日志历史记录。

只读依赖项属性

可以定义只读的依赖属性。 典型方案是存储内部状态的依赖属性。 例如,IsMouseOver 是只读的,因为其状态只应由鼠标输入确定。 有关详细信息,请参阅只读依赖属性

集合类型依赖属性

集合类型依赖属性具有需要考虑的额外实现问题,例如为集合元素设置引用类型和数据绑定支持的默认值。 有关详细信息,请参阅集合类型依赖属性

依赖项属性的安全性

通常会将依赖属性声明为公共属性,并将 DependencyProperty 标识符字段声明为 public static readonly 字段。 如果指定限制性更高的访问级别(例如 protected),仍可通过将其标识符与属性系统 API 结合使用来访问依赖属性。 甚至可以通过 WPF 元数据报告或值确定 API(例如 LocalValueEnumerator)访问受保护的标识符字段。 有关详细信息,请参阅依赖属性安全性

对于只读依赖属性,从 RegisterReadOnly 返回的值是 DependencyPropertyKey,通常不会使 DependencyPropertyKey 成为类的 public 成员。 由于 WPF 属性系统不会在代码外部传播 DependencyPropertyKey,因此只读依赖属性具有比读写依赖属性更好的 set 安全性。

依赖属性和类构造函数

托管代码编程(通常通过代码分析工具强制执行)的一般原则是:类构造函数不应调用虚拟方法。 这是因为基构造函数可以在派生类构造函数的初始化期间进行调用,并且基构造函数调用的虚拟方法可能会在完成派生类的初始化之前运行。 从已派生自 DependencyObject 的类进行派生时,属性系统本身会在内部调用和公开虚拟方法。 这些虚拟方法属于 WPF 属性系统服务。 替代方法会使派生类参与值确定。 为避免运行时初始化出现潜在问题,不应该在类的构造函数中设置依赖属性值,除非遵循特定的构造函数模式进行操作。 有关详细信息,请参阅 DependencyObject 的安全构造函数模式

另请参阅