自定义依赖属性Custom dependency properties

我们在此处介绍了如何为使用 C++、C# 或 Visual Basic 的 Windows 运行时应用定义和实现你自己的依赖属性。Here we explain how to define and implement your own dependency properties for a Windows Runtime app using C++, C#, or Visual Basic. 我们列出了应用开发人员和组件作者可能希望创建自定义依赖属性的原因。We list reasons why app developers and component authors might want to create custom dependency properties. 我们描述了自定义依赖属性的实现步骤,以及一些可改善依赖属性的性能、实用性或通用性的最佳做法。We describe the implementation steps for a custom dependency property, as well as some best practices that can improve performance, usability, or versatility of the dependency property.

先决条件Prerequisites

我们假设你已阅读依赖属性概述,并且从现有依赖属性用户的角度理解依赖属性。We assume that you have read the Dependency properties overview and that you understand dependency properties from the perspective of a consumer of existing dependency properties. 要理解本主题中的示例,你还应该理解 XAML,知道如何编写使用 C++、C# 或 Visual Basic 的基本 Windows 运行时应用。To follow the examples in this topic, you should also understand XAML and know how to write a basic Windows Runtime app using C++, C#, or Visual Basic.

什么是依赖属性?What is a dependency property?

若要为属性支持样式设置、数据绑定、动画和默认值,它应实现为依赖属性。To support styling, data binding, animations, and default values for a property, then it should be implemented as a dependency property. 依赖属性值不会存储为类中的字段,它们由 XAML 框架进行存储,并且使用密钥进行引用,该密钥会在通过调用 DependencyProperty.Register 方法以使用 Windows 运行时属性系统注册该属性时检索。Dependency property values are not stored as fields on the class, they are stored by the xaml framework, and are referenced using a key, which is retrieved when the property is registered with the Windows Runtime property system by calling the DependencyProperty.Register method. 依赖属性只能由从 DependencyObject 派生的类型使用。Dependency properties can be used only by types deriving from DependencyObject. DependencyObject 位于类层次结构中很高的级别,所以大部分用于 UI 和演示支持的类都能支持依赖属性。But DependencyObject is quite high in the class hierarchy, so the majority of classes that are intended for UI and presentation support can support dependency properties. 有关依赖属性以及本文档中用于描述它们的一些术语和约定的详细信息,请参阅依赖属性概述For more information about dependency properties and some of the terminology and conventions used for describing them in this documentation, see Dependency properties overview.

Windows 运行时中的依赖属性的示例如下:控件背景FrameworkElementText 和 TextBox。 Examples of dependency properties in the Windows Runtime are: Control.Background, FrameworkElement.Width, and TextBox.Text, among many others.

约定如下:一个类公开的每个依赖属性都有一个 DependencyProperty 类型的相应 public static readonly 属性,该属性在同一个类上公开并提供依赖属性的标识符。Convention is that each dependency property exposed by a class has a corresponding public static readonly property of type DependencyProperty that is exposed on that same class the provides the identifier for the dependency property. 标识符的名称遵循以下约定:已向名称末尾添加字符串“Property”的依赖属性的名称。The identifier's name follows this convention: the name of the dependency property, with the string "Property" added to the end of the name. 例如,Control.Background 属性对应的 DependencyProperty 标识符是 Control.BackgroundPropertyFor example, the corresponding DependencyProperty identifier for the Control.Background property is Control.BackgroundProperty. 标识符在注册依赖属性时存储其相关信息,然后可用于其他涉及依赖属性的操作,例如调用 SetValueThe identifier stores the information about the dependency property as it was registered, and can then be used for other operations involving the dependency property, such as calling SetValue.

属性包装器Property wrappers

依赖属性通常有一个包装器实现。Dependency properties typically have a wrapper implementation. 没有包装器,获取或设置属性的唯一方式就是使用依赖属性实用程序方法 GetValueSetValue 并将标识符作为参数传递给它们。Without the wrapper, the only way to get or set the properties would be to use the dependency property utility methods GetValue and SetValue and to pass the identifier to them as a parameter. 从表面上看,这是一个明显很奇怪的属性用法。This is a rather unnatural usage for something that is ostensibly a property. 但有了包装器,你的代码和任何其他引用依赖属性的代码都可使用一种直观的对象-属性语法,这对你所使用的语言而言显得很正常。But with the wrapper, your code and any other code that references the dependency property can use a straightforward object-property syntax that is natural for the language you're using.

如果自行实现一个自定义依赖属性,并且希望它是公共的且易于调用,也可定义属性包装器。If you implement a custom dependency property yourself and want it to be public and easy to call, define the property wrappers too. 属性包装器对向反射或静态分析流程报告有关依赖属性的基本信息也很有用。The property wrappers are also useful for reporting basic information about the dependency property to reflection or static analysis processes. 具体来讲,包装器是放置特性(例如 ContentPropertyAttribute)的地方。Specifically, the wrapper is where you place attributes such as ContentPropertyAttribute.

何时将属性实现为依赖属性When to implement a property as a dependency property

在类上实现公共读/写属性,只要你的类派生自 DependencyObject,就可以选择让属性以依赖属性的方式工作。Whenever you implement a public read/write property on a class, as long as your class derives from DependencyObject, you have the option to make your property work as a dependency property. 有时使用一个私有字段来支持属性的典型技术就足够了。Sometimes the typical technique of backing your property with a private field is adequate. 将自定义属性定义为依赖属性并非总是必要或合适的。Defining your custom property as a dependency property is not always necessary or appropriate. 具体选择将取决于你希望属性支持的场景。The choice will depend on the scenarios that you intend your property to support.

