自定义的依赖项属性

更新:2007 年 11 月

本主题介绍 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 属性的现有实现会已使用相应的标识符字段在该属性的 get 和 set 包装实现内调用 GetValueSetValue。如果要亲自实现自定义的依赖项属性,则需要用类似的方法定义包装。

何时应实现依赖项属性?

在类上实现属性时,只要类派生自 DependencyObject,便可以选择使用 DependencyProperty 标识符来备份属性,从而将其设置为依赖项属性。将属性设置为依赖项属性并不总是必要或适当,具体取决于方案需要。有时,使用私有字段备份属性的典型方法便能满足要求。但是,只要您希望属性支持以下一种或多种 WPF 功能,就应将属性作为依赖项属性来实现:

  • 希望可在样式中设置属性。有关更多信息,请参见样式设置和模板化

  • 希望属性支持数据绑定。有关数据绑定依赖项属性的更多信息,请参见如何:绑定两个控件的属性

  • 希望可使用动态资源引用设置属性。有关更多信息,请参见资源概述

  • 希望从元素树中的父元素自动继承属性值。在这种情况下,即使还为 CLR 访问创建了属性包装,也应使用 RegisterAttached 方法进行注册。有关更多信息,请参见属性值继承

  • 希望属性可进行动画处理。有关更多信息,请参见动画概述

  • 希望属性系统在属性系统、环境或用户执行的操作或者读取并使用样式更改了属性以前的值时报告。通过使用属性元数据,属性可以指定将在每次属性系统确定属性值已被明确更改时调用的回调方法。相关概念是属性值强制。有关更多信息,请参见依赖项属性回调和验证

  • 希望使用已建立的、WPF 进程也使用的元数据约定,例如报告更改属性值时是否要求布局系统重新编写元素的可视化对象。或者,希望能够使用元数据重写,以便派生类可更改基于元数据的特性(如默认值)。

  • 希望自定义控件的属性接收 Visual Studio 2008 WPF 设计器 支持,如“属性”窗口编辑。有关更多信息,请参见控件创作概述

检查这些方案时,还应考虑是否可以通过重写现有依赖项属性的元数据,而不是实现一个全新属性来实现方案。元数据重写可行与否取决于方案以及方案与现有 WPF 依赖项属性和类的实现的相似情况。有关重写现有属性的元数据的更多信息,请参见依赖项属性元数据

定义依赖项属性的检查表

定义依赖项属性包括四个不同的概念。这些概念并不一定是严格的过程步骤,因为其中某些概念最后在实现中被组合为单行代码:

  • (可选)为依赖项属性创建属性元数据。

  • 在属性系统中注册属性名称,并指定所有者类型和属性值的类型。如果使用了属性元数据,则也应同时指定。

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

  • 定义其名称与依赖项属性的名称匹配的 CLR“包装”属性。实现 CLR“包装”属性的 get 和 set 访问器,以便与支持此属性的依赖项属性进行连接。

在属性系统中注册属性

为将属性设置为依赖项属性,必须在属性系统维护的表中注册该属性,并为其指定一个唯一的标识符,以用作属性系统后续操作的限定符。这些操作可能是内部操作,也可能是您自己的代码调用属性系统 API。若要注册属性,请在类体中调用 Register 方法(在类的内部,但在任何成员定义的外部)。Register 方法调用还提供了标识符字段以作为返回值。在其他成员定义外部执行 Register 调用的原因在于,使用此返回值可以分配和创建一个类型为 DependencyProperty 的 public static readonly 字段,以作为类的一部分。此字段将成为依赖项属性的标识符。

public static readonly DependencyProperty AquariumGraphicProperty = DependencyProperty.Register(
  "AquariumGraphic",
  typeof(Uri),
  typeof(AquariumObject),
  new FrameworkPropertyMetadata(null,
      FrameworkPropertyMetadataOptions.AffectsRender, 
      new PropertyChangedCallback(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); }
}

此外,按照约定,包装属性的名称必须与所选择并指定为注册该属性的 Register 调用的第一个参数的名称相同。如果属性不遵循该约定,尽管不一定要禁用所有可能的用法,但您将会遇到几个突出的问题:

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

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

  • WPF XAML 加载程序的当前实现会完全跳过包装,并且在处理属性值时依赖于命名约定。有关更多信息,请参见XAML 加载和依赖项属性

新依赖项属性的属性元数据

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

如果要创建在 FrameworkElement 的派生类上存在的依赖项属性,则可以使用更专用的元数据类 FrameworkPropertyMetadata,而不是 PropertyMetadata 基类。FrameworkPropertyMetadata 类的构造函数具有几种签名,您可以在其中组合指定多个元数据特性。如果要仅指定默认值,请使用接受类型为 Object 的单个参数的签名,并将该对象参数作为属性特定于类型的默认值进行传递。(提供的默认值必须是 Register 调用中作为 propertyType 参数提供的类型。)

对于 FrameworkPropertyMetadata,还可以为属性指定元数据选项标志。在注册之后,这些标志将转换为属性元数据中的不同属性,用于将某些条件传送给其他进程(如布局引擎)。

设置相应的元数据标志

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

    • AffectsMeasure 指示在对此属性进行更改时,需要更改包含对象在父对象中可能需要更多或较少空间的 UI 呈现。例如,“Width”属性应设置此标志。

    • AffectsArrange 指示在对此属性进行更改时,需要更改通常无须更改专用空间、但又指示该空间中的位置已发生了更改的 UI 呈现。例如,“Alignment”属性应设置此标志。

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

    • 这些标志在元数据中通常用作协议,以便您自己重写实现属性系统或布局回调。例如,如果实例的任何属性报告值发生更改,并且在其元数据中将 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 的安全构造函数模式

请参见

概念

依赖项属性概述

依赖项属性元数据

控件创作概述

集合类型依赖项属性

依赖项属性的安全性

XAML 加载和依赖项属性

DependencyObject 的安全构造函数模式