自定义附加属性

附加属性是一个 XAML 概念。 附加属性通常定义为依赖属性的专用形式。 本主题介绍如何将一个附加属性实现为依赖属性,以及如何定义让附加属性可用于 XAML 所必需的访问器约定。

先决条件

我们假设你从现有依赖属性用户的角度理解依赖属性,并且已阅读依赖属性概述。 还应阅读附加属性概述。 要理解本主题中的示例,你还应该理解 XAML,知道如何编写使用 C++、C# 或 Visual Basic 的基本 Windows 运行时应用。

附加属性的应用场景

当需要有一个可用于定义类之外的其他类的属性设置机制时,建议创建附加属性。 最常见的应用场景是布局和服务支持。 现有布局属性的示例包括 Canvas.ZIndexCanvas.Top 在布局应用场景中,作为布局控制元素的子元素存在的元素能够分别向其父级元素表达布局要求,其中每个元素都设置一个被父级定义为附加属性的属性值。 Windows 运行时 API 中的服务支持应用场景是 ScrollViewer 的一组附加属性,例如 ScrollViewer.IsZoomChainingEnabled

警告

Windows 运行时 XAML 实现的一个现有限制是,你无法为自定义附加属性创建动画。

注册自定义附加属性

如果将附加属性严格定义为用于其他类型,则注册该属性所在的类不必从 DependencyObject 派生。 但是,如果需要让访问器的目标参数使用 DependencyObject (如果遵循将附加属性设置为依赖属性的典型模型),以便可以使用后备属性存储。

通过声明类型 DependencyProperty公共静态只读属性,将附加属性定义为依赖属性。 通过使用 RegisterAttached 方法的返回值来定义此属性。 属性名称必须与指定为 RegisterAttachedname 属性的附加属性名称相匹配,末尾处同样添加字符串“Property”。 这是用于命名依赖属性的标识符相对于它们所表示的属性的既定约定。

定义自定义附加属性的主要区域与自定义依赖属性的区别在于定义访问器或包装器的方式。 除了使用自定义依赖属性中所述的包装器技巧之外,还必须提供静态 GetPropertyNameSetPropertyName 方法作为附加属性的访问器。 访问器主要由 XAML 分析器使用,尽管任何其他调用方也可以使用它们在非 XAML 应用场景中设置值。

重要

如果未正确定义访问器,XAML 处理器将无法访问你的附加属性,尝试使用它的任何人都可能会得到一个 XAML 分析器错误。 另外,在引用的程序集中遇到自定义依赖属性时,设计和编码工具常常依赖于“*Property”约定来命名标识符。

访问器

获取PropertyName”访问器的签名必须是这个。

public staticvalueTypeGetPropertyName(DependencyObject target)

Microsoft Visual Basic 的签名是这个。

Public Shared Function GetPropertyName(ByVal target As DependencyObject) As valueType)

目标对象可以是实现中更具体的类型,但必须从 DependencyObject 派生。 valueType 返回值在实现中也可以指定为更具体的类型。 基本对象类型是可接受的,但附加属性通常要强制执行类型安全。 建议在 getter 和 setter 签名中使用键入。

设置PropertyName”访问器的签名必须是这个。

public static void SetPropertyName(DependencyObject target ,valueType value)

Visual Basic 的名称是这个。

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

目标对象可以是实现中更具体的类型,但必须从 DependencyObject 派生。 value 对象及其 valueType 可以是实现中更具体的类型。 请记住,此方法的值是 XAML 加载器在标记中的附加属性用法中遇到附加属性时的输入。 必须存在可用于你所使用的类型的类型转换或现有标记扩展支持,以便可以从属性值(最终仅仅是一个字符串)创建相应的类型。 基本对象类型是可接受的,但通常需要进一步的类型安全性。 为此,请将类型强制置于访问器中。

注意

还可以定义附加属性,它旨在通过属性元素语法使用。 这种情况下不需要对值进行类型转换,但需确保要使用的值能够以 XAML 的格式进行构建。 VisualStateManager.VisualStateGroups 是一个仅支持属性元素用法的现有附加属性的示例。

代码示例

此示例演示依赖属性注册(使用 RegisterAttached 方法),以及自定义附加属性的获取设置访问器。 在此示例中,附加属性名称为 IsMovable。 因此,访问器必须名为 GetIsMovableSetIsMovable。 附加属性的所有者是名为 GameService 没有自己的 UI 的一项服务,其唯一的作用就是在使用 GameService.IsMovable 附加属性时提供附加属性服务。