当你希望属性支持 Windows 运行时或 Windows 运行时应用的以下一个或多个功能时,你可以考虑将属性实现为依赖属性:You might consider implementing your property as a dependency property when you want it to support one or more of these features of the Windows Runtime or of Windows Runtime apps:

  • 通过 Style 设置属性Setting the property through a Style
  • 用作通过 {Binding} 绑定数据的有效目标属性Acting as valid target property for data binding with {Binding}
  • 通过 Storyboard 支持动画值Supporting animated values through a Storyboard
  • 报告属性值何时被以下实体更改:Reporting when the value of the property has been changed by:
    • 属性系统本身执行的操作Actions taken by the property system itself
    • 环境The environment
    • 用户操作User actions
    • 读取和写入样式Reading and writing styles

定义依赖属性的检查列表Checklist for defining a dependency property

一个依赖属性的定义可视为一组概念。Defining a dependency property can be thought of as a set of concepts. 这些概念不一定是顺序步骤,因为在实现的一行代码中可解决多个概念。These concepts are not necessarily procedural steps, because several concepts can be addressed in a single line of code in the implementation. 这个列表只是提供了简短概述。This list gives just a quick overview. 我们将在本主题后面更详细地介绍每个概念,并且显示多种语言的示例代码。We'll explain each concept in more detail later in this topic, and we'll show you example code in several languages.

  • 向属性系统注册属性名称(调用 Register),指定所有者类型和属性值的类型。Register the property name with the property system (call Register), specifying an owner type and the type of the property value.
    • Register 有一个必需的参数需要使用属性元数据。There's a required parameter for Register that expects property metadata. 为 Register 指定 null,或者如果你希望通过调用 ClearValue 可还原属性已更改的行为或基于元数据的默认值,请指定 PropertyMetadata 的实例。Specify null for this, or if you want property-changed behavior, or a metadata-based default value that can be restored by calling ClearValue, specify an instance of PropertyMetadata.
  • 将一个 DependencyProperty 标识符定义为所有者类型上的一个 public static readonly 属性成员。Define a DependencyProperty identifier as a public static readonly property member on the owner type.
  • 按照你正在实现的语言中所用的属性访问器模型,定义一个包装器属性。Define a wrapper property, following the property accessor model that's used in the language you are implementing. 包装器属性名称应该与 Register 中使用的 name 字符串匹配。The wrapper property name should match the name string that you used in Register. 实现 getset 访问器将包装器与它包装的依赖属性相连接,方法是调用 GetValueSetValue 并将你自己的属性标识符作为一个参数传递。Implement the get and set accessors to connect the wrapper with the dependency property that it wraps, by calling GetValue and SetValue and passing your own property's identifier as a parameter.
  • (可选)将 ContentPropertyAttribute 等特性放在包装器上。(Optional) Place attributes such as ContentPropertyAttribute on the wrapper.

备注

如果要定义自定义附加属性,通常会省略包装。If you are defining a custom attached property, you generally omit the wrapper. 而是编写一种可供 XAML 处理器使用的不同访问器样式。Instead, you write a different style of accessor that a XAML processor can use. 查看自定义附加属性See Custom attached properties. 

注册属性Registering the property

若要使你的属性成为依赖属性,必须将该属性注册到由 Windows 运行时属性系统所维护的属性存储中。For your property to be a dependency property, you must register the property into a property store maintained by the Windows Runtime property system. 若要注册属性,请调用 Register 方法。To register the property, you call the Register method.

