WPF 中的(自定义)依赖属性和附加属性有什么区别?各有什么用途?实现通常有何不同?
5 回答
附加属性是一种依赖属性。不同之处在于它们的使用方式。
使用附加属性,该属性是在一个与其使用的类不同的类上定义的。这通常用于布局。很好的例子是 Panel.ZIndex 或 Grid.Row - 您将其应用于控件(即:Button),但它实际上是在 Panel 或 Grid 中定义的。该属性“附加”到按钮的实例。
例如,这允许容器创建可用于任何 UI 元素的属性。
至于实现差异 - 基本上只是在定义属性时使用 Register 与 RegisterAttached 的问题。
抽象的
由于我几乎没有找到关于此事的文档,因此需要对源代码进行一些研究,但这里有一个答案。
将依赖属性注册为常规属性和附加属性之间存在区别,而不是“哲学”属性(常规属性旨在由声明类型及其派生类型使用,附加属性旨在用作任意 DependencyObject
实例的扩展)。“哲学”,因为正如@MarqueIV 在他对@ReedCopsey 答案的评论中注意到的那样,常规属性也可以用于任意DependencyObject
实例。
此外,我不得不不同意其他说附加属性是“依赖属性类型”的答案,因为它具有误导性——没有任何依赖属性的“类型”。该框架并不关心该属性是否已注册为附加 - 甚至无法确定(从某种意义上说,该信息没有被记录,因为它是不相关的)。事实上,所有属性都像附加属性一样注册,但在常规属性的情况下,会做一些额外的事情来稍微修改它们的行为。
代码摘录
为了省去你自己浏览源代码的麻烦,这里有一个简单的版本。
注册未指定元数据的属性时,调用
DependencyProperty.Register(
name: "MyProperty",
propertyType: typeof(object),
ownerType: typeof(MyClass))
产生与调用完全相同的结果
DependencyProperty.RegisterAttached(
name: "MyProperty",
propertyType: typeof(object),
ownerType: typeof(MyClass))
但是,在指定元数据时,调用
DependencyProperty.Register(
name: "MyProperty",
propertyType: typeof(object),
ownerType: typeof(MyClass),
typeMetadata: new FrameworkPropertyMetadata
{
CoerceValueCallback = CoerceCallback,
DefaultValue = "default value",
PropertyChangedCallback = ChangedCallback
});
相当于调用
var property = DependencyProperty.RegisterAttached(
name: "MyProperty",
propertyType: typeof(object),
ownerType: typeof(MyClass),
defaultMetadata: new PropertyMetadata
{
DefaultValue = "default value",
});
property.OverrideMetadata(
forType: typeof(MyClass),
typeMetadata: new FrameworkPropertyMetadata
{
CoerceValueCallback = CoerceCallback,
DefaultValue = "default value",
PropertyChangedCallback = ChangedCallback
});
结论
常规依赖属性和附加依赖属性之间的关键(也是唯一)区别是可通过DependencyProperty.DefaultMetadata属性获得的默认元数据。备注部分甚至提到了这一点:
对于非附加属性,此属性返回的元数据类型不能转换为PropertyMetadata类型的派生类型,即使该属性最初是使用派生元数据类型注册的。如果您希望原始注册的元数据包括其原始可能派生的元数据类型,请改为调用GetMetadata(Type),将原始注册类型作为参数传递。
对于附加属性,此属性返回的元数据类型将与原始RegisterAttached注册方法中给出的类型相匹配。
这在提供的代码中清晰可见。注册方法中也隐藏了一些小提示,即对于RegisterAttached
元数据参数是命名的defaultMetadata
,而对于Register
它是命名的typeMetadata
。对于附加属性,提供的元数据成为默认元数据。但是,在常规属性的情况下,默认元数据始终是PropertyMetadata
仅DefaultValue
设置的新实例(来自提供的元数据或自动)。只有后续调用OverrideMetadata
实际使用提供的元数据。
结果
主要的实际区别在于,对于常规属性,CoerceValueCallback
and仅PropertyChangedCallback
适用于从声明为所有者类型的类型派生的类型,而对于附加属性,它们适用于所有类型。例如在这种情况下:
var d = new DependencyObject();
d.SetValue(SomeClass.SomeProperty, "some value");
如果该财产被注册为附属财产,则将调用注册者,但PropertyChangedCallback
如果将其注册为常规财产,则不会调用该注册者。也一样CoerceValueCallback
。
次要差异源于OverrideMetadata
要求提供的类型派生自DependencyObject
. 实际上,这意味着常规属性的所有者类型必须派生自DependencyObject
,而附加属性 in 可以是任何类型(包括静态类、结构、枚举、委托等)。
补充
除了@MarqueIV 的建议之外,我曾多次遇到过这样的观点,即常规属性和附加属性在XAML中的使用方式不同。即,常规属性需要隐式名称语法,而不是附加属性所需的显式名称语法。这在技术上是不正确的,尽管在实践中通常是这样。为了清楚起见:
<!-- Implicit property name -->
<ns:SomeClass SomeProperty="some value" />
<!-- Explicit property name -->
<DependencyObject ns:SomeClass.SomeProperty="some value" />
在纯 XAML中,管理这些语法使用的唯一规则如下:
- 当且仅当该元素表示的类具有该名称的CLR属性时,才能对元素使用隐式名称语法
- 当且仅当全名的第一部分指定的类公开了名称与全名的第二部分匹配的适当静态get / set方法(称为访问器)时,才能在元素上使用显式名称语法
满足这些条件使您能够使用相应的语法,而不管支持依赖属性是注册为常规还是附加。
现在提到的误解是由于绝大多数教程(连同库存的Visual Studio代码片段)指示您将CLR属性用于常规依赖属性,并使用 get/set 访问器用于附加属性。但是没有什么能阻止您同时使用这两种语法,让您可以使用您喜欢的任何语法。
附加属性基本上适用于容器元素。就像如果你有一个网格并且你有 grid.row 现在这被认为是一个网格元素的附加属性。你也可以在 texbox、按钮等中使用这个属性来设置它放置在网格中。
依赖属性就像属性基本上属于其他类并在其他类中使用。例如:就像你有一个矩形,height 和 width 是矩形的常规属性,但 left 和 top 是依赖属性,因为它属于 Canvass 类。
附加属性是一种特殊的 DependencyProperties。它们允许您将值附加到对该值一无所知的对象。这个概念的一个很好的例子是布局面板。每个布局面板都需要不同的数据来对齐其子元素。Canvas 需要 Top 和 Left,DockPanel 需要 Dock 等。由于您可以编写自己的布局面板,因此列表是无限的。所以你看,不可能在所有 WPF 控件上都拥有所有这些属性。解决方案是附加属性。它们由在特定上下文中需要来自另一个控件的数据的控件定义。例如,由父布局面板对齐的元素。
我认为您可以在类本身中定义附加属性,也可以在另一个类中定义它。我们总是可以使用附加属性来扩展标准的微软控件。但是依赖属性,你在你自己的自定义控件中定义它。例如,您可以从标准控件继承您的控件,并在您自己的控件中定义一个依赖属性并使用它。这相当于定义一个附加属性,并在标准控件中使用这个附加属性。