在 C++/CX 中定义附加属性要更为复杂。 必须决定如何在标头和代码文件之间如何考虑。 此外,出于自定义依赖属性中所述的原因,应该将标识符暴露为仅具有获取访问器的属性。 在 C++/CX 中,必须显式定义这种属性-字段关系,而不是依赖于 .NET readonly 关键字和简单属性的隐式支持。 此外,还需要在仅运行一次的帮助程序函数中执行附加属性的注册,当应用首次启动时,但在加载需要附加属性的任何 XAML 页面之前。 通常是从 app.xaml 文件的代码中的 App / Application 构造函数中为所有依赖或附加属性调用属性注册帮助程序函数。

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 标记设置自定义附加属性

定义附加属性并将其支持成员包含在自定义类型的一部分后,必须使定义可用于 XAML 用法。 为此,必须映射一个 XAML 命名空间,该命名空间将引用包含相关类的代码命名空间。 如果将附加属性定义为库的一部分,则必须将该库作为应用的应用包的一部分包含。

XAML 的 XML 命名空间映射通常放置在 XAML 页面的根元素中。 例如,对于命名空间中UserAndCustomControls命名GameService的类,其中包含前面代码片段中显示的附加属性定义,映射可能如下所示。

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

使用映射,可以在与目标定义匹配的任何元素上设置 GameService.IsMovable 附加属性,包括 Windows 运行时定义的现有类型。

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

如果要对同样位于同一映射 XML 命名空间中的元素设置属性,则仍必须在附加属性名称中包含前缀。 这是因为前缀限定所有者类型。 不能假定附加属性的属性与包含属性的元素位于同一 XML 命名空间中,即使通过普通 XML 规则,属性也可以从元素继承命名空间。 例如,如果在 ImageWithLabelControl(未显示定义)的自定义类型上设置 GameService.IsMovable,即使两个都在映射到同一前缀的相同代码命名空间中定义的,XAML 仍然会是这个。

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

注意

如果使用 C++/CX 编写 XAML UI,每当一个 XAML 页面使用一种定义附加属性的自定义类型,就必须包含该类型的标头文件。 每个 XAML 页面都有一个与之关联的代码隐藏标头 (.xaml.h)。 你应该在这里包含(使用 #include)附加属性的所有者类型定义的标头文件。

以强制形式设置自定义附加属性

还可以访问强制代码中的自定义附加属性。 下面的代码演示了如何操作。

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

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

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

自定义附加属性的值类型

用作自定义附加属性的值类型的类型会影响用法、定义或同时使用和定义。 附加属性的值类型在多个位置声明:在获取设置访问器方法中,也作为 RegisterAttached 调用的 propertyType 参数。

附加属性(自定义或其他)的最常见值类型是一个简单的字符串。 这是因为附加属性一般供 XAML 属性,并且将字符串用作值类型可让属性保持轻量化。 对字符串方法进行本机转换的其他基元(例如整数、双精度值或枚举值)也常见为附加属性的值类型。 可以使用其他值类型(不支持本机字符串转换)作为附加属性值。 但是,这需要选择用法或实现:

  • 可以将附加属性保留原样,但附加属性仅支持使用附加属性是属性元素,并且值声明为对象元素。 这种情况下,属性类型必须支持 XAML 用法作为对象元素。 对于现有 Windows 运行时引用类,检查 XAML 语法以确保该类型支持 XAML 对象元素用法。
  • 可以将附加属性保留原样,但只能通过 XAML 引用技术(如绑定StaticResource)在属性使用中使用它,该技术可以表示为字符串。

Canvas.Left 示例的详细信息

在前面的附加属性用法示例中,我们介绍了设置 Canvas.Left 附加属性的不同方法。 但是,画布如何与对象交互以及何时进行交互呢? 我们将进一步介绍此特定示例,因为如果你实现附加属性,则会看到有趣的现象,那就是如果典型附加属性所有者类在其他对象上发现它们的话,会对其附加属性值采取哪些其他操作。

画布的主要功能是 UI 中的绝对定位布局容器。 画布的子项存储在基类定义的属性“子项”中。 在所有面板中,只有画布使用绝对定位。 不能膨胀普通 UIElement 类型的对象模型来添加可能只有画布会要考虑的属性,尤其是它们是 UIElement 子元素的 UIElement 场景下。 将画布的布局控制属性定义为任何 UIElement 都可以使用的附加属性可以让对象模型更纯净。

为了成为一个实用的面板,画布会出现替代框架等级度量安排方法的行为。 这是画布在其子级上实际检查附加属性值的位置。 度量安排模式中都包含会对任何内容进行循环访问的循环,以及具有子项属性的面板,会明确定义哪些算作面板的子项。 因此,画布布局行为会循环访问这些子项,并对每个子项进行静态的 Canvas.GetLeftCanvas.GetTop 调用来查看这些附加属性是否包含非默认值(默认为 0)。 然后,根据每个子项提供的具体值来对画布可用布局空间中的每个子项进行绝对定位,再使用安排方法提交。

此代码看上去与下面的伪代码相似。

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 自定义面板概述