对于 Microsoft .NET 语言(C# 和 Microsoft Visual Basic),你可以在类的主体中调用 Register(在类中,但在任何成员定义外部)。For Microsoft .NET languages (C# and Microsoft Visual Basic) you call Register within the body of your class (inside the class, but outside any member definitions). 该标识符由 Register 方法调用以返回值的形式提供。The identifier is provided by the Register method call, as the return value. Register 通常调用为静态构造函数,或作为类中包括的 DependencyProperty 类型的 public static readonly 属性初始化的一部分。The Register call is typically made as a static constructor or as part of the initialization of a public static readonly property of type DependencyProperty as part of your class. 此属性会公开你的依赖属性的标识符。This property exposes the identifier for your dependency property. 以下是 Register 调用的一些示例。Here are examples of the Register call.

备注

在标识符属性定义中注册依赖属性是典型的实现,但也可以在类静态构造函数中注册依赖属性。Registering the dependency property as part of the identifier property definition is the typical implementation, but you can also register a dependency property in the class static constructor. 如果需要多行代码来初始化依赖属性,此方法可能很有用。This approach may make sense if you need more than one line of code to initialize the dependency property.

对于C++/cx,可以选择如何在标头和代码文件之间拆分实现。For C++/CX, you have options for how you split the implementation between the header and the code file. 典型的拆分方式是在标头中将标识符本身声明为 public static 属性,它具有一个 get 实现但没有 setThe typical split is to declare the identifier itself as public static property in the header, with a get implementation but no set. get 实现引用一个私有字段,该字段是一个未初始化的 DependencyProperty 实例。The get implementation refers to a private field, which is an uninitialized DependencyProperty instance. 你也可以声明包装器和包装器的 getset 实现。You can also declare the wrappers and the get and set implementations of the wrapper. 在此情况下,标头文件包含一些极小的实现。In this case the header includes some minimal implementation. 如果包装器需要归属于 Windows 运行时,标头文件中的特性也需要。If the wrapper needs Windows Runtime attribution, attribute in the header too. Register 调用放置在代码文件内仅在应用首次初始化时运行的 helper 函数中。Put the Register call in the code file, within a helper function that only gets run when the app initializes the first time. 使用 Register 的返回值填充你在标头文件中声明的静态但未初始化的标识符,你最初已在实现文件的根作用域上将其设置为 nullptrUse the return value of Register to fill the static but uninitialized identifiers that you declared in the header, which you initially set to nullptr at the root scope of the implementation file.

public static readonly DependencyProperty LabelProperty = DependencyProperty.Register(
  "Label",
  typeof(String),
  typeof(ImageWithLabelControl),
  new PropertyMetadata(null)
);
Public Shared ReadOnly LabelProperty As DependencyProperty = 
    DependencyProperty.Register("Label", 
      GetType(String), 
      GetType(ImageWithLabelControl), 
      New PropertyMetadata(Nothing))
// ImageWithLabelControl.idl
namespace ImageWithLabelControlApp
{
    runtimeclass ImageWithLabelControl : Windows.UI.Xaml.Controls.Control
    {
        ImageWithLabelControl();
        static Windows.UI.Xaml.DependencyProperty LabelProperty{ get; };
        String Label;
    }
}

// ImageWithLabelControl.h
...
private:
    static Windows::UI::Xaml::DependencyProperty m_labelProperty;
...

// ImageWithLabelControl.cpp
...
Windows::UI::Xaml::DependencyProperty ImageWithLabelControl::m_labelProperty =
    Windows::UI::Xaml::DependencyProperty::Register(
        L"Label",
        winrt::xaml_typename<winrt::hstring>(),
        winrt::xaml_typename<ImageWithLabelControlApp::ImageWithLabelControl>(),
        Windows::UI::Xaml::PropertyMetadata{ nullptr }
);
...
//.h file
//using namespace Windows::UI::Xaml::Controls;
//using namespace Windows::UI::Xaml::Interop;
//using namespace Windows::UI::Xaml;
//using namespace Platform;

public ref class ImageWithLabelControl sealed : public Control
{
private:
    static DependencyProperty^ _LabelProperty;
...
public:
    static void RegisterDependencyProperties();
    static property DependencyProperty^ LabelProperty
    {
        DependencyProperty^ get() {return _LabelProperty;}
    }
...
};

//.cpp file
using namespace Windows::UI::Xaml;
using namespace Windows::UI::Xaml.Interop;

DependencyProperty^ ImageWithLabelControl::_LabelProperty = nullptr;

// This function is called from the App constructor in App.xaml.cpp
// to register the properties
void ImageWithLabelControl::RegisterDependencyProperties()
{ 
    if (_LabelProperty == nullptr)
    { 
        _LabelProperty = DependencyProperty::Register(
          "Label", Platform::String::typeid, ImageWithLabelControl::typeid, nullptr);
    } 
}

备注

对于C++/cx 代码,如果你有一个私有字段和一个公共只读属性,其中显示了DependencyProperty的原因是,使用依赖属性的其他调用方也可以使用需要要公开的标识符。For the C++/CX code, the reason why you have a private field and a public read-only property that surfaces the DependencyProperty is so that other callers who use your dependency property can also use property-system utility APIs that require the identifier to be public. 如果保持标识符为私有,人们将无法使用这些实用程序 API。If you keep the identifier private, people can't use these utility APIs. 此类 API 示例和场景包括 GetValueSetValueClearValueGetAnimationBaseValueSetBindingSetter.Property 等。Examples of such API and scenarios include GetValue or SetValue by choice, ClearValue, GetAnimationBaseValue, SetBinding, and Setter.Property. 不可将公共字段用于这些内容,因为 Windows 运行时元数据规则不支持公共字段。You can't use a public field for this, because Windows Runtime metadata rules don't allow for public fields.

依赖属性名称约定Dependency property name conventions

依赖属性具有命名约定;需要在除一些例外情况外的所有情形中遵循这些约定。There are naming conventions for dependency properties; follow them in all but exceptional circumstances. 依赖属性本身有一个基本名称(上一个示例中的“Label”),它作为 Register 的第一个参数提供。The dependency property itself has a basic name ("Label" in the preceding example) that is given as the first parameter of Register. 该名称必须在每个注册类型中是唯一的,这种唯一性需求也适用于任何继承的成员。The name must be unique within each registering type, and the uniqueness requirement also applies to any inherited members. 通过基础类型继承的依赖属性已被视为注册类型的一部分;不能再次注册继承属性的名称。Dependency properties inherited through base types are considered to be part of the registering type already; names of inherited properties cannot be registered again.

警告

尽管你在此处提供的名称可以是任何在你选择的语言中有效的字符串标识符,但你通常还希望能够在 XAML 中设置依赖属性。Although the name you provide here can be any string identifier that is valid in programming for your language of choice, you usually want to be able to set your dependency property in XAML too. 要在 XAML 中设置,你选择的属性名称必须是有效的 XAML 名称。To be set in XAML, the property name you choose must be a valid XAML name. 有关详细信息,请参阅 XAML 概述For more info, see XAML overview.

创建标识符属性时,将你注册属性时的属性名称与后缀“Property”结合在一起(例如“LabelProperty”)。When you create the identifier property, combine the name of the property as you registered it with the suffix "Property" ("LabelProperty", for example). 此属性是依赖属性的标识符,并且它用作你在自己的属性包装器中执行的 SetValueGetValue 调用的输入。This property is your identifier for the dependency property, and it is used as an input for the SetValue and GetValue calls you make in your own property wrappers. 它还供属性系统以及其他 XAML 处理器(例如 {x:Bind} )使用It is also used by the property system and other XAML processors such as {x:Bind}

实现包装器Implementing the wrapper

属性包装器应该在 get 实现中调用 GetValue,在 set 实现中调用 SetValueYour property wrapper should call GetValue in the get implementation, and SetValue in the set implementation.

警告

除异常情况以外,包装实现仅应执行GetValueSetValue操作。In all but exceptional circumstances, your wrapper implementations should perform only the GetValue and SetValue operations. 否则,在通过 XAML 设置属性时的行为与通过代码设置属性时的行为不同。Otherwise, you'll get different behavior when your property is set via XAML versus when it is set via code. 为了提高效率,在设置依赖属性时,XAML 分析程序将绕过包装器,并通过 SetValue 与后备存储通信。For efficiency, the XAML parser bypasses wrappers when setting dependency properties; and talks to the backing store via SetValue.

public String Label
{
    get { return (String)GetValue(LabelProperty); }
    set { SetValue(LabelProperty, value); }
}
Public Property Label() As String
    Get
        Return DirectCast(GetValue(LabelProperty), String) 
    End Get 
    Set(ByVal value As String)
        SetValue(LabelProperty, value)
    End Set
End Property
// ImageWithLabelControl.h
...
winrt::hstring Label()
{
    return winrt::unbox_value<winrt::hstring>(GetValue(m_labelProperty));
}

void Label(winrt::hstring const& value)
{
    SetValue(m_labelProperty, winrt::box_value(value));
}
...
//using namespace Platform;
public:
...
  property String^ Label
  {
    String^ get() {
      return (String^)GetValue(LabelProperty);
    }
    void set(String^ value) {
      SetValue(LabelProperty, value);
    }
  }

自定义依赖属性的属性元数据Property metadata for a custom dependency property

向一个依赖属性分配属性元数据时,针对属性所有者类型的每个实例或其子类,向该属性应用相同的元数据。When property metadata is assigned to a dependency property, the same metadata is applied to that property for every instance of the property-owner type or its subclasses. 在属性元数据中,你可以指定两种行为:In property metadata, you can specify two behaviors:

  • 属性系统在所有情况下向属性分配的默认值。A default value that the property system assigns to all cases of the property.
  • 只要检测到属性值更改,就会在属性系统中自动调用静态回调方法。A static callback method that is automatically invoked within the property system whenever a property value change is detected.

使用属性元数据调用注册Calling Register with property metadata

在调用 DependencyProperty.Register 的先前示例中,我们为 propertyMetadata 参数传递了一个 Null 值。In the previous examples of calling DependencyProperty.Register, we passed a null value for the propertyMetadata parameter. 要使依存关系属性能够提供一个默认值,或使用某个属性已更改的回调,必须定义一个提供其中一项或全部两项功能的 PropertyMetadata 实例。To enable a dependency property to provide a default value or use a property-changed callback, you must define a PropertyMetadata instance that provides one or both of these capabilities.

通常,你将在 DependencyProperty.Register 的参数内提供一个 PropertyMetadata,作为一个内联创建的参数。Typically you provide a PropertyMetadata as an inline-created instance, within the parameters for DependencyProperty.Register.

备注

如果正在定义CreateDefaultValueCallback实现,则必须使用实用工具方法PropertyMetadata ,而不是调用PropertyMetadata构造函数来定义PropertyMetadata实例。If you are defining a CreateDefaultValueCallback implementation, you must use the utility method PropertyMetadata.Create rather than calling a PropertyMetadata constructor to define the PropertyMetadata instance.

下一个示例将通过使用 PropertyChangedCallback 值引用 PropertyMetadata 实例,修改先前显示的 DependencyProperty.Register 示例。This next example modifies the previously shown DependencyProperty.Register examples by referencing a PropertyMetadata instance with a PropertyChangedCallback value. 本节的后续内容中将介绍“OnLabelChanged”回调的实现。The implementation of the "OnLabelChanged" callback will be shown later in this section.

public static readonly DependencyProperty LabelProperty = DependencyProperty.Register(
  "Label",
  typeof(String),
  typeof(ImageWithLabelControl),
  new PropertyMetadata(null,new PropertyChangedCallback(OnLabelChanged))
);
Public Shared ReadOnly LabelProperty As DependencyProperty =
    DependencyProperty.Register("Label",
      GetType(String),
      GetType(ImageWithLabelControl),
      New PropertyMetadata(
        Nothing, new PropertyChangedCallback(AddressOf OnLabelChanged)))
// ImageWithLabelControl.cpp
...
Windows::UI::Xaml::DependencyProperty ImageWithLabelControl::m_labelProperty =
    Windows::UI::Xaml::DependencyProperty::Register(
        L"Label",
        winrt::xaml_typename<winrt::hstring>(),
        winrt::xaml_typename<ImageWithLabelControlApp::ImageWithLabelControl>(),
        Windows::UI::Xaml::PropertyMetadata{ nullptr, Windows::UI::Xaml::PropertyChangedCallback{ &ImageWithLabelControl::OnLabelChanged } }
);
...
DependencyProperty^ ImageWithLabelControl::_LabelProperty =
    DependencyProperty::Register("Label",
    Platform::String::typeid,
    ImageWithLabelControl::typeid,
    ref new PropertyMetadata(nullptr,
      ref new PropertyChangedCallback(&ImageWithLabelControl::OnLabelChanged))
    );

默认值Default value

你可以为依存关系属性指定一个默认值,这样,未设置该属性时,该属性将始终返回某个特定的默认值。You can specify a default value for a dependency property such that the property always returns a particular default value when it is unset. 此值不同于该属性的类型的固有默认值。This value can be different than the inherent default value for the type of that property.

如果未指定默认值,对于引用类型,依赖属性的默认值为空;对于值类型或语言原语,为该类型的默认值(例如 0 用于整型,或空字符串用于字符串)。If a default value is not specified, the default value for a dependency property is null for a reference type, or the default of the type for a value type or language primitive (for example, 0 for an integer or an empty string for a string). 建立默认值的主要原因是,你在属性上调用 ClearValue 时会还原此值。The main reason for establishing a default value is that this value is restored when you call ClearValue on the property. 为每个属性建立默认值可能比在构造函数中建立默认值更加方便,特别是对于值类型。Establishing a default value on a per-property basis might be more convenient than establishing default values in constructors, particularly for value types. 但是对于引用类型,请确保建立的默认值不会创建意外的单一实例模式。However, for reference types, make sure that establishing a default value does not create an unintentional singleton pattern. 有关详细信息,请参阅本主题后面的最佳实践For more info, see Best practices later in this topic

// ImageWithLabelControl.cpp
...
Windows::UI::Xaml::DependencyProperty ImageWithLabelControl::m_labelProperty =
    Windows::UI::Xaml::DependencyProperty::Register(
        L"Label",
        winrt::xaml_typename<winrt::hstring>(),
        winrt::xaml_typename<ImageWithLabelControlApp::ImageWithLabelControl>(),
        Windows::UI::Xaml::PropertyMetadata{ winrt::box_value(L"default label"), Windows::UI::Xaml::PropertyChangedCallback{ &ImageWithLabelControl::OnLabelChanged } }
);
...

备注

不要使用默认值dependencyproperty.unsetvalue进行注册。Do not register with a default value of UnsetValue. 如果注册了,它将让属性使用者难以理解,并且将在属性系统中产生意外的后果。If you do, it will confuse property consumers and will have unintended consequences within the property system.

CreateDefaultValueCallbackCreateDefaultValueCallback

在某些情况下,你将为在多个 UI 线程上使用的对象定义依存关系属性。In some scenarios, you are defining dependency properties for objects that are used on more than one UI thread. 如果你要定义由多个应用使用的某个数据对象,或者要定义你在多个应用中使用的某个控件,则可能属于这种情况。This might be the case if you are defining a data object that is used by multiple apps, or a control that you use in more than one app. 你可以通过提供一个 CreateDefaultValueCallback 实现(而不是一个默认值实例)来启用在不同 UI 线程之间对象的交换,默认值实例被绑定到注册该属性的线程。You can enable the exchange of the object between different UI threads by providing a CreateDefaultValueCallback implementation rather than a default value instance, which is tied to the thread that registered the property. 基本上,一个 CreateDefaultValueCallback 为默认值定义一个工厂。Basically a CreateDefaultValueCallback defines a factory for default values. CreateDefaultValueCallback 返回的值始终与正在使用该对象的当前 UI CreateDefaultValueCallback 线程相关联。The value returned by CreateDefaultValueCallback is always associated with the current UI CreateDefaultValueCallback thread that is using the object.

要定义指定某个 CreateDefaultValueCallback 的元数据,必须调用 PropertyMetadata.Create 来返回一个元数据实例;PropertyMetadata 构造函数没有包含 CreateDefaultValueCallback 参数的签名。To define metadata that specifies a CreateDefaultValueCallback, you must call PropertyMetadata.Create to return a metadata instance; the PropertyMetadata constructors do not have a signature that includes a CreateDefaultValueCallback parameter.

CreateDefaultValueCallback 的典型实现模式是创建一个新 DependencyObject 类,将 DependencyObject 的每个属性的特定属性值设置为预定的默认值,然后通过 CreateDefaultValueCallback 方法的返回值将新类返回为一个 Object 引用。The typical implementation pattern for a CreateDefaultValueCallback is to create a new DependencyObject class, set the specific property value of each property of the DependencyObject to the intended default, and then return the new class as an Object reference via the return value of the CreateDefaultValueCallback method.

属性已更改的回调方法Property-changed callback method

你可以定义一个属性已更改的回调方法来定义你的属性与其他依赖属性的交互,或者更新一个内部属性或该属性更改时的对象状态。You can define a property-changed callback method to define your property's interactions with other dependency properties, or to update an internal property or state of your object whenever the property changes. 如果调用该回调,则属性系统确定发生了有效的属性值更改。If your callback is invoked, the property system has determined that there is an effective property value change. 因为回调方法是静态的,所以回调的 d 参数很重要,因为它会告诉你类的哪个实例报告了更改情况。Because the callback method is static, the d parameter of the callback is important because it tells you which instance of the class has reported a change. 典型的实现使用事件数据的 NewValue 属性并以某种方式处理该值,通常是在作为 d 传递的对象上执行其他某种更改。A typical implementation uses the NewValue property of the event data and processes that value in some manner, usually by performing some other change on the object passed as d. 对属性更改的其他响应包括拒绝 NewValue 报告的值,还原 OldValue,或者将该值设置为应用于 NewValue 的编程约束。Additional responses to a property change are to reject the value reported by NewValue, to restore OldValue, or to set the value to a programmatic constraint applied to the NewValue.

下一个示例展示了一种 PropertyChangedCallback 实现。This next example shows a PropertyChangedCallback implementation. 它实现你在前面的 Register 示例中引用的方法,作为 PropertyMetadata 构造参数的一部分。It implements the method you saw referenced in the previous Register examples, as part of the construction arguments for the PropertyMetadata. 此回调解决的场景是,该类也有一个名为“HasLabelValue”的计算只读属性(未给出实现)。The scenario addressed by this callback is that the class also has a calculated read-only property named "HasLabelValue" (implementation not shown). 只要重新计算了“Label”属性,就会调用此回调方法,该回调使依赖的计算值与依赖属性的更改保持同步。Whenever the "Label" property gets reevaluated, this callback method is invoked, and the callback enables the dependent calculated value to remain in synchronization with changes to the dependency property.

private static void OnLabelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
    ImageWithLabelControl iwlc = d as ImageWithLabelControl; //null checks omitted
    String s = e.NewValue as String; //null checks omitted
    if (s == String.Empty)
    {
        iwlc.HasLabelValue = false;
    } else {
        iwlc.HasLabelValue = true;
    }
}
    Private Shared Sub OnLabelChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
        Dim iwlc As ImageWithLabelControl = CType(d, ImageWithLabelControl) ' null checks omitted
        Dim s As String = CType(e.NewValue,String) ' null checks omitted
        If s Is String.Empty Then
            iwlc.HasLabelValue = False
        Else
            iwlc.HasLabelValue = True
        End If
    End Sub
