自定义附加属性Custom attached properties

附加属性是一种 XAML 概念。An attached property is a XAML concept. 附加属性通常定义为一种特殊形式的依赖属性。Attached properties are typically defined as a specialized form of dependency property. 本主题介绍如何将一个 XAML 附加属性实现为依赖属性,如何定义让附加属性可用于 XAML 所必需的访问器约定。This topic explains how to implement an attached property as a dependency property and how to define the accessor convention that is necessary for your attached property to be usable in XAML.

先决条件Prerequisites

我们假设你能从现有依赖属性的客户角度理解依赖属性,并且已阅读了依赖属性概述We assume that you understand dependency properties from the perspective of a consumer of existing dependency properties, and that you have read the Dependency properties overview. 你还应该阅读了附加属性概述You should also have read Attached properties overview. 要理解本主题中的示例,你还应该理解 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.

附加属性的使用场景Scenarios for attached properties

除了定义类,如果有理由提供其他属性设置机制,则可以创建一个附加属性。You might create an attached property when there is a reason to have a property-setting mechanism available for classes other than the defining class. 最常见的情况是布局和服务支持。The most common scenarios for this are layout and services support. 现有布局属性的示例包括 Canvas.ZIndexCanvas.TopExamples of existing layout properties are Canvas.ZIndex and Canvas.Top. 在布局场景中,以布局控制元素的子元素形式存在的元素可单独向其父元素表达布局需求,每个元素设置一个被其父元素定义为附加属性的属性值。In a layout scenario, elements that exist as child elements to layout-controlling elements can express layout requirements to their parent elements individually, each setting a property value that the parent defines as an attached property. Windows 运行时 API 中服务支持方案的一个示例是 ScrollViewer 的一组附加属性,例如 ScrollViewer.IsZoomChainingEnabledAn example of the services-support scenario in the Windows Runtime API is set of the attached properties of ScrollViewer, such as ScrollViewer.IsZoomChainingEnabled.

警告

Windows 运行时 XAML 实现的现有限制是, 无法对自定义附加属性进行动画处理。An existing limitation of the Windows Runtime XAML implementation is that you cannot animate your custom attached property.

注册自定义附加属性Registering a custom attached property

如果将附加属性严格定义为在其他类型上使用,在其中注册该属性的类不必派生自 DependencyObjectIf you are defining the attached property strictly for use on other types, the class where the property is registered does not have to derive from DependencyObject. 但是,如果你采用将附加属性也用作依赖属性的典型模型,则需要让访问器的目标参数使用 DependencyObject,以便你可以使用支持属性存储。But you do need to have the target parameter for accessors use DependencyObject if you follow the typical model of having your attached property also be a dependency property, so that you can use the backing property store.

通过声明类型为DependencyProperty公共 静态 readonly属性, 将附加属性定义为依赖属性。Define your attached property as a dependency property by declaring a public static readonly property of type DependencyProperty. 你可以使用 RegisterAttached 方法的返回值来定义此属性。You define this property by using the return value of the RegisterAttached method. 属性名称必须与指定为system.windows.dependencyproperty.registerattached name参数的附加属性名称匹配, 并将字符串 "property" 添加到末尾。The property name must match the attached property name you specify as the RegisterAttached name parameter, with the string "Property" added to the end. 这是相对于其表示的属性来命名依赖属性标识符的既有约定。This is the established convention for naming the identifiers of dependency properties in relation to the properties that they represent.

定义自定义附加属性与自定义依赖属性的主要区别在定义访问器或包装器的方式上。The main area where defining a custom attached property differs from a custom dependency property is in how you define the accessors or wrappers. 并不使用自定义依赖属性中介绍的包装器技术,你还必须提供静态的 GetPropertyNameSetPropertyName 方法作为附加属性的访问器。Instead of the using the wrapper technique described in Custom dependency properties, you must also provide static GetPropertyName and SetPropertyName methods as accessors for the attached property. 访问器多数供 XAML 分析器使用,但任何其他调用方也可以使用它们来设置非 XAML 场景中的值。The accessors are used mostly by the XAML parser, although any other caller can also use them to set values in non-XAML scenarios.

