附加属性概述 (WPF .NET)

附加属性是一个 Extensible Application Markup Language (XAML) 概念。 附加属性允许为派生自 DependencyObject 的任何 XAML 元素设置额外的属性/值对,即使该元素未在其对象模型中定义这些额外的属性。 额外的属性可进行全局访问。 附加属性通常定义为没有常规属性包装器的依赖属性的专用形式。

重要

面向 .NET 7 和 .NET 6 的桌面指南文档正在撰写中。

先决条件

本文假定你对依赖属性有基本的了解,并且已阅读依赖属性概述。 若要理解本文中的示例,还应当熟悉 XAML 并知道如何编写 Windows Presentation Foundation (WPF) 应用程序。

使用附加属性的原因

附加属性允许子元素为父元素中定义的属性指定唯一值。 一个常见方案是,一个子元素指定它应如何被其父元素呈现在 UI 中。 例如,DockPanel.Dock 是一个附加属性,因为它在 DockPanel 的子元素上设置,而不是在 DockPanel 本身设置。 DockPanel 类定义名为 DockProperty 的静态 DependencyProperty 字段,然后提供 GetDockSetDock 方法作为附加属性的公共访问器。

XAML 中的附加属性

在 XAML 中,可以使用语法 <attached property provider type>.<property name> 设置附加属性,其中附加属性提供程序是定义附加属性的类。 以下示例演示 DockPanel 的子元素如何设置 DockPanel.Dock 属性值。

<DockPanel>
    <TextBox DockPanel.Dock="Top">Enter text</TextBox>
</DockPanel>

其用法类似于静态属性,因为你引用的是拥有和注册附加属性的类型(例如 DockPanel),而不是实例名称。

使用 XAML 特性指定附加属性时,只适合执行 set 操作。 尽管存在一些用于比较值的间接机制(如样式中的触发器),但无法通过 XAML 直接获取属性值。

WPF 中的附加属性

附加属性是 XAML 概念,依赖属性是 WPF 概念。 在 WPF 中,WPF 类型上大多数与 UI 相关的附加属性都作为依赖属性实现。 作为依赖属性实现的 WPF 附加属性支持依赖属性概念,例如包含元数据中的默认值的属性元数据。

附加属性用法模型

尽管任何对象都可以设置附加属性值,但这并不意味着设置值会生成有形的结果,或者该值将被另一个对象使用。 附加属性的主要用途是为来自各种类层次结构和逻辑关系的对象提供一种方法,将公共信息报告给定义附加属性的类型。 附加属性用法通常遵循以下模型之一:

  • 定义附加属性的类型是为附加属性设置值的元素的父级。 父类型通过作用于对象树结构的内部逻辑循环访问其子对象,获取值,并以某种方式作用于这些值。
  • 定义附加属性的类型将用作各种可能的父元素和内容模型的子元素。
  • 定义附加属性的类型表示一项服务。 其他类型为该附加属性设置值。 然后,当在服务的上下文中计算设置该属性的元素时,将通过服务类的内部逻辑获取附加属性的值。

父级定义的附加属性示例

WPF 定义附加属性的典型场景如下:父元素支持子元素集合,并且父元素根据其每个子元素报告的数据实现行为。

DockPanel 定义 DockPanel.Dock 附加属性。 DockPanel 具有类级代码,特别是 MeasureOverrideArrangeOverride,这是其呈现逻辑的一部分。 DockPanel 实例检查其任何直接子元素是否为 DockPanel.Dock 设置了值。 如果已设置,这些值将变为应用于每个子元素的呈现逻辑的输入。 尽管理论上附加属性可能会影响直接父元素之外的元素,但嵌套 DockPanel 实例的定义行为是仅与其直接子元素集合交互。 因此,如果为没有 DockPanel 父级的元素设置 DockPanel.Dock,则不会引发错误或异常,并且会创建一个不由任何 DockPanel 使用的全局属性值。

代码中的附加属性

WPF 中的附加属性没有典型的 CLR getset 包装器方法,因为这些属性可能从 CLR 命名空间外部设置。 若要允许 XAML 处理器在分析 XAML 时设置这些值,定义附加属性的类必须实现 Get<property name>Set<property name> 形式的专用访问器方法。

你还可以使用专用访问器方法在代码中获取和设置附加属性,如以下示例所示。 在此示例中,myTextBoxTextBox 类的实例。

DockPanel myDockPanel = new();
TextBox myTextBox = new();
myTextBox.Text = "Enter text";

// Add child element to the DockPanel.
myDockPanel.Children.Add(myTextBox);

// Set the attached property value.
DockPanel.SetDock(myTextBox, Dock.Top);
Dim myDockPanel As DockPanel = New DockPanel()
Dim myTextBox As TextBox = New TextBox()
myTextBox.Text = "Enter text"

' Add child element to the DockPanel.
myDockPanel.Children.Add(myTextBox)

' Set the attached property value.
DockPanel.SetDock(myTextBox, Dock.Top)

如果不将 myTextBox 添加为 myDockPanel 的子元素,则调用 SetDock 不会引发异常或产生任何影响。 只有对 DockPanel 的子元素设置的 DockPanel.Dock 值才会影响呈现效果,并且无论在将子元素添加到 DockPanel 之前还是之后设置值,呈现效果都相同。

从代码的角度来看,附加属性类似于具有方法访问器而不是属性访问器的支持字段,可以在任何对象上设置,并且无需先在这些对象上定义。

附加属性元数据

附加属性的元数据通常与依赖属性的元数据没有什么不同。 注册附加属性时,可使用 FrameworkPropertyMetadata 指定属性的特征,例如属性是否影响呈现或测量。 通过替代附加属性元数据指定默认值时,该值将成为替代类实例上显式附加属性的默认值。 如果未另外设置附加属性值,则在使用 Get<property name> 访问器和指定元数据的类实例查询属性时,将报告默认值。