void ImageWithLabelControl::OnLabelChanged(Windows::UI::Xaml::DependencyObject const& d, Windows::UI::Xaml::DependencyPropertyChangedEventArgs const& e)
{
    auto iwlc{ d.as<ImageWithLabelControlApp::ImageWithLabelControl>() };
    auto s{ winrt::unbox_value<winrt::hstring>(e.NewValue()) };
    iwlc.HasLabelValue(s.size() != 0);
}
static void OnLabelChanged(DependencyObject^ d, DependencyPropertyChangedEventArgs^ e)
{
    ImageWithLabelControl^ iwlc = (ImageWithLabelControl^)d;
    Platform::String^ s = (Platform::String^)(e->NewValue);
    if (s->IsEmpty()) {
        iwlc->HasLabelValue=false;
    }
}

结构和枚举的属性已更改行为Property changed behavior for structures and enumerations

如果 DependencyProperty 的类型为枚举或结构,则可能会调用该回调,即使结构的内部值或枚举值未改变时也是如此。If the type of a DependencyProperty is an enumeration or a structure, the callback may be invoked even if the internal values of the structure or the enumeration value did not change. 这与系统基元(如仅当值改变时才会调用的字符串)不同。This is different from a system primitive such as a string where it only is invoked if the value changed. 这是在内部执行的对这些值的装箱和取消装箱操作的一个副作用。This is a side effect of box and unbox operations on these values that is done internally. 如果你的值是枚举或结构时,你有一个针对某个属性的 PropertyChangedCallback 方法,那么你需要通过自己转换值并使用提供给即时转换值的超负荷的比较运算符来比较 OldValueNewValueIf you have a PropertyChangedCallback method for a property where your value is an enumeration or structure, you need to compare the OldValue and NewValue by casting the values yourself and using the overloaded comparison operators that are available to the now-cast values. 或者,如果没有这样的运算符(自定义结构可能是这种情形),那么你可能需要比较各个值。Or, if no such operator is available (which might be the case for a custom structure), you may need to compare the individual values. 如果结果是值未改变,那么你通常不会采取任何操作。You would typically choose to do nothing if the result is that the values have not changed.

