揭秘依赖属性

17

我在 Stack Overflow 和其他网站上读了很多关于依赖属性的文章,但是仍然感到困惑,没有找到一个好的解释。我同时使用 Silverlight (SL) 和 Windows Presentation Foundation (WPF),它们的实现方式是否不同?我们真正需要它们吗?它们是否静态的,也就是说它们的值是共享的?为什么微软要引入依赖属性呢?

7个回答

20
答案就在名称中,尽管在软件开发中,“依赖关系”这个词带有很多含义,但这并不特别清楚它的意思。
依赖属性是一个对象上的属性,其值取决于其他对象。例如:
- TextBox 的 FontFamily 属性的值(通常)取决于其容器的 FontFamily 属性。如果更改容器上的该属性,则 TextBox 上的值也会更改。 - TextBox 的 Text 属性的值可能会依赖于绑定数据源。当绑定属性的值更改时,Text 属性的值也会更改。 - Label 的 Opacity 属性的值可能会依赖于动画故事板,在常见的情况下,您已设置 UI 元素以响应某些事件淡入或淡出。 - 任何 UI 元素的各种属性的值都可能取决于您应用的样式。
依赖的核心概念是,依赖的东西应该从其所依赖的东西获取属性值。这就是为什么依赖属性被实现为调用方法的 CLR 属性 getter 的原因。
当 TextBox 或呈现它的东西需要知道其 FontFamily 是什么时,FontFamily getter 调用 GetValue 方法。该方法从容器、动画、绑定或其他位置查找值。
该方法有很多复杂性。例如,如果该值是继承的,则它的工作方式与 WPF 在资源字典中查找样式相当类似:它在本地字典中查找该值,如果没有条目,则在其父级字典中查找,依此类推,直到找到一个值或到达层次结构顶部为止,在这种情况下,它将使用默认值。
如果您查看依赖属性的实现,就会发现这一点。依赖对象具有可能包含给定属性条目的字典。GetValue 方法从该字典获取值(这就是具有依赖属性的对象可以具有覆盖其继承自的值的本地值的方式),如果未找到该值,则使用有关属性的元信息来确定应查找的位置。
由于该元信息对于类中的每个对象都是相同的(即 TextBox.Text 对于每个 TextBox 的行为都相同),因此存储它的字典是该类的静态属性。
因此,当您看到如下代码时:
static Button()
{
   // Register the property
   Button.IsDefaultProperty = 
     DependencyProperty.Register("IsDefault",
     typeof(bool), typeof(Button),
     new FrameworkPropertyMetadata(false,
        new PropertyChangedCallback(OnIsDefaultChanged)));
}

发生的情况是在该字典中添加了定义所有Button对象上的IsDefault属性的元信息。当你看到这个:

public bool IsDefault
{
  get { return (bool)GetValue(Button.IsDefaultProperty); }
  set { SetValue(Button.IsDefaultProperty, value); }
}
您所看到的是获取器方法,它基于元信息查找属性值(从本地字典、父对象或其他地方)。您可能还记得我之前提到过,获取器查找属性值的第一个位置是对象的本地字典。设置器中的 "SetValue" 方法是将该条目添加到字典中(仅在需要覆盖依赖项并显式设置属性时才会调用,即说“我想让此文本框显示 Consolas 字体,而与窗口中的其他控件使用的字体无关。”)。
这种表面上复杂的系统给我们带来了极大的好处,即对象上的依赖属性仅在设置时占用内存。如果我创建了 10,000 个 TextBox 控件并将它们添加到 Window 中,则其中没有一个实际包含对 FontFamily 对象的引用。这意味着我不必为其分配内存,垃圾回收器也不必检查这些 10,000 个对象引用。实际上,如果一个TextBox具有 100 个依赖属性(实际上差不多就是这样),则每当您创建一个 TextBox 时,您都不必为其分配 100 个后备字段的内存。依赖属性仅在您明确设置它们时才会占用内存。由于 UI 对象上的绝大多数属性都不会被明确设置,因此这些节约是非常可观的。

11

其他人已经对依赖属性进行了解释。我将尝试提供一些关于它们设计的背景信息。(这有助于我理解为什么首先存在这些奇怪的称为依赖属性的创建物。) WPF 设计者添加依赖属性而不是使用普通属性有几个原因...

  1. 能够为当前元素不适用但适用于其子元素的属性添加值。例如,在容器上设置字体,让它向下级文本元素级联。
  2. 未使用属性的大小成本。以上面的字体示例为例...在设计良好的页面上,只有很少几个元素会修改其字体 - 可能是一些顶层容器。如果我们将 Font 存储为普通的 C# 属性,则每个对象都需要指向字体对象的 32/64 位指针,其中大部分为 null 或默认值。将 XAML 属性的数量乘以页面中元素的典型数量,就会需要存储大量的空白或默认值。与此相比,依赖属性仅在设置时占用空间。(我没有查看内部实现,但我想 GetValue(propName) 和 SetValue(propName) 将值存储在某种按对象/属性分组的哈希表中。)

如果微软像迭代器一样使用编译器魔法来隐藏重复的代码,为创建新依赖属性提供更好的语法将会很好。也许在 C# 6 中... :)


