自定义依赖项属性

本主题介绍 Windows Presentation Foundation (WPF) 应用程序开发人员和组件作者想要创建自定义依赖属性的原因,属性的实现步骤以及一些可以提高属性性能、可用性或通用性的实现选项。

先决条件

本主题假定你作为 WPF 类的现有依赖属性的使用者已经对依赖属性有所了解,并且已经阅读了依赖属性概述主题。 为了能理解本主题中的示例,还应了解 XAML 并知道如何编写 WPF 应用程序。

什么是依赖属性?

可以通过启用原本是公共语言运行时 (CLR) 属性的属性来支持样式设置、数据绑定、继承、动画和默认值,方法是将该属性作为依赖属性进行实现。 依赖属性是通过调用 Register 方法(或 RegisterReadOnly)在 WPF 属性系统中注册,并且受 DependencyProperty 标识符字段支持的属性。 只有 DependencyObject 类型可以使用依赖属性,但是 DependencyObject 在 WPF 类层次结构中级别很高,因此 WPF 中大部分可用的类都支持依赖属性。 若要详细了解依赖属性和此 SDK 中对依赖属性进行描述所使用的一些术语和约定,请参阅依赖属性概述

依赖属性示例

WPF 类上实现的依赖属性示例包括 Background 属性、Width 属性、Text 属性以及其他众多属性。 类公开的每个依赖属性都有一个在相同类上公开且类型为 DependencyProperty 的相应公共静态字段。 这是依赖属性的标识符。 此标识符的命名约定为:依赖属性名称后面加上字符串 Property。 例如,Background 属性对应的 DependencyProperty 标识符字段为 BackgroundProperty。 标识符存储了注册依赖属性时的相关信息,之后此标识符用于涉及依赖属性的其他操作,例如调用 SetValue

依赖属性概述中所述,因为“包装器”实现,WPF 中的所有依赖属性(大多数附加属性除外)也是 CLR 属性。 因此,在代码中通过调用定义包装器的 CLR 访问器(调用方法与使用其他 CLR 属性相同)可以获取或设置依赖属性。 已建立依赖属性的使用者通常不会使用 DependencyObject 方法 GetValueSetValue(这二者为基础属性系统的连接点)。 相反,CLR 属性的现有实现已经使用相应的标识符字段在属性的 getset 包装器实现中调用 GetValueSetValue。 若要自己实现自定义依赖属性,则需要使用类似的方法定义包装器。

应该何时实现依赖属性?

在类上实现属性时,只要类派生自 DependencyObject,就可以选择使用 DependencyProperty 标识符支持属性,从而使其成为依赖属性。 不必总是将属性实现为依赖属性,这不一定合适,具体取决于方案需要。 有时,使用私有字段支持属性的通常方法已足够满足需求。 但是,如果要使属性支持以下一个或多个 WPF 功能,则应该将属性实现为依赖属性:

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

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

  • 需要可以使用动态资源引用设置属性。 有关详细信息,请参阅 XAML 资源

  • 需要从元素树中的父元素自动继承属性值。 这种情况下,即使为 CLR 访问也创建了属性包装器,也应该使用 RegisterAttached 方法注册。 有关详细信息,请参阅属性值继承

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

  • 需要属性系统在先前的值因属性系统、环境或用户执行的操作而发生更改,或者因读取和使用样式而发生更改时进行报告。 通过使用属性元素据,属性可以指定回调方法,每次属性系统确定属性值已明确改动时将调用此回调方法。 与此相关的一个概念是属性值强制转换。 有关详细信息,请参阅依赖属性回调和验证

  • 需要使用同时也被 WPF 进程使用的已建立的元数据约定,例如报告更改属性值是否需要布局系统重新安排元素的视觉对象。 或者需要能够使用元素据替代,以便派生类可以更改基于元数据的特性,例如默认值。

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

检查这些方案时,还应考虑是否可以通过替代现有依赖属性的元素据而不是通过实现一个全新的属性来实现方案。 元数据替代是否可行取决于方案以及方案与现有 WPF 依赖属性和类中的实现的相似度。 有关替代现有属性上的元素据的详细信息,请参阅依赖属性元素据

定义依赖属性的检查清单

定义依赖属性包含 4 个不同的概念。 这些概念并不一定是严格的过程步骤,因为其中一些概念在实现中会被合并为一行代码:

  • (可选)创建依赖属性的属性元素据。

  • 在属性系统中注册属性名称,并指定所有者类型和属性值类型。 此外,还应指定属性元素据(如果用到)。

  • 在所有者类型上将 DependencyProperty 标识符定义为 publicstaticreadonly 字段。

  • 定义一个 CLR“包装器”属性,并且其名称与依赖属性名称相匹配。 实现 CLR“包装器”属性的 getset 访问器,以连接支持此属性的依赖属性。