private static void OnVisibilityValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
    if ((Visibility)e.NewValue != (Visibility)e.OldValue)
    {
        //value really changed, invoke your changed logic here
    } // else this was invoked because of boxing, do nothing
}
Private Shared Sub OnVisibilityValueChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
    If CType(e.NewValue,Visibility) != CType(e.OldValue,Visibility) Then
        '  value really changed, invoke your changed logic here
    End If
    '  else this was invoked because of boxing, do nothing
End Sub
static void OnVisibilityValueChanged(Windows::UI::Xaml::DependencyObject const& d, Windows::UI::Xaml::DependencyPropertyChangedEventArgs const& e)
{
    auto oldVisibility{ winrt::unbox_value<Windows::UI::Xaml::Visibility>(e.OldValue()) };
    auto newVisibility{ winrt::unbox_value<Windows::UI::Xaml::Visibility>(e.NewValue()) };

    if (newVisibility != oldVisibility)
    {
        // The value really changed; invoke your property-changed logic here.
    }
    // Otherwise, OnVisibilityValueChanged was invoked because of boxing; do nothing.
}
static void OnVisibilityValueChanged(DependencyObject^ d, DependencyPropertyChangedEventArgs^ e)
{
    if ((Visibility)e->NewValue != (Visibility)e->OldValue)
    {
        //value really changed, invoke your changed logic here
    } 
    // else this was invoked because of boxing, do nothing
    }
}

