自定义依赖项对象和依赖项属性
本主题描述了 Silverlight 的应用程序开发人员和组件的作者可能想要创建自定义依赖项属性的原因。本主题描述为依赖项属性,以及一些可以提高性能、可用性、或属性的多功能性的执行选项的实施步骤。
本主题包括下列各节。
- 先决条件
- 什么是依赖项属性?
- Silverlight 属性系统与 WPF 属性系统之间的差异
- 依赖项属性的实现模式
- 何时应实现依赖项属性?
- 定义依赖项属性的检查表
- 自定义依赖项属性的属性元数据
- 有关自定义依赖项属性的更多信息
- 相关主题
本主题假定您从 Silverlight 类的现有依赖项属性的使用者角度了解依赖项属性,并且已阅读依赖项属性概述。为了按照本主题中的示例操作,您还应当了解 XAML 并知道如何编写基本的 Silverlight 应用程序。
依赖项属性是通过调用 DependencyProperty.Register 方法向 Silverlight 属性系统注册并由 DependencyProperty 标识符字段标识的属性。通过 CLR 属性可以支持样式设置、数据绑定、动画和默认值,而这些功能也可通过将其作为依赖项属性实现来获得。依赖项属性只能由 DependencyObject 类型使用,但 DependencyObject 在 Silverlight 类层次结构中的级别很高,因此,Silverlight 中的大多数可用于用户界面和演示的类都可能支持依赖项属性。有关依赖项属性和此文档中用于描述依赖项属性的某些术语和约定的更多信息,请参见依赖项属性概述。
Windows Presentation Foundation (WPF) 是第一个定义依赖项属性概念的技术。Silverlight 在很多方面为 WPF 的子集。此子集原则也适用于各自的属性系统。下面是 WPF 中存在但 Silverlight 5 Beta 中不存在的属性系统功能的列表:
-
强制值回调和 DependencyObject.CoerceValue。不过,您通常可以使用属性更改回调(在 Silverlight 中支持)来满足您在这一领域的大多数需要。
-
自定义只读依赖项属性。Silverlight 没有实现使用密钥的 DependencyPropertyKey 或 SetValue 模式。没有此键技术,就没有可用的 API 可以防止外部代码通过 SetValue 设置该依赖项属性或违反该属性的只读用途。
-
每类型属性元数据(使用 OverrideMetadata 建立)。Silverlight 中用于依赖项属性的属性元数据始终是最初向其所有者类型注册的元数据。这一注意事项适用于默认值和属性更改回调,它们是 Silverlight 属性元数据中两个支持的信息集。
-
由框架定义的属性元数据子类,例如 UIPropertyMetadata 或 FrameworkPropertyMetadata。在 Silverlight 中仅存在 PropertyMetadata,但无疑您可以根据它进行派生。WPF FrameworkPropertyMetadata 为信息传递约定提供"标志"机制,但 Silverlight 5 Beta 不支持此机制。
-
AddOwner 机制以及现有的依赖项属性可以重新注册到不同的所有者类型。
-
所有的可设置属性集合见 PropertyMetadata。从 PropertyMetadata截止到 Silverlight 3,Silverlight 支持 GetMetadata 方法和一种可读的 DefaultValue,但不允许更改元数据或从元数据检索 PropertyChangedCallback。此外没有 Silverlight 被属性系统检索和分析 APIs,如 DependencyObjectType 类。
-
DependencyPropertyHelper 实用工具和 DependencyObject.GetLocalValueEnumerator。
-
Freezable 和 FreezableCollection 类。它们并非必须严格遵守的依赖项属性注意事项,但在整个 WPF 属性系统中涉及这些概念/类并且它们适用于某些方案(如自定义动画类型)。
Silverlight 依赖项属性的示例有:Control.Background、FrameworkElement.Width 和 TextBox.Text 等。某个类公开的每个依赖项属性都有一个对应的、在该相同类上公开的类型为 DependencyProperty 的 public static readonly 字段。该字段是依赖项属性的标识符。该标识符字段的命名约定为:依赖项属性的名称以及在该名称后面追加字符串 Property。例如,与 Control.Background 属性对应的 DependencyProperty 标识符字段为 Control.BackgroundProperty。该标识符用于在注册依赖项属性时存储其相关信息,并且在随后可用于与依赖项属性有关的其他操作,如调用 SetValue。
如依赖项属性概述中所述,由于包装实现,Silverlight 中的所有依赖项属性(大多数附加属性除外)也是 CLR 属性。因此,可以从代码中获取或设置依赖项属性,方法是按照与使用其他 CLR 属性相同的方式来调用充当包装的 CLR 访问器。作为确定的依赖项属性的使用者,您通常不会使用 DependencyObject 方法 GetValue 和 SetValue,这两种方法是基础属性系统的连接点。相反,Silverlight 依赖项属性的现有实现将已经相应地使用标识符字段在该属性的 get 和 set 访问器内调用了 GetValue 和 SetValue。如果您要亲自实现自定义的依赖项属性,则需要用类似的方法为 CLR 类型系统定义访问器。
除了向调用方提供易于访问的权限以外,属性包装也有助于将有关依赖项属性的基本信息报告给反射或静态分析。属性包装的属性定义也是您放置与 XAML 有关的 CLR 属性的位置, 如ContentPropertyAttribute 或 TypeConverterAttribute;当 Silverlight XAML 分析器使用自定义依赖项属性时,这是那特性所期望的位置。
在类上实现读/写属性时,只要类派生自 DependencyObject,便可以选择使用 DependencyProperty 标识符来支持该属性,从而使其成为依赖项属性。使 Silverlight 属性成为依赖项属性并不始终必要或适当,具体取决于您的需要。有时,使用私有 CLR 字段备份属性的典型方法便能满足要求。但是,只要您希望属性支持以下一种或多种 Silverlight 属性系统功能,就应将您的属性作为依赖项属性实现:
-
您希望可在样式中设置此属性。有关更多信息,请参见 通过使用 ControlTemplate 自定义现有控件的外观。
-
您要此属性是一个支持数据绑定的属性目标。有关数据绑定依赖项属性的更多信息,请参见数据绑定。
-
在使用 Silverlight 动画系统时,您要此属性来支持动画值。有关更多信息,请参见 动画概述。
-
您希望 Silverlight 属性系统在属性系统本身、环境或用户执行的操作或者读取并使用样式而更改了属性以前的值时进行报告。您的属性可以指定在每次属性系统确定属性值已被明确更改时将调用的回调方法。
定义依赖项属性包括四个不同的概念。这些概念并不一定是严格的过程步骤,因为其中某些概念在实现中被组合为单行代码:
-
(可选)为依赖项属性创建属性元数据。仅当您需要属性更改行为或基于元数据的默认值(可通过调用 ClearValue 来恢复)时,才需要属性元数据。
-
在属性系统中注册属性名称,并指定所有者类型和属性值的类型。还可以指定属性元数据(如果使用);或者,如果您不需要属性元数据,则指定 null。
-
在所有者类型上将 DependencyProperty 标识符定义为 public static readonly 字段。
-
定义其名称与依赖项属性字段的名称匹配的 CLR 包装属性,减去 Property 后缀,和从 Register(String, Type, Type, PropertyMetadata) 调用的 name 参数也匹配。实现 CLR get 和 set 访问器,以便将提供 CLR 公开的包装与支持它的依赖项属性进行连接。(如果您定义一个附加属性,则通常将忽略此包装,而是编写 XAML 处理器可以使用的不同风格的访问器。有关更多信息,请参见 附加属性概述。)
-
(可选)将任何与 XAML 有关的 CLR 属性,如ContentPropertyAttribute 或 TypeConverterAttribute,放入属性定义(外部定义,而不是获取 - 设置实现)。
在属性系统中注册属性
为使属性成为依赖项属性,必须在由 Silverlight 属性系统维护的属性存储区中注册该属性,并且必须为其指定一个唯一标识符,以用作属性系统后续操作的限定符。这些操作可能是内部操作,也可能是调用属性系统 API 的您自己的代码。若要注册属性,请在类体中(在类的内部,但在任何成员定义的外部)调用 DependencyProperty.Register 方法。DependencyProperty.Register 方法调用也提供此标识符字段作为返回值。通常在其他成员定义的外部执行 DependencyProperty.Register 调用的原因在于:使用此返回值可以分配和创建一个类型为 DependencyProperty 的 public static readonly 字段,以作为类的一部分。此字段将成为依赖项属性的标识符。
依赖项属性名称约定
您应完全遵循为依赖项属性建立的有关命名约定,但例外情况除外。
依赖项属性本身将具有一个基本名称(在前一示例中为 AquariumGraphic),该名称是作为 DependencyProperty.Register 的第一个参数提供的。该名称在每个注册类型中必须唯一,并且唯一性范围包括任何继承的成员。通过基类型继承的依赖项属性将被视为已是注册类型的一部分;无法再次注册已继承属性的名称。
警告: |
---|
虽然您在此处提供的名称可以是在 CLR 领域实体的 CLR 编程中接受的任何标识符,您应该预测依赖项属性可能还需要在 XAML 中可设置。为使其在 XAML 中可设置,所选择的名称必须是有效的 XAML 名称。有关更多信息,请参见 XamlName 语法。 |
当您创建标识符字段时,请将您注册该属性时的属性名称和后缀 Property 组合起来(例如,AquariumGraphicProperty)。此字段是依赖项属性的标识符;并且对于在您自己的 CLR 包装中作出的、由属性系统作出的以及可能由 XAML 处理器作出的 SetValue 和 GetValue 调用,此字段将用作输入。
说明: |
---|
在类体中定义依赖项属性是典型的实现,但也可以在类静态构造函数中定义依赖项属性。如果您需要多行代码来初始化依赖项属性,则此方法可能会很有意义。 |
实现包装
包装实现应调用 get 实现中的 GetValue 和 set 实现中的 SetValue(为清楚起见,此代码示例中还演示原始注册调用和字段)。
除例外情况以外,包装实现在其他情况中应只分别执行 GetValue 和 SetValue 操作。有关这一点的原因将在本主题后面的 XAML 加载和依赖项属性中讨论。
此外,按照约定,包装属性的名称应该与所选择并指定为注册该属性的 DependencyProperty.Register 调用的第一个参数的名称相同。如果您的属性未能遵循该约定,尽管不一定所有可能的用法都失效,但您将会遇到以下几个显而易见的问题:
-
样式和模板的某些环节可能不起作用。
-
大多数工具和设计器都必须依赖于命名约定,以便在每个属性级别上都提供设计器环境帮助。
-
XAML 处理器可能会完全跳过包装,并且在处理属性值时依赖于命名约定。请参见下一节。
XAML 加载和依赖项属性
XAML 处理器的 Silverlight 5 Beta 实现在本质上能够识别依赖项属性。在加载 XAML 内容并处理作为依赖项属性 (Property) 的属性 (Attribute) 时,如果依赖项属性是使用样式 Setter 进行设置的,则 Silverlight 5 Beta XAML 处理器针对依赖项属性使用属性系统方法。此操作将跳过 CLR 包装。在实现自定义依赖项属性时,必须考虑此行为,而且应当避免将任何其他代码放在除属性系统方法 GetValue 和 SetValue 以外的属性包装中。
同样,在 XAML 处理器中,从 XAML 处理中获取属性值的其他环节也可能使用 GetValue,而不是使用包装。因此,您还应当避免在 get 定义中使用除 GetValue 调用以外的任何其他实现。
当将属性元数据分配给某个依赖项属性时,相同元数据将应用于所有对象实例上出现这一属性的所有情形。在属性元数据中,您可以指定该属性的两种行为:
-
属性系统在出现这一属性的所有情形下分配给此属性的默认值。
-
只要更改了属性值,就会在属性系统中调用的静态回调方法。
默认值
如果未指定,则对于引用类型,依赖项属性的默认值为 null;或者,对于值类型,则为默认类型。您可能希望确定默认值的主要原因是:如果您对属性调用了 ClearValue,则将恢复此值。在每个属性的基础之上确定默认值与在构造函数中确定默认值的模式相比,前者可能更为方便,对于值类型更是如此。不过,对于引用类型,应确保确定默认值不会导致意外的单一实例模式。如果您需要非空值,则可以使用类构造函数来为引用类型依赖项属性设置初始值,但请注意,这会被视为一个旨在体现依赖项属性优先性的本地值。如果您的类可模板化,则使用模板来实现此目的可能更恰当。另一种避免单一实例模式但仍提供有用默认值的方法是对引用类型公开一个静态属性,以便为该类的各个值提供合适的默认值。
重要说明: |
---|
不要使用 UnsetValue 的特定默认值注册依赖项属性。这将使属性使用方产生困扰,并且将在属性系统中产生意外后果。 |
属性更改回调
如果依赖项属性与其他依赖项属性进行交互,或者如果它用于设置对象的内部属性或状态,则应使用属性更改回调。您不需要比较 OldValue 和 NewValue 来确定是否发生了更改;如果调用了回调,则属性系统已经确定发生了有效的属性值更改。由于此方法是静态的,因此回调的 d 参数很重要,因为它通知您类的哪个实例已报告了更改。
典型的实现使用事件数据类的 NewValue 并以某种方式处理该值(通常是通过对作为 d 传递的对象执行某项其他更改)。其他可能的方案是拒绝所尝试的 NewValue 集,并恢复 OldValue 或将值设置为某个已知约束(该约束的含义取决于所尝试的 NewValue)。
PropertyChangedCallback 实现示例
下面的示例按照在前面演示的 Register 示例中进行注册的方式实现 OnUriChanged 回调。在此情况下,实现使用更改后的 Uri 值,以便重新构建其内部属性之一,而该内部属性根据 Uri 构造 BitmapImage。
集合类型依赖项属性
集合类型依赖项属性具有一些需要考虑的其他实现问题。有关详细信息,请参见集合类型依赖项属性。
依赖项属性安全注意事项
依赖项属性应声明为 public 属性。依赖项属性标识符字段应声明为 publicstaticreadonly 字段。即使您试图声明其他访问级别(如 protected),也始终可通过标识符并结合属性系统 API 来访问依赖项属性。有关更多信息,请参见 依赖项属性的安全性。
依赖项属性和类构造函数
托管代码编程(通常通过 FxCop 等代码分析工具来强制执行)的一般原则是:类构造函数不应调用虚方法。这是因为构造函数可作为派生的类构造函数的基本初始化来调用,并且可能会在所构造的对象实例的不完全初始化状态下通过构造函数输入虚方法。当从已派生自 DependencyObject 的任何类派生时请注意,属性系统本身会在内部将实质方法作为 Silverlight 属性系统服务的一部分进行调用和公开。为了避免运行时初始化出现潜在问题,不应在类的构造函数中设置依赖项属性值,除非您遵循非常明确的构造函数模式。有关详细信息,请参见依赖项对象的安全构造函数模式。