重要

如果未正确定义访问器, 则 XAML 处理器无法访问附加属性, 尝试使用它的任何人都可能会收到 XAML 分析器错误。If you don't define the accessors correctly, the XAML processor can't access your attached property and anyone who tries to use it will probably get a XAML parser error. 此外, 设计和编码工具通常依赖于在引用*的程序集中遇到自定义依赖属性时命名标识符的 "属性" 约定。Also, design and coding tools often rely on the "*Property" conventions for naming identifiers when they encounter a custom dependency property in a referenced assembly.

访问器Accessors

GetPropertyName 访问器的签名必须如下所示。The signature for the GetPropertyName accessor must be this.

public staticvalueType获取PropertyName(DependencyObject target)public static valueType GetPropertyName (DependencyObject target)

对于 Microsoft Visual Basic,应如下所示。For Microsoft Visual Basic, it is this.

Public Shared Function Get_PropertyName_valueType(ByVal target As DependencyObject) As ``)Public Shared Function GetPropertyName(ByVal target As DependencyObject) AsvalueType)

target 对象可以是实现中的一种更为具体的类型,但必须派生自 DependencyObjectThe target object can be of a more specific type in your implementation, but must derive from DependencyObject. valueType 返回值也可以是你的实现中一种更为具体的类型。The valueType return value can also be of a more specific type in your implementation. 基本的 Object 类型也可接受,但通常希望附加属性执行类型安全性。The basic Object type is acceptable, but often you'll want your attached property to enforce type safety. getter 和 setter 签名中对类型的使用是一种推荐的类型安全技术。The use of typing in the getter and setter signatures is a recommended type-safety technique.

SetPropertyName 访问器的签名必须如下所示。The signature for the SetPropertyName accessor must be this.

public static void Set_PropertyName_valueType(DependencyObject target , `` value)public static void SetPropertyName(DependencyObject target ,valueTypevalue)

对于 Visual Basic,应如下所示。For Visual Basic, it is this.

Public Shared Sub Set_PropertyName_valueType(ByVal target As DependencyObject, ByVal value As ``)Public Shared Sub SetPropertyName(ByVal target As DependencyObject, ByVal value AsvalueType)

target 对象可以是实现中的一种更为具体的类型,但必须派生自 DependencyObjectThe target object can be of a more specific type in your implementation, but must derive from DependencyObject. value 对象和它的 valueType 可以是你的实现中一种更为具体的类型。The value object and its valueType can be of a more specific type in your implementation. 请记住,此方法的值是 XAML 处理器在标记中遇到你的附加属性时提供的输入。Remember that the value for this method is the input that comes from the XAML processor when it encounters your attached property in markup. 你使用的类型必须具有类型转换或现有的标记扩展支持,这样才能通过该特性值(最终是一个字符串)创建合适的类型。There must be type conversion or existing markup extension support for the type you use, so that the appropriate type can be created from an attribute value (which is ultimately just a string). 基本的 Object 类型也可接受,但通常希望进一步增强类型安全性。The basic Object type is acceptable, but often you'll want further type safety. 为此,请在取值函数中增加类型增强措施。To accomplish that, put type enforcement in the accessors.

备注

还可以通过属性元素语法定义要使用的附加属性。It's also possible to define an attached property where the intended usage is through property element syntax. 在此情况下,你不需要对值进行类型转换,但需要确保所需的值可采用 XAML 构造。In that case you don't need type conversion for the values, but you do need to assure that the values you intend can be constructed in XAML. VisualStateManager。 system.windows.visualstatemanager.visualstategroups是一个仅支持属性元素用法的现有附加属性的示例。VisualStateManager.VisualStateGroups is an example of an existing attached property that only supports property element usage.

代码示例Code example