最佳实践Best practices

在定义自定义依赖属性时,将以下考虑因素作为最佳实践。Keep the following considerations in mind as best practices when as you define your custom dependency property.

DependencyObject 和线程处理DependencyObject and threading

所有 DependencyObject 实例都必须在与 Windows 运行时应用所显示的当前 Window 相关联的 UI 线程上创建。All DependencyObject instances must be created on the UI thread which is associated with the current Window that is shown by a Windows Runtime app. 虽然每个 DependencyObject 都必须在主 UI 线程上创建,但可以通过调用 Dispatcher 从其他线程使用调度程序引用来访问这些对象。Although each DependencyObject must be created on the main UI thread, the objects can be accessed using a dispatcher reference from other threads, by calling Dispatcher.

DependencyObject 的线程处理特性很重要,因为这通常意味着只有那些在 UI 线程上运行的代码才能更改或读取依赖属性的值。The threading aspects of DependencyObject are relevant because it generally means that only code that runs on the UI thread can change or even read the value of a dependency property. 在正确使用 async 模式和后台工作线程的典型 UI 代码中,通常可以避免线程处理问题。Threading issues can usually be avoided in typical UI code that makes correct use of async patterns and background worker threads. 通常,如果你定义自己的 DependencyObject 类型并尝试将这些类型用于 DependencyObject 未必适宜的数据源或其他场景,只会遇到与 DependencyObject 相关的线程处理问题。You typically only run into DependencyObject-related threading issues if you are defining your own DependencyObject types and you attempt to use them for data sources or other scenarios where a DependencyObject isn't necessarily appropriate.