+1 是希望编译器的魔力可以解决丑陋的实现。 - Ken Smith
+1。第二项才是DP的真正杀手锏,正因为这个原因,DP被实现成它们现在的模样。 - AnthonyWJones
@James Kovacs:您能否进一步解释您所说的观点 - “与依赖属性相比,只有设置了值才会占用空间。” 另外,您能否指向一些讨论第二点的链接? - Sandbox
1
@Ken:我不会说永远不会,但我不会抱太大希望。DependencyObject在System.Windows命名空间中,这使得它远离编译器喜欢处理类型的范畴。我想不出任何一种编译器知道但不在System命名空间中的类型。 - AnthonyWJones
1
@Sandbox - 通过依赖属性,只有在实际设置它时,它才会占用值哈希表中的空间。没有一个空值的内存位置。这样是否更有意义?至于参考资料,我没有具体的东西。这是多年前Don Box和Chris Anderson的一次未记录的讲话中获得的信息。 - James Kovacs
@James Kovacs 现在更有意义了。谢谢你。 - Sandbox

4
  1. 它们在SL和WPF中是否有区别?
    没有

  2. 我们真正需要它们的原因是什么?
    简短的答案是,我们需要它们来进行数据绑定和样式设置,这也是微软添加它们的原因。

  3. 它们是静态的,这意味着它们的值是共享的吗?
    静态部分是属性的名称,因此可以使用GetValue(propName)检索它的值,并使用SetValue(propName, value)进行设置。


1
我们需要它们来进行数据绑定和样式设置。为什么我们不能只使用带有一些通知机制的clr属性呢?我没有理解你的第三点。 - Sandbox
2
它们也用于动画。DP的目的基本上是让第三方在一段时间内接管您的属性,然后在没有太多开销的情况下将其归还为原样。样式将注入属性值,动画也将如此。特别是动画需要更改属性的值,然后在动画完成后将其恢复为原始值。这就是DP所能实现的。 - Matt Greer
2
但是为什么普通的CLR属性不能胜任呢?我并不是说它能够,我只是像OP一样还没有理解。 - Ken Smith
@沙盒 你是正确的,你可以为属性创建一个通知系统,这样它们的值就会在绑定中得到更新。事实上,微软为此创建了一个接口,即INotifyPropertyChanged。你也可以绑定到一个普通属性,但如果没有INotifyPropertyChange,那么它只会执行一次,当值改变时绑定将不会再次更新。 - Brian

3
请查看MSDN上的Dependency Properties Overview(或者Silverlight版本)。
(Silverlight/WPF)提供了一组服务,可用于扩展CLR属性的功能。总体而言,这些服务通常被称为(Silverlight/WPF)属性系统。由(Silverlight/WPF)属性系统支持的属性称为依赖属性。
有关Silverlight和WPF属性系统之间的差异,请参见此处
将属性与Silverlight/WPF类型系统连接起来的依赖属性系统重要性的几个原因如下(有关更详细的解释,请参见此处):
  • 允许属性在样式中进行设置
  • 允许属性支持数据绑定
  • 允许属性进行动画化
  • 允许自定义控件的属性在Visual Studio中接收设计师支持

请注意,依赖属性通常由CLR属性支持,这意味着您可以像使用CLR属性一样在代码中与其交互。


我已经看了msdn的链接,但它远远不能揭示这个主题的奥秘。因此,提出这个问题。 - Sandbox

3

WPF是围绕属性架构设计的,需要一个非常强大的属性系统来支持:

  1. 各种提供程序提供其值,例如触发器、样式、动画、主题等可以在运行时更改其值(依赖属性值优先级)。
  2. XAML(表达式支持)。
  3. 内存高效(它们的静态性质)。
  4. 更改通知。
  5. 属性值继承(附加属性)。

我发现以下文章非常有用 -

WPF中的依赖属性概述: http://joshsmithonwpf.wordpress.com/2007/06/22/overview-of-dependency-properties-in-wpf/

依赖属性内部: http://www.i-programmer.info/programming/wpf-workings/443-inside-dependency-properties-.html

WPF:依赖属性的静态性质: http://dotnetslackers.com/Debugger/re-126399_WPF_The_Static_Nature_of_Dependency_Properties.aspx


2
你提出的大多数问题已经被其他人回答过了,但我会给你我的简单看法。
首先,WPF依赖属性比Silverlight依赖属性具有更多的功能。但是,在排除WPF中这些附加功能的情况下,它们基本上是相同的。
关键问题是“何时”应该使用它们(“为什么”已经被回答过了)。当您需要在XAML中为它们分配标记扩展(例如绑定表达式)时,您实际上需要它们,然后在运行时解析/评估它们。在大多数情况下,只有在编写公开属性的自定义控件时才需要使用它们。
即您将自定义控件放置在XAML中的一个表面上,并将绑定表达式分配给其中一个属性。
我看到一些人到处都在使用它们,认为在进行Silverlight/WPF开发时必须使用它们,但事实并非如此。不同的人有不同的底线,但我建议只在必要时使用它们,这几乎100%的时间都是在自定义控件中使用。
明目张胆的自我推销:我在我的书的第10章《Silverlight 4中的专业商务应用》中,就这个话题进行了更深入的(针对Silverlight的)讨论。

1

为了补充其他答案已经涵盖的内容:

在概念上,依赖属性的核心是一种系统,允许查询根据不同来源的列表获取值,而不是返回单个存储的值。这样做有很多影响,正如其他答案中所列举的那样,但实际上发生的情况是,在获取值时表现出以下行为(当然是极其简化的):

private string _coerced;
private string _animated;
private string _local;
private string _triggered;
private string _styled;
private string _inherited;
private string _default;

public string MyDP
{
    get
    {
        return _coerced ?? _animated ?? _local ?? _triggered ?? _styled ?? _inherited ?? _default;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接