在属性系统中注册属性

为使属性成为依赖属性,必须在属性系统维护的表中注册该属性,并为属性指定一个唯一标识符。此唯一标识符会用作后续属性系统操作的限定符。 这些操作可能是内部操作,也可能使用你自己的代码调用属性系统 API。 若要注册属性,可在类的主体内(在类中但在所有成员定义外)调用 Register 方法。 Register 方法调用也会提供标识符字段作为返回值。 Register 调用在其他成员定义外完成的原因在于,需要使用此返回值分配并创建一个 DependencyProperty 类型的 publicstaticreadonly 字段,作为类的一部分。 此字段会作为依赖属性的标识符。

public static readonly DependencyProperty AquariumGraphicProperty = DependencyProperty.Register(
  "AquariumGraphic",
  typeof(Uri),
  typeof(AquariumObject),
  new FrameworkPropertyMetadata(null,
      FrameworkPropertyMetadataOptions.AffectsRender,
      new PropertyChangedCallback(OnUriChanged)
  )
);
Public Shared ReadOnly AquariumGraphicProperty As DependencyProperty = DependencyProperty.Register("AquariumGraphic", GetType(Uri), GetType(AquariumObject), New FrameworkPropertyMetadata(Nothing, FrameworkPropertyMetadataOptions.AffectsRender, New PropertyChangedCallback(AddressOf OnUriChanged)))

依赖属性命名约定

必需完全遵循已有的依赖属性命名约定,例外情况除外。

依赖属性本身有一个基本名称(此示例中为“AquariumGraphic”),此名称用作 Register 的第一个参数。 此名称在每个注册类型内必须唯一。 通过基类型继承的依赖属性会被视为注册类型的已有部分;无法再次注册已继承属性的名称。 但是,即使不继承依赖属性,也有方法可将类添加为依赖属性的所有者;有关详细信息,请参阅依赖属性元素据

创建标识符字段时,按注册时的属性名称命名此字段,再加上后缀 Property。 此字段是依赖属性的标识符,之后会被用作在包装器中进行 SetValueGetValue 调用的输入,供任何其他代码通过你自己的代码、经过允许的外部代码、属性系统甚至可能通过 XAML 处理器来访问属性时使用。

注意

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

实现“包装器”

包装器实现应在 get 实现中调用 GetValue,并在 set 实现中调用 SetValue(为清楚起见,此处也显示了原始注册调用和字段)。

除特殊情况外,包装器实现仅应执行 GetValueSetValue 操作。 其原因请参阅 XAML 加载和依赖属性主题。

WPF 类上提供的所有现有公共依赖属性都使用这一简单的包装器实现模型;大多数情况下,依赖属性工作原理的复杂性本质上在于它是属性系统的行为,还是通过其他概念(例如强制转换或通过属性元数据进行的属性更改回调)实现的行为。


public static readonly DependencyProperty AquariumGraphicProperty = DependencyProperty.Register(
  "AquariumGraphic",
  typeof(Uri),
  typeof(AquariumObject),
  new FrameworkPropertyMetadata(null,
      FrameworkPropertyMetadataOptions.AffectsRender,
      new PropertyChangedCallback(OnUriChanged)
  )
);
public Uri AquariumGraphic
{
  get { return (Uri)GetValue(AquariumGraphicProperty); }
  set { SetValue(AquariumGraphicProperty, value); }
}

Public Shared ReadOnly AquariumGraphicProperty As DependencyProperty = DependencyProperty.Register("AquariumGraphic", GetType(Uri), GetType(AquariumObject), New FrameworkPropertyMetadata(Nothing, FrameworkPropertyMetadataOptions.AffectsRender, New PropertyChangedCallback(AddressOf OnUriChanged)))
Public Property AquariumGraphic() As Uri
    Get
        Return CType(GetValue(AquariumGraphicProperty), Uri)
    End Get
    Set(ByVal value As Uri)
        SetValue(AquariumGraphicProperty, value)
    End Set
End Property

同样,根据约定,包装器属性名称必须与注册属性的 Register 调用选择并指定的第一个参数相同。 如果属性不遵从此约定,尽管不一定会禁用所有可能的用法,但你会遇到几个比较突出的问题:

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

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

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

新依赖属性的属性元数据

注册依赖属性时,通过属性系统进行注册会创建一个存储属性特征的元素据对象。 如果属性使用 Register 的简单签名进行注册,则其中许多特征会使用设置的默认值。 使用 Register 的其他签名在注册属性时可以指定需要的元数据。 为依赖属性使用的最常见元数据是为其使用默认值。该默认值适用于使用此属性的新实例。