避免意外的单一实例Avoiding unintentional singletons

如果声明一个接受引用类型的依赖属性,并且你在建立 PropertyMetadata 的代码中针对该引用类型调用了一个构造函数,可能产生意外的单一实例。An unintentional singleton can happen if you are declaring a dependency property that takes a reference type, and you call a constructor for that reference type as part of the code that establishes your PropertyMetadata. 发生的事情是,依赖属性的所有用途仅共享一个 PropertyMetadata 实例,进而尝试共享你构造的单个引用类型。What happens is that all usages of the dependency property share just one instance of PropertyMetadata and thus try to share the single reference type you constructed. 你通过依赖属性设置的该值类型的任何子属性随后会以你可能意想不到的方式传播到其他对象。Any subproperties of that value type that you set through your dependency property then propagate to other objects in ways you may not have intended.

如果想要一个非空值,可以使用类构造函数设置一个引用类型依赖属性的初始值,但请注意,出于依赖属性概述用途,这将被视为一个局部值。You can use class constructors to set initial values for a reference-type dependency property if you want a non-null value, but be aware that this would be considered a local value for purposes of Dependency properties overview. 如果你的类支持模板,可能将模板用于此用途会更合适。It might be more appropriate to use a template for this purpose, if your class supports templates. 另一种避免单一实例模式,但仍然提供有用默认值的方式为,在引用类型上公开一个为该类的值提供合适默认值的静态属性。Another way to avoid a singleton pattern, but still provide a useful default, is to expose a static property on the reference type that provides a suitable default for the values of that class.

集合类型依赖属性Collection-type dependency properties

集合类型依赖属性有另外一些实现问题需要考虑。Collection-type dependency properties have some additional implementation issues to consider.

集合类型依赖属性在 Windows 运行时 API 中相对较少。Collection-type dependency properties are relatively rare in the Windows Runtime API. 在大部分情况下,可以在各项内容是一个 DependencyObject 子类时使用集合,但集合属性本身实现为一种传统的 CLR 或 C++ 属性。In most cases, you can use collections where the items are a DependencyObject subclass, but the collection property itself is implemented as a conventional CLR or C++ property. 这是因为集合不一定适用于某些调用依赖属性的典型场景。This is because collections do not necessarily suit some typical scenarios where dependency properties are involved. 例如:For example:

  • 你通常不会为集合制作动画。You do not typically animate a collection.
  • 你通常不会使用样式或模板预先填充集合中的各项。You do not typically prepopulate the items in a collection with styles or a template.
  • 尽管绑定到集合是一种主要的场景,但集合不需要将依赖属性用作绑定来源。Although binding to collections is a major scenario, a collection does not need to be a dependency property to be a binding source. 对于绑定目标,更典型的用法是使用 ItemsControlDataTemplate 的子类来支持集合项,或使用视图-模型模式。For binding targets, it is more typical to use subclasses of ItemsControl or DataTemplate to support collection items, or to use view-model patterns. 有关绑定到集合和从集合绑定的详细信息,请参阅深入了解数据绑定For more info about binding to and from collections, see Data binding in depth.
  • 集合更改通知问题最好通过 INotifyPropertyChangedINotifyCollectionChanged 等接口,或通过从 ObservableCollection<T> 派生的集合类型来解决。Notifications for collection changes are better addressed through interfaces such as INotifyPropertyChanged or INotifyCollectionChanged, or by deriving the collection type from ObservableCollection<T>.

但是,有些场景确实需要集合类型依赖属性。Nevertheless, scenarios for collection-type dependency properties do exist. 接下来的 3 节提供了有关如何实现集合类型依赖属性的一些指南。The next three sections provide some guidance on how to implement a collection-type dependency property.

初始化集合Initializing the collection