若要对属性启用属性值继承,应使用附加属性,而不是非附加依赖属性。 有关详细信息,请参阅属性值继承

自定义附加属性

何时创建附加属性

在以下情况下创建附加属性很有用:

  • 需要一种可用于类(定义类除外)的属性设置机制。 一个常见方案是用于 UI 布局,例如,DockPanel.DockPanel.ZIndexCanvas.Top 都是现有布局属性的示例。 在布局方案中,布局控制元素的子元素能够向其布局父级表达布局要求,并为父级定义的附加属性设置值。

  • 你的某个类表示服务,你希望其他类更透明地集成该服务。

  • 需要 Visual Studio WPF 设计器支持,例如能够通过“属性”窗口编辑属性。 有关详细信息,请参阅控件创作概述

  • 你想使用属性值继承。

如何创建附加属性

如果类定义了一个仅供其他类型使用的附加属性,那么该类不需要从 DependencyObject 派生。 否则,应遵循 WPF 模型,通过从 DependencyObject 派生类,将附加属性也设置成依赖属性。

通过声明 DependencyProperty 类型的 public static readonly 字段,将附加属性定义为定义类中的依赖项。 然后,将 RegisterAttached 方法的返回值分配给该字段,该字段也称为依赖属性标识符。 遵循 WPF 属性命名约定,通过命名标识符字段 <property name>Property 将字段与它们所表示的属性区分开来。 此外,提供静态 Get<property name>Set<property name> 访问器方法,让属性系统访问附加属性。

以下示例演示如何使用 RegisterAttached 方法注册依赖属性,以及如何定义访问器方法。 在此示例中,附加属性的名称为 HasFish,因此标识符字段命名为 HasFishProperty,访问器方法命名为 GetHasFishSetHasFish

public class Aquarium : UIElement
{
    // Register an attached dependency property with the specified
    // property name, property type, owner type, and property metadata.
    public static readonly DependencyProperty HasFishProperty = 
        DependencyProperty.RegisterAttached(
      "HasFish",
      typeof(bool),
      typeof(Aquarium),
      new FrameworkPropertyMetadata(defaultValue: false,
          flags: FrameworkPropertyMetadataOptions.AffectsRender)
    );

    // Declare a get accessor method.
    public static bool GetHasFish(UIElement target) =>
        (bool)target.GetValue(HasFishProperty);

    // Declare a set accessor method.
    public static void SetHasFish(UIElement target, bool value) =>
        target.SetValue(HasFishProperty, value);
}
Public Class Aquarium
    Inherits UIElement

    ' Register an attached dependency property with the specified
    ' property name, property type, owner type, and property metadata.
    Public Shared ReadOnly HasFishProperty As DependencyProperty =
        DependencyProperty.RegisterAttached("HasFish", GetType(Boolean), GetType(Aquarium),
            New FrameworkPropertyMetadata(defaultValue:=False,
                flags:=FrameworkPropertyMetadataOptions.AffectsRender))

    ' Declare a get accessor method.
    Public Shared Function GetHasFish(target As UIElement) As Boolean
        Return target.GetValue(HasFishProperty)
    End Function

    ' Declare a set accessor method.
    Public Shared Sub SetHasFish(target As UIElement, value As Boolean)
        target.SetValue(HasFishProperty, value)
    End Sub

End Class

Get 访问器

get 访问器方法签名为 public static object Get<property name>(DependencyObject target),其中:

  • target 是从中读取附加属性的 DependencyObjecttarget 类型可以比 DependencyObject 更具体。 例如,DockPanel.GetDock 访问器方法将 UIElement 类型化为 target,因为附加属性要在 UIElement 实例上设置。 UiElement 间接派生自 DependencyObject
  • 返回类型可以比 object 更具体。 例如,GetDock 方法将返回值类型化为 Dock,因为返回值应为 Dock 枚举。

注意

附加属性的 get 访问器是设计工具(如 Visual Studio 或 Blend for Visual Studio)中的数据绑定支持所必需的。

Set 访问器

set 访问器方法签名为 public static void Set<property name>(DependencyObject target, object value),其中:

  • target 是写入附加属性的 DependencyObjecttarget 类型可以比 DependencyObject 更具体。 例如,SetDock 方法将 UIElement 类型化为 target,因为附加属性要在 UIElement 实例上设置。 UiElement 间接派生自 DependencyObject
  • value 类型可以比 object 更具体。 例如,SetDock 方法需要一个 Dock 值。 XAML 加载程序必须能够根据表示附加属性值的标记字符串生成 value 类型。 因此,你使用的类型必须有类型转换、值序列化程序或标记扩展支持。

附加属性特性

WPF 定义了几个 .NET 特性,这些特性向反射进程以及反射和属性信息的使用者(例如设计器)提供有关附加属性的信息。 设计器使用 WPF 定义的 .NET 特性来限制属性窗口中显示的属性,以避免用户被包含所有附加属性的全局列表淹没。 你可以考虑对自己的自定义附加属性应用这些特性。 以下参考页面介绍了 .NET 特性的用途和语法:

了解详细信息

  • 有关如何创建附加属性的详细信息,请参阅注册附加属性
  • 有关依赖属性和附加属性的更多高级使用方案,请参阅自定义依赖属性
  • 你可以将一个属性同时注册为附加属性和依赖属性,并包含常规属性包装器。 这样一来,就可以使用属性包装器对某个元素设置属性,同时使用 XAML 附加属性语法对任何其他元素设置属性。 有关示例,请参见 FrameworkElement.FlowDirection

另请参阅