自定义附加属性

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

先决条件

假设你从现有依赖属性使用者的角度了解依赖属性,并且你已阅读 依赖项属性概述。 还应阅读 “附加属性”概述。 若要遵循本主题中的示例,还应了解 XAML 以及如何使用 C++、C# 或 Visual Basic 编写基本的 Windows 运行时应用。

附加属性的应用场景

当有理由为除定义类以外的类提供属性设置机制时,可以创建附加属性。 最常见的方案是布局和服务支持。 现有布局属性的示例包括 Canvas.ZIndexCanvas.Top。 在布局方案中,作为子元素存在的元素可以单独向布局控制元素表达布局要求,每个元素设置父元素定义为附加属性的属性值。 Windows 运行时 API 中服务支持方案的示例是 ScrollViewer 的附加属性集,例如 ScrollViewer.IsZoomChainingEnabled

警告

Windows 运行时 XAML 实现的现有限制是无法对自定义附加属性进行动画处理。

注册自定义附加属性

如果要严格定义附加属性以用于其他类型的类型,则注册该属性的类不必派生自 DependencyObject。 不过,如果您按照典型模型将附加属性也设为依赖属性,那么您确实需要让访问器的目标参数使用 DependencyObject,这样才能使用后备属性存储。

通过声明一个 publicstaticreadonly 类型的 DependencyProperty 属性,将附加属性定义为依赖属性。 使用 RegisterAttached 方法的返回值定义此属性。 属性名称必须与指定为 RegisterAttached名称 参数的附加属性名称匹配,并将字符串“Property”添加到末尾。 这是用于命名依赖属性的标识符相对于它们所表示的属性的既定约定。

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

重要

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

Accessors

GetPropertyName 访问器的签名必须是此签名。

public static 值类型获取属性名(DependencyObject target)

对于 Microsoft Visual Basic,这是这样。

Public Shared Function Get PropertyName(ByVal target As DependencyObject) As valueType)

目标对象可以是实现中更具体的类型,但必须派生自 DependencyObjectValueType 返回值也可以是实现中更具体的类型。 基本 对象 类型是可接受的,但通常需要附加属性强制实施类型安全性。 建议在 getter 和 setter 方法签名中使用类型安全性技术。

SetPropertyName 访问器的签名必须是如此。

public static void Set PropertyName(DependencyObject target ,valueType value)

对于 Visual Basic,这是这样。

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

目标对象可以是实现中更具体的类型,但必须派生自 DependencyObjectvalue 对象及其 valueType 可以在您的实现中是更具体的类型。 请记住,此方法的值是当 XAML 处理器在标记中遇到您的附加属性时传入的输入。 为了确保能够从属性值(最终只是字符串)创建合适的类型,你所使用的类型必须支持类型转换或现有的标记扩展。 基本 对象 类型是可接受的,但通常需要进一步的类型安全性。 为此,请在访问器中实施类型强制。

注释

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

代码示例

此示例演示如何为自定义附加属性注册依赖属性(使用 RegisterAttached 方法),以及定义 GetSet 访问器。 在此示例中,附加的属性名称为 IsMovable. 因此,访问器必须命名 GetIsMovableSetIsMovable。 附加属性的所有者是一个名为没有其 UI 的服务类 GameService ;其用途是仅在使用 GameService.IsMovable 附加属性时提供附加属性服务。

在 C++/CX 中定义附加属性更为复杂。 必须决定如何在头文件和代码文件之间划分。 此外,出于在自定义依赖属性中讨论的原因,您应将标识符公开为仅具有get访问器的属性。 在 C++/CX 中,必须显式地定义属性与字段的关系,而不是依赖于简单属性的 .NET 只读关键字和隐式存储。 此外,还需要在仅运行一次的帮助程序函数中执行附加属性的注册,当应用首次启动时,但在加载需要附加属性的任何 XAML 页面之前。 调用任何依赖项或附加属性的属性注册帮助程序函数的典型位置来自 app.xaml 文件代码中的 应用 / 应用程序 构造函数。

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 页面的根元素中。 例如,对于名为GameService且位于UserAndCustomControls命名空间的类,其中包含前面代码片段中显示的附加属性定义,映射可能如下所示。

<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 规则,属性也可以从元素继承命名空间。 例如,如果要对自定义类型GameService.IsMovable(未显示定义)进行设置ImageWithLabelControl,并且即使两者在映射到同一前缀的相同代码命名空间中定义,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);
}
...

自定义附加属性的值类型

用作自定义附加属性的值类型的类型会影响用法、定义或同时使用和定义。 附加属性的值类型在多个地方声明:在 GetSet 访问器方法的签名中,以及在 RegisterAttached 调用中作为 propertyType 参数。

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

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

有关 Canvas.Left 示例的详细信息

在前面的附加属性用法示例中,我们显示了设置 Canvas.Left 附加属性的不同方法。 但是,这对 Canvas 与你的对象的交互方式有何改变?以及,这种改变何时发生? 我们将进一步研究此特定示例,因为当你实现附加属性时,观察典型的附加属性所有者类在发现这些附加属性值存在于其他对象上时会如何处理它们是很有趣的。

Canvas 的主要功能是 UI 中绝对定位的布局容器。 Canvas 的子元素存储在基类中定义的属性 Children 中。 在所有面板中, Canvas 是唯一使用绝对定位的面板。 这样做会给常见的UIElement类型的对象模型带来负担,因为增加的属性可能只对Canvas和特定作为UIElement子元素的UIElement实例有用。 定义 Canvas 的布局控件属性,使之成为任何 UIElement 可以使用的附加属性,使对象模型更加清晰。

为了成为一个实际面板, Canvas 具有替代框架级 MeasureArrange 方法的行为。 这是 Canvas 在其子级上实际检查附加属性值的处。 MeasureArrange模式的一部分是遍历任意内容的循环,而面板具有子级属性,明确指出哪些内容应视为面板的子元素。 因此 Canvas 布局行为循环访问这些子级,并对每个子级进行静态调用 Canvas.GetLeftCanvas.GetTop,以查看这些附加属性是否包含非默认值(默认值为 0)。 然后,这些值用于依据每个子项提供的特定值,在画布的可用布局空间中绝对定位每个子项,并使用Arrange进行提交。

代码类似于此伪代码。

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