创建依赖属性时,可通过依赖属性元数据的形式建立一个默认值。When you create a dependency property, you can establish a default value by means of dependency property metadata. 但请注意,不要使用单一实例静态集合作为默认值。But be careful to not use a singleton static collection as the default value. 相反,必须在集合属性的所有者类的类构造函数逻辑中特意将集合值设置为一个唯一的(实例)集合。Instead, you must deliberately set the collection value to a unique (instance) collection as part of class-constructor logic for the owner class of the collection property.

更改通知Change notifications

将集合定义为依赖属性时,不会通过调用“PropertyChanged”回调方法的属性系统自动为集合中的各项提供更改通知。Defining the collection as a dependency property does not automatically provide change notification for the items in the collection by virtue of the property system invoking the "PropertyChanged" callback method. 如果想要集合或集合项的通知(例如用于数据绑定场景),可实现 INotifyPropertyChangedINotifyCollectionChanged 接口。If you want notifications for collections or collection items—for example, for a data-binding scenario— implement the INotifyPropertyChanged or INotifyCollectionChanged interface. 有关详细信息,请参阅深入了解数据绑定For more info, see Data binding in depth.

依赖属性安全注意事项Dependency property security considerations

将依赖属性声明为公共属性。Declare dependency properties as public properties. 将依赖属性标识符声明为公共静态只读成员。Declare dependency property identifiers as public static readonly members. 即使尝试声明语言所允许的其他访问级别(例如 protected),也始终可以结合使用标识符和属性系统 API 来访问依赖属性。Even if you attempt to declare other access levels permitted by a language (such as protected), a dependency property can always be accessed through the identifier in combination with the property-system APIs. 将依赖属性标识符声明为内部或私有不起作用,因为这样属性系统无法正常操作。Declaring the dependency property identifier as internal or private will not work, because then the property system cannot operate properly.

包装器属性实际上只是为了提供方便,应用于包装器的安全机制可通过调用 GetValueSetValue 来绕过。Wrapper properties are really just for convenience, Security mechanisms applied to the wrappers can be bypassed by calling GetValue or SetValue instead. 所以请保持包装器属性为公共的;否则只会使属性更难被合法调用方使用,并且不会提供任何切实的安全优势。So keep wrapper properties public; otherwise you just make your property harder for legitimate callers to use without providing any real security benefit.

Windows 运行时没有提供将自定义依赖属性注册为只读的方式。The Windows Runtime does not provide a way to register a custom dependency property as read-only.

依赖属性和类构造函数Dependency properties and class constructors

一条一般原则是类构造函数不应调用虚拟方法。There is a general principle that class constructors should not call virtual methods. 这是因为可调用构造函数来完成派生类构造函数的基本初始化工作,并且在构造的对象实例未完成初始化时,可能发生通过构造函数进入虚拟方法的情形。This is because constructors can be called to accomplish base initialization of a derived class constructor, and entering the virtual method through the constructor might occur when the object instance being constructed is not yet completely initialized. 当通过任何派生自 DependencyObject 的类进行派生时,请记住属性系统本身会在其服务中从内部调用和公开虚拟方法。When you derive from any class that already derives from DependencyObject, remember that the property system itself calls and exposes virtual methods internally as part of its services. 要避免运行时初始化的潜在问题,不要在类的构造函数中设置依赖属性值。To avoid potential problems with run-time initialization, don't set dependency property values within constructors of classes.

注册 C++/CX 应用的依赖属性Registering the dependency properties for C++/CX apps

由于分为标头文件和实现文件以及在实现文件的作用域上进行初始化是错误做法,所以在 C++/CX 中注册属性的实现比在 C# 中实现更为复杂。The implementation for registering a property in C++/CX is trickier than C#, both because of the separation into header and implementation file and also because initialization at the root scope of the implementation file is a bad practice. (可视化C++组件扩展(C++/cx)将根范围中的静态初始值设定项代码直接放入C# DllMain,而编译器将静态初始值设定项分配给类,从而避免DllMain加载锁问题。)。(Visual C++ component extensions (C++/CX) puts static initializer code from the root scope directly into DllMain, whereas C# compilers assign the static initializers to classes and thus avoid DllMain load lock issues.). 此处执行的最佳做法是为某个类声明可注册所有依赖属性的 helper 函数,一个类对应一个函数。The best practice here is to declare a helper function that does all your dependency property registration for a class, one function per class. 然后,对于你的应用使用的每个自定义类,必须引用要使用的每个自定义类公开的 helper 注册函数。Then for each custom class your app consumes, you'll have to reference the helper registration function that's exposed by each custom class you want to use. InitializeComponent 之前,调用每个 helper 注册函数以作为 Application constructor (App::App()) 的一部分。Call each helper registration function once as part of the Application constructor (App::App()), prior to InitializeComponent. 例如,该构造函数仅在首次引用应用时运行,如果恢复暂停的应用,该构造函数不会再次运行。That constructor only runs when the app is really referenced for the first time, it won't run again if a suspended app resumes, for example. 同样,如之前 C++ 注册示例所示,每个 Register 调用周围的 nullptr 标识非常重要:它确保该函数的任何调用方均不能注册此属性两次。Also, as seen in the previous C++ registration example, the nullptr check around each Register call is important: it's insurance that no caller of the function can register the property twice. 第二次注册调用可能会导致没有此类标识的应用崩溃,因为属性名称可能重复。A second registration call would probably crash your app without such a check because the property name would be a duplicate. 如果你要查找 C++/CX 版本示例的代码,请参阅 XAML 用户和自定义控件示例中的这一实现模式。You can see this implementation pattern in the XAML user and custom controls sample if you look at the code for the C++/CX version of the sample.