此示例展示了依赖属性注册(使用 RegisterAttached 方法),以及一个自定义附加属性的 GetSet 访问器。This example shows the dependency property registration (using the RegisterAttached method), as well as the Get and Set accessors, for a custom attached property. 在此示例中,附加属性名称为 IsMovableIn the example, the attached property name is IsMovable. 因此,访问器必须命名为 GetIsMovableSetIsMovableTherefore, the accessors must be named GetIsMovable and SetIsMovable. 附加属性的所有者是自身不具有 UI 的名为 GameService 服务类;其目的只是在使用 GameService.IsMovable 附加属性时提供附加属性服务。The owner of the attached property is a service class named GameService that doesn't have a UI of its own; its purpose is only to provide the attached property services when the GameService.IsMovable attached property is used.

在/Cx 中C++定义附加属性会稍微复杂一些。Defining the attached property in C++/CX is a bit more complex. 必须决定如何协调标头文件和代码文件。You have to decide how to factor between the header and code file. 另外,应该将标识符公开为只有一个 get 访问器的属性,原因如自定义依赖属性中所述。Also, you should expose the identifier as a property with only a get accessor, for reasons discussed in Custom dependency properties. 在C++/cx 中, 必须显式定义此属性字段关系, 而不是依赖于 .net readonly keywording 和简单属性的隐式支持。In C++/CX you must define this property-field relationship explicitly rather than relying on .NET readonly keywording and implicit backing of simple properties. 你还需要在首次启动应用时,在加载需要附加属性的任何 XAML 页面之前,在仅运行一次的帮助程序函数中执行附加属性的注册操作。You also need to perform the registration of the attached property within a helper function that only gets run once, when the app first starts but before any XAML pages that need the attached property are loaded. 为所有依赖属性或附加属性调用属性注册帮助程序函数的典型位置是 app.xaml 文件代码的 App / Application 构造函数内。The typical place to call your property registration helper functions for any and all dependency or attached properties is from within the App / Application constructor in the code for your app.xaml file.

public class GameService : DependencyObject
{
    public static readonly DependencyProperty IsMovableProperty = 
    DependencyProperty.RegisterAttached(
      "IsMovable",
      typeof(Boolean),
      typeof(GameService),
      new PropertyMetadata(false)
    );
    public static void SetIsMovable(UIElement element, Boolean value)
    {
        element.SetValue(IsMovableProperty, value);
    }
    public static Boolean GetIsMovable(UIElement element)
    {
        return (Boolean)element.GetValue(IsMovableProperty);
    }
}
Public Class GameService
    Inherits DependencyObject

    Public Shared ReadOnly IsMovableProperty As DependencyProperty = 
        DependencyProperty.RegisterAttached("IsMovable",  
        GetType(Boolean), 
        GetType(GameService), 
        New PropertyMetadata(False))

    Public Shared Sub SetIsMovable(ByRef element As UIElement, value As Boolean)
        element.SetValue(IsMovableProperty, value)
    End Sub

    Public Shared Function GetIsMovable(ByRef element As UIElement) As Boolean
        GetIsMovable = CBool(element.GetValue(IsMovableProperty))
    End Function
End Class
// GameService.idl
namespace UserAndCustomControls
{
    [default_interface]
    runtimeclass GameService : Windows.UI.Xaml.DependencyObject
    {
        GameService();
        static Windows.UI.Xaml.DependencyProperty IsMovableProperty{ get; };
        static Boolean GetIsMovable(Windows.UI.Xaml.DependencyObject target);
        static void SetIsMovable(Windows.UI.Xaml.DependencyObject target, Boolean value);
    }
}

// GameService.h
...
    static Windows::UI::Xaml::DependencyProperty IsMovableProperty() { return m_IsMovableProperty; }
    static bool GetIsMovable(Windows::UI::Xaml::DependencyObject const& target) { return winrt::unbox_value<bool>(target.GetValue(m_IsMovableProperty)); }
    static void SetIsMovable(Windows::UI::Xaml::DependencyObject const& target, bool value) { target.SetValue(m_IsMovableProperty, winrt::box_value(value)); }

private:
    static Windows::UI::Xaml::DependencyProperty m_IsMovableProperty;