如果要创建一个存在于 FrameworkElement 派生类上的依赖属性,可使用更专业的元数据类 FrameworkPropertyMetadata,而不是基本的 PropertyMetadata 类。 FrameworkPropertyMetadata 类的构造函数具有数个签名,可在这些签名中同时指定多个元数据特征。 若仅需指定默认值,请使用采用 Object 类型的单个参数的签名。 将该对象参数作为属性的特定于类型的默认值进行传递(提供的默认值必须是 Register 调用中作为 propertyType 参数提供的类型)。

对于 FrameworkPropertyMetadata,还可以为属性指定元数据选项标记。 注册后这些标记会转换为属性元素据上的不同属性,并用于将某些条件传送给布局引擎等其他进程。

设置合适的元数据标记

  • 属性(或属性值的更改)影响用户界面 (UI),特别是影响布局系统如何调整页面中元素的大小或呈现方式时,请设置以下一个或多个标记:AffectsMeasureAffectsArrangeAffectsRender

    • AffectsMeasure 指示更改此属性需要更改 UI 呈现,其中包含的对象在父级内可能需要更多或更少的空间。 例如,“宽度”属性应该设置此标记。

    • AffectsArrange 指示更改此属性需要更改 UI 呈现,通常无需在专用空间中进行更改,但会指示该空间内的位置已发生更改。 例如,“对齐”属性应该设置此标记。

    • AffectsRender 指示已发生一些其他更改,这些更改不会影响布局和度量值,但需要其他的呈现方式。 更改现有元素的颜色的属性便是一个示例,例如“背景”。

    • 对属性系统或布局回调进行自己的替代实现时,这些标记通常用作元数据中的协议。 例如,如果实例的任何属性报告值发生了更改,并且在其元数据中将 AffectsArrange 设置为 true,则可以使用调用 InvalidateArrangeOnPropertyChanged 回调。

  • 超出上述所需大小时,某些属性可能会影响所含父元素的呈现特征。 其中一个示例是流文档模型中使用的 MinOrphanLines 属性,对该属性的更改会更改包含该段落的流文档的整体呈现。 使用 AffectsParentArrangeAffectsParentMeasure 标识自己属性中相似的情况。

  • 默认情况下,依赖属性支持数据绑定。 在无实际的数据绑定方案或大型对象的数据绑定性能构成问题的情况下,可有意禁用数据绑定。

  • 默认情况下,依赖属性的数据绑定 Mode 默认为 OneWay。 随时可以将每个绑定实例的绑定更改为 TwoWay;有关详细信息,请参阅指定绑定的方向。 但作为依赖属性的作者,你可选择将属性设置为默认使用 TwoWay 绑定模式。 现有依赖属性的一个示例是 MenuItem.IsSubmenuOpen;此属性的应用场景为:IsSubmenuOpen 设置逻辑和 MenuItem 合成与默认的主题样式交互。 IsSubmenuOpen 属性逻辑以本机方式使用数据绑定,使属性状态与其他状态属性和方法调用保持一致。 另一个默认情况下绑定 TwoWay 的示例属性是 TextBox.Text

  • 还可以通过设置 Inherits 标记在自定义依赖属性中启用属性继承。 在父元素和子元素具有相同属性的情况中,属性继承非常有用,它可以使子元素将该特定属性值设置为与父元素设置的值相同。 可继承属性的一个示例是 DataContext,此属性用于绑定操作,以便为数据呈现启用重要的主-从方案。 通过使 DataContext 成为可继承属性,所有子元素还会继承该数据上下文。 因为使用了属性值继承,你可以在页面或应用程序根目录上指定数据上下文,而无需对所有可能子元素中的绑定重新指定上下文。 DataContext 也是演示继承替代默认值的一个很好的示例,但始终可以在任何特定子元素上对其进行本地设置;有关详细信息,请参阅对分层数据使用主-从模式。 属性值继承确实可能存在性能成本,因此应谨慎使用;有关详细信息,请参阅属性值继承

  • 设置 Journal 标记,以指示导航日志服务是否应该检测或使用依赖属性。 其中一个示例是 SelectedIndex 属性;导航日志历史记录时应保留选择控件中选择的任何项。

只读依赖项属性

可以定义只读的依赖属性。 但是,为何将属性定义为只读的情况略有不同,其过程与在属性系统中注册属性并公开标识符相同。 有关详细信息,请参阅只读依赖属性

集合类型依赖属性

集合类型依赖属性要考虑一些其他实现问题。 有关详细信息,请参阅集合类型依赖属性

依赖属性安全注意事项

依赖属性应声明为公共属性。 依赖属性标识符字段应声明为公共静态字段。 即使尝试声明其他访问级别(例如受保护),也始终可以通过标识符和属性系统 API 来访问依赖属性。 由于元数据报告或值确定 API 属于属性系统,因此甚至可以访问受保护的标识符字段,例如 LocalValueEnumerator。 有关详细信息,请参阅依赖属性的安全性

依赖属性和类构造函数

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

另请参阅