自定义依赖属性

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

先决条件

本文假设对依赖属性有一个基本的了解,并且你已阅读 依赖项属性概述。 若要遵循本文中的示例,如果熟悉可扩展应用程序标记语言(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 资源

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

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

  • 当属性值更改时由 WPF 属性系统通知。 更改可能是由于属性系统、环境、用户或样式的动作引起的。 属性可以在属性元数据中指定回调方法,每次属性系统确定属性值发生更改时都会调用该方法。 相关的概念是属性值强制。 有关详细信息,请参阅 依赖项属性的回调和验证

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

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

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

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

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

用于创建依赖属性的清单

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

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

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

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

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

登记房产

为了使属性成为依赖属性,必须将它注册到属性系统。 若要注册属性,请在类主体内但在任何成员定义之外调用Register 方法。 该方法 Register 返回调用属性系统 API 时将使用的唯一依赖属性标识符。 在成员定义之外进行调用的原因是因为您将返回值分配给一种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 属性系统服务的一部分。 重写方法使派生类能够参与值确定。 为了避免运行时初始化的潜在问题,不应在类的构造函数中设置依赖项属性值,除非遵循特定的构造函数模式。 有关详细信息,请参阅 DependencyObjects 的安全构造函数模式

另请参阅