...

// GameService.cpp
...
Windows::UI::Xaml::DependencyProperty GameService::m_IsMovableProperty =
    Windows::UI::Xaml::DependencyProperty::RegisterAttached(
        L"IsMovable",
        winrt::xaml_typename<bool>(),
        winrt::xaml_typename<UserAndCustomControls::GameService>(),
        Windows::UI::Xaml::PropertyMetadata{ winrt::box_value(false) }
);
...
// GameService.h
#pragma once

#include "pch.h"
//namespace WUX = Windows::UI::Xaml;

namespace UserAndCustomControls {
    public ref class GameService sealed : public WUX::DependencyObject {
    private:
        static WUX::DependencyProperty^ _IsMovableProperty;
    public:
        GameService::GameService();
        void GameService::RegisterDependencyProperties();
        static property WUX::DependencyProperty^ IsMovableProperty
        {
            WUX::DependencyProperty^ get() {
                return _IsMovableProperty;
            }
        };
        static bool GameService::GetIsMovable(WUX::UIElement^ element) {
            return (bool)element->GetValue(_IsMovableProperty);
        };
        static void GameService::SetIsMovable(WUX::UIElement^ element, bool value) {
            element->SetValue(_IsMovableProperty,value);
        }
    };
}

// GameService.cpp
#include "pch.h"
#include "GameService.h"

using namespace UserAndCustomControls;

using namespace Platform;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::UI::Xaml;
using namespace Windows::UI::Xaml::Controls;
using namespace Windows::UI::Xaml::Data;
using namespace Windows::UI::Xaml::Documents;
using namespace Windows::UI::Xaml::Input;
using namespace Windows::UI::Xaml::Interop;
using namespace Windows::UI::Xaml::Media;

GameService::GameService() {};

GameService::RegisterDependencyProperties() {
    DependencyProperty^ GameService::_IsMovableProperty = DependencyProperty::RegisterAttached(
         "IsMovable", Platform::Boolean::typeid, GameService::typeid, ref new PropertyMetadata(false));
}

从 XAML 标记设置自定义附加属性Setting your custom attached property from XAML markup

备注

如果使用C++的是/WinRT, 请跳到下一节 ( C++使用/WinRT 强制设置自定义附加属性)。If you're using C++/WinRT, then skip to the following section (Setting your custom attached property imperatively with C++/WinRT).

定义附加属性并将它的支持成员包含在一个自定义类型中后,你必须让这些定义可供 XAML 使用。After you have defined your attached property and included its support members as part of a custom type, you must then make the definitions available for XAML usage. 为此,你必须映射一个 XAML 命名空间,它将引用其中包含相关类的代码命名空间。To do this, you must map a XAML namespace that will reference the code namespace that contains the relevant class. 如果在一个库中定义了附加属性,必须将该库包含在应用的应用程序包中。In cases where you have defined the attached property as part of a library, you must include that library as part of the app package for the app.

XAML 的 XML 命名空间映射通常位于一个 XAML 页面的根元素中。An XML namespace mapping for XAML is typically placed in the root element of a XAML page. 例如,对于命名空间 UserAndCustomControls 中有一个名为 GameService 的类,它包含前面代码段中所示的附加属性定义,映射类似于这样。For example, for the class named GameService in the namespace UserAndCustomControls that contains the attached property definitions shown in preceding snippets, the mapping might look like this.

<UserControl
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:uc="using:UserAndCustomControls"
  ... >

使用映射,你可以在任何与目标定义相匹配的元素(包括 Windows 运行时定义的一种现有类型)上设置 GameService.IsMovable 附加属性。Using the mapping, you can set your GameService.IsMovable attached property on any element that matches your target definition, including an existing type that Windows Runtime defines.

<Image uc:GameService.IsMovable="True" .../>

如果在一个元素上设置附加属性并且该元素包含在同一个映射的 XML 命名空间中,则必须在附加属性名称中包含该前缀。If you are setting the property on an element that is also within the same mapped XML namespace, you still must include the prefix on the attached property name. 这是因为该前缀限定了所有者类型。This is because the prefix qualifies the owner type. 不能假设附加属性的特性与包含该特性的元素位于相同的 XML 命名空间中,尽管按照正常的 XML 规则,特性可从元素继承命名空间。The attached property's attribute cannot be assumed to be within the same XML namespace as the element where the attribute is included, even though, by normal XML rules, attributes can inherit namespace from elements. 例如,如果在一种自定义类型 ImageWithLabelControl(未给出定义)上设置 GameService.IsMovable,即使二者都在映射到同一个前缀的同一个代码命名空间中定义,XAML 仍将是这样的。For example, if you are setting GameService.IsMovable on a custom type of ImageWithLabelControl (definition not shown), and even if both were defined in the same code namespace mapped to same prefix, the XAML would still be this.

<uc:ImageWithLabelControl uc:GameService.IsMovable="True" .../>

备注

如果要使用C++/CX 编写 XAML UI, 则必须包含定义附加属性的自定义类型的标头, 无论 XAML 页何时使用该类型。If you are writing a XAML UI with C++/CX, then you must include the header for the custom type that defines the attached property, any time that a XAML page uses that type. 每个 XAML 页都具有关联的代码隐藏标头 (.xaml .h)。Each XAML page has an associated code-behind header (.xaml.h). 你应在此中包括 (使用 #include) 附加属性的所有者类型定义的标头。This is where you should include (using #include) the header for the definition of the attached property's owner type.

通过C++/WinRT 强制设置自定义附加属性Setting your custom attached property imperatively with C++/WinRT

如果你使用C++的是/WinRT, 则可以从命令性代码 (而不是 XAML 标记) 访问自定义附加属性。If you're using C++/WinRT, then you can access a custom attached property from imperative code, but not from XAML markup. 下面的代码演示了如何操作。The code below shows how.

<Image x:Name="gameServiceImage"/>
// MainPage.h
...
#include "GameService.h"
...

// MainPage.cpp
...
MainPage::MainPage()
{
    InitializeComponent();

    GameService::SetIsMovable(gameServiceImage(), true);
}
...

自定义附加属性的值类型Value type of a custom attached property

用作自定义附加属性的值类型的类型会影响附加属性使用、定义或同时影响二者。The type that is used as the value type of a custom attached property affects the usage, the definition, or both the usage and definition. 附加属性的值类型在多个位置声明:在 GetSet 访问器方法的签名中,以及作为 RegisterAttached 调用的 propertyType 参数。The attached property's value type is declared in several places: in the signatures of both the Get and Set accessor methods, and also as the propertyType parameter of the RegisterAttached call.

附加属性(无论是否为自定义的)最常用的值类型是一个简单字符串。The most common value type for attached properties (custom or otherwise) is a simple string. 这是因为附加属性一般用作 XAML 特性,并且使用一个字符串作为值类型可保持属性的简单性。This is because attached properties are generally intended for XAML attribute usage, and using a string as the value type keeps the properties lightweight. 可本地转换为字符串方法的其他原语(例如整型、双精度或枚举值)也是常用的附加属性值类型。Other primitives that have native conversion to string methods, such as integer, double, or an enumeration value, are also common as value types for attached properties. 你可以使用其他值类型,不支持本地字符串转换的类型作为附加属性值。You can use other value types—ones that don't support native string conversion—as the attached property value. 但是,这需要选择是使用还是实现:However, this entails making a choice about either the usage or the implementation:

  • 你可以将附加属性保持不变,但附加属性只能在它是一个属性元素并且它的值声明为对象元素时才支持使用。You can leave the attached property as it is, but the attached property can support usage only where the attached property is a property element, and the value is declared as an object element. 在此情况下,属性类型必须支持用作对象元素的 XAML 用法。In this case, the property type does have to support XAML usage as an object element. 对于现有的 Windows 运行时引用类,请检查 XAML 语法,确保该类型支持 XAML 对象元素用法。For existing Windows Runtime reference classes, check the XAML syntax to make sure that the type supports XAML object element usage.
  • 你可以将附加属性保持不变,但只能通过一种 XAML 引用技术和特性用法来使用它,例如可表示为一个字符串的 BindingStaticResourceYou can leave the attached property as it is, but use it only in an attribute usage through a XAML reference technique such as a Binding or StaticResource that can be expressed as a string.

有关 Canvas.Left 示例的详细信息More about the Canvas.Left example

在先前的附加属性用法示例中,我们显示了几种用来设置 Canvas.Left 附加属性的方法。In earlier examples of attached property usages we showed different ways to set the Canvas.Left attached property. 但是,这对于 Canvas 与你的对象的交互方式有何更改,这是在何时发生的?But what does that change about how a Canvas interacts with your object, and when does that happen? 我们将在这个特定示例中深入介绍,原因在于:如果你实现了一个附加属性,则会发现一个有趣的情况,那就是当典型的附加属性所有者类在其他对象上发现了它的附加属性值时,它应当会对这些值进行处理。We'll examine this particular example further, because if you implement an attached property, it's interesting to see what else a typical attached property owner class intends to do with its attached property values if it finds them on other objects.

Canvas 的主要功能是成为 UI 中具有绝对位置的布局容器。The main function of a Canvas is to be an absolute-positioned layout container in UI. Canvas 的子项存储在由基类定义的 Children 属性中。The children of a Canvas are stored in a base-class defined property Children. 在所有的面板中,Canvas 是唯一一个使用绝对位置的面板。Of all the panels Canvas is the only one that uses absolute positioning. 如果在添加属性时仅关注 Canvas,或者在 UIElement 作为 UIElement 的子元素的特定情况下,该面板中会充斥着常见 UIElement 类的对象模型。It would've bloated the object model of the common UIElement type to add properties that might only be of concern to Canvas and those particular UIElement cases where they are child elements of a UIElement. Canvas 的布局控件属性定义为可由任何 UIElement 用来使对象模型更简洁的附加属性。Defining the layout control properties of a Canvas to be attached properties that any UIElement can use keeps the object model cleaner.

为了成为实际的面板,Canvas 具有可替代框架级 MeasureArrange 方法的行为。In order to be a practical panel, Canvas has behavior that overrides the framework-level Measure and Arrange methods. Canvas 就是在这里实际检查其子项上的附加属性值。This is where Canvas actually checks for attached property values on its children. MeasureArrange 模式的一部分就是一个遍历任何内容的循环,一个面板具有 Children 属性,通过该属性可以明确假设被视为面板子项的内容。Part of both the Measure and Arrange patterns is a loop that iterates over any content, and a panel has the Children property that makes it explicit what's supposed to be considered the child of a panel. 因此,Canvas 布局行为会循环访问这些子项,并针对每个子项进行静态 Canvas.GetLeftCanvas.GetTop 调用,查看这些附加属性是否包含非默认值(默认值为 0)。So the Canvas layout behavior iterates through these children, and makes static Canvas.GetLeft and Canvas.GetTop calls on each child to see whether those attached properties contain a non-default value (default is 0). 之后,系统将使用这些值,按照由每个子项提供的特定值将每个子项以绝对位置方式放置到 Canvas 中的可用布局空间中。然后系统将使用 Arrange 提交这些值。These values are then used to absolutely position each child in the Canvas available layout space according to the specific values provided by each child, and committed using Arrange.

此代码类似于此伪代码。The code looks something like this pseudocode.

protected override Size ArrangeOverride(Size finalSize)
{
    foreach (UIElement child in Children)
    {
        double x = (double) Canvas.GetLeft(child);
        double y = (double) Canvas.GetTop(child);
        child.Arrange(new Rect(new Point(x, y), child.DesiredSize));
    }
    return base.ArrangeOverride(finalSize); 
    // real Canvas has more sophisticated sizing
}

备注

有关面板工作方式的详细信息, 请参阅XAML 自定义面板概述For more info on how panels work, see XAML custom panels overview.