3.2 依赖属性
WPF引入了一个新的属性类型叫作依赖属性,整个WPF平台中都会使用到它,用来实现样式化、自动数据绑定、动画等。你可能在怀疑论(skepticism)中第一次遇到这个概念,它使得.NET类型图变得很复杂,其中有简单的字段、属性、方法和事件。但是在你理解依赖属性解决的问题之后,很有可能会把它们作为一种“不错的添加剂”。
依赖属性在任何时刻都是依靠多个提供程序来判断它的值的。这些提供程序可以是一段一直在改变值的动画,或者一个父元素的属性值从上慢慢传递给子元素等。依赖属性的最大特征是其内建的传递变更通知(change notification)的能力。
添加这样的智能给属性,其动力在于能够声明标记中直接启用富功能(rich functionality)。WPF友好声明设计的关键在于它使用了很多属性。例如,Button控件有96个公共属性!属性可以方便地在XAML中设置(直接或者通过设计工具)而不用程序代码。但是如果依赖属性没有额外的垂直传递,在不写额外代码的情况下,很难在设置属性这样简单的动作中获得想要的结果。
在本节中,我们将简要地看一下依赖属性的实现,让讨论更加具体。然后我们再深入分析依赖属性在普通.NET属性上赋值的下面一些方式:
l 变更通知
l 属性值继承
l 对多提供程序的支持
理解大多数依赖属性的细微差别,通常只是对于自定义控件设计者来说是重要的。然而,即使是WPF的普通用户,最终也需要了解依赖属性是什么以及它们如何工作。例如,你只能为依赖属性添加风格和动画效果。在使用WPF工作一段时间之后,你会发现你其实希望所有的属性都是依赖属性!
3.2.1 依赖属性的实现
实际上,依赖属性仅仅是普通的.NET属性,只不过它已融入到了WPF架构中。它完全是由WPF API实现的,没有一种.NET语言(除了XAML以外)天生就能理解依赖属性。
代码清单3-3展示了一个Button如何有效地实现一个叫作IsDefault的依赖属性。
代码清单3-3 一个标准的依赖属性实现
|
|
|
|
IsDefaultProperty静态成员是真正的依赖属性,类型为System.Windows.DependencyProperty。按规则,所有的DependencyProperty成员都必须是public、static,并且有一个Property作为后缀。依赖属性通常是通过调用DependencyProperty.Register静态方法创建的,这样的方法需要一个名称(IsDefault)、一个属性类型(bool)以及拥有这个属性的类(Button类)。通过不同的Register方法重载,你可以传入metadata(元数据)来告诉WPF如何处理该属性、如何处理属性值改变的回调、如何处理强制值转换,以及如何验证值。Button会在它的静态构造函数中调用Register的重载,给依赖属性一个默认值false,并为变更通知添加一个委托。
最后,那个叫作IsDefault的传统.NET属性会调用继承自System.Windows.Dependency- Object的GetValue和SetValue方法来实现自己的访问器,System.Windows.DependencyObject是底层基类,这是拥有依赖属性的类必须继承的。GetValue返回最后一次由SetValue设置的值,如果SetValue从未被调用过,那么就是该属性注册时的默认值。IsDefault .NET属性(有时叫作此上下文中的属性包装器)并不是必需的,Button的使用者可能会直接调用GetValue/ SetValue方法,因为它们是公开的。但是.NET属性会让以编程方式读写属性变得更加自然,它还允许通过XAML设置属性。
注意 在运行时,绕过了.NET属性包装器在XAML中设置依赖属性。
虽然XAML编译器在编译时是依靠该属性包装器的,但在运行时WPF是直接调用GetValue和SetValue的!因此,为了让使用XAML设置属性与使用过程式代码设置属性保持一致,在属性包装器中除了GetValue/SetValue调用以外,不应该包含任何其他逻辑,这是至关重要的。如果需要添加自定义逻辑,应该在注册的回调函数中添加。所有WPF的内建属性包装器都应遵守这个规则,因此这个警告是针对那些打算写带有依赖属性的自定义类的人的。
从表面上看,代码清单3-3像是一种冗长的呈现简单布尔属性的方式。然而,因为GetValue和SetValue内部使用了高效的稀疏存储系统,而IsDefaultProperty是一个静态成员(而不是一个实例成员),与典型的.NET属性相比,依赖属性的实现节省了保存每个实例所需要的内存。如果WPF控件的所有属性都是实例成员的包装器(与大部分.NET属性一样),由于所有的本地数据都会被添加到每个实例中,这样将会消耗大量的内存。每个Button有96个成员,每个Label有89个成员……,它们消耗的内存增长起来会很快!实际上,Button的96个属性中有78个是依赖属性,而Label的89个属性中的71个是依赖属性。
然而,依赖属性的好处远远不止内存使用这一项。它把相当一部分代码集中起来,并做标准化处理,这部分代码原本是要由属性实现者自己来写的,用来检查线程访问、请求容器元素重新呈现等。例如,当属性的值改变时(如Button的Background属性),如果它要求子元素重新呈现,就只要传递一个FrameworkPropertyMetadataOptions.AffectsRender标志给DependencyProperty. Register的一个重载。这一实现也列出了之前提到的3个特征,现在我们一个一个来讲解,首先是变更通知。
3.2.2 变更通知
无论何时,只要依赖属性的值改变了,WPF就会自动根据属性的元数据(metadata)触发一系列动作。这些动作可以重新呈现适当的元素、更新当前布局、刷新数据绑定等。内建的变更通知最有趣的特性之一是属性触发器,它可以在属性值改变时执行自定义动作,而不用更改任何过程式代码。
例如,假设你想让代码清单3-1中About对话框的每个Button的文本,在鼠标指针移上去时变为蓝色。如果没有属性触发器的话,你得为每个Button添加两个事件处理程序,一个是为MouseEvent事件准备的,一个是为MouseLeave事件准备的。

下面的C# code-behind文件中实现了这两个事件处理程序。
|
|
然而,有了属性触发器,你可以完全在XAML中完成相同的行为。下面Trigger对象就是需要写的所有代码:
![]()
这个触发器能够基于Button的IsMouseOver属性工作,当MouseEnter触发时,IsMouseOver属性会变为true;在MouseLeave触发时,它又变为false。注意,当IsMouseOver变为false时,不用把Foregound变为黑色,这是WPF自动完成的!
唯一需要做的就是把这个Trigger赋给每个Button。不幸的是,因为WPF 3.0的人为限制,不能把属性触发器直接应用到Button这样的元素上。它们只能在Style对象内部应用,因此在第10章中,我们将深入讲解属性触发器。同时,如果需要验证一下属性触发器,可以把触发器放入下面的过渡性XML元素,来把前面的Trigger应用到这个Button上。
![]()

然而,属性触发器仅仅是WPF支持的3种触发器之一。数据触发器是属性触发器的另一种形式,它可以在任何.NET属性中工作(而不仅仅是依赖属性),数据触发器将在第10章中讲解。事件触发器会通过声明方式指定动作,该动作在路由事件(将在本章中讲解)触发时生效。由于事件触发器总是与动画或声音一起工作,所以将在第13章中讲解。
注意 不要被元素的Triggers集合所愚弄!
FrameworkElement的Triggers属性是一个可读写的TriggerBase项(三种触发器类型的通用基类)集合,因此它是把属性触发器添加到item控件(如Button)的一种简单方式。不幸的是,这个集合只能包含WPF
3.0的事件触发器,因为WPF团队没有时间来实现这种支持。如果你尝试添加一个属性触发器(或数据触发器)到集合中,将导致运行时抛出一个异常。
3.2.3
属性值继承
术语“属性值继承”(简称属性继承)并不是指传统的面向对象的类继承,而是指属性值自顶向下沿着元素树传递。代码清单3-4展示了属性继承的简单示例,在代码清单3-1的基础上作了更新,显式地设置FontSize和FontStyle这两个依赖属性。图3-4展示了改变后的结果。(注意,是简单的SizeToContent设置,实现了窗口自动改变大小适应所有内容的功能!)
代码清单3-4 根Window元素上设置了Font属性的About对话框
![]()


对于大部分元素来说,这两个设置将会沿着逻辑树向下传递,并由子元素继承。这甚至会影响Button和ListBoxItem元素,尽管它们位于逻辑树的第三层。第一个Label的FontSize并没有改变,因为它被显式地设置为20,重载了继承的值30。而继承后的FontStyle的Italic(斜体)设置将会影响所有的Label、ListBoxItem和Button元素,这是因为它们没有被显式设置过。
注意,虽然StatusBar也像其他控件一样支持这两个属性,但它的文本并没有受到任何值的影响。属性值的继承行为是由以下两种因素决定的:
l 并不是每个依赖属性都参与属性值继承的。(从其内部来讲,依赖属性会通过传递FrameworkPropertyMetadataOptions.Inherits给DependencyProperty.Register方法来完成继承。)
l 有其他一些优先级更高的源来设置这些属性值,下一节中会解释这个内容。
在本例中,满足了后面一种因素。有一些控件如StatusBar、Menu和ToolTip控件,其内部会把字体属性设置为当前的系统设置。通过这种方式,用户可以在控制面板中控制它们的字体,这对于用户来说很熟悉。但结果可能会让你觉得迷惑,因为这样的控件最终会“吞噬”所有的继承,它会阻止继承继续沿着元素树向下传递。例如,如果把一个Button作为一个逻辑子元素添加到StatusBar控件中(见代码清单3-4),它的FontSize和FontStyle的默认值分别是12和Normal,这与StatusBar之外的其他按钮的值是不同的。
在其他地方使用属性值继承
属性值继承最初是基于元素树设计的,但它已经被扩展到了其他一些上下文中。例如,值可以被传递给一些看起来像XML子元素一样的元素(这是由XAML属性元素语法造成的),但是它们并不是逻辑树或者可视树中的子元素。这些伪子元素(pseudo-children)可以是一个元素的触发器或者任何属性的值(并不仅仅是Content或者Children属性),只要它是从Freezable派生而来的对象就行。这听上去可能有点霸道,且没有文档说明这一点,但这么做的目的是让几个基于XAML的方案能够按照你的意愿工作,而不用想太多。
3.2.4 对多个提供程序的支持
WPF有许多强大的机制可以独立地去尝试设置依赖属性的值。如果没有设计良好的机制来处理这些完全不同的属性值提供程序,这个系统会变得混乱,属性值会变得不稳定。当然,正如它们的名字所表达的,依赖属性就是设计为以一致的、有序的方式依靠这些提供程序。
图3-5展示了这5步流程,通过该流程,WPF运行每个依赖属性并最终计算出它的值。多亏了依赖属性中内嵌的变更通知,这个流程才可以自动发生。
|
|
|
|
|
图3-5 计算依赖属性值的管道
1.第一步:判断基础值
大多数属性值提供程序会把基础值的计算纳入考虑范畴。下面的代码清单显示了8个提供程序,它们可以设置大多数依赖属性的值,优先级顺序从高到低为:
(1) 本地值 (5) 主题样式触发器
(2) 样式触发器 (6) 主题样式设置程序
(3) 模板触发器 (7) 属性值继承
(4) 样式设置程序 (8) 默认值
你已见到了一些属性值提供程序,如属性值继承。本地值,技术上的含义是任何对DependencyObject.SetValue的调用,但是它通常会有一个简单的属性赋值,这是用XAML或者过程式代码完成的(由依赖属性的实现方式造成,在之前的Button.IsDefault示例中已经看到过了)。默认值指的是依赖属性注册时使用的初始值,自然其优先级是最低的。其他样式和模板方面的提供程序将在第10章中讲解。
这个优先级顺序解释了为什么代码清单3-4中的StatusBar控件的FontSize和FontStyle属性没有受到属性值继承特性的影响。StatusBar字体属性的设置是与系统设置一致的,这是由主题样式设置程序(上面清单中的第6项)负责实现的。
虽然这比属性值继承(上面清单中的第7项)的优先级要高,但是仍然可以使用更高优先级的机制来重载这些字体设置,例如设置StatusBar的本地值。
2.第二步:计算
如果第一步中的值是表达式(派生自System.Windows.Expression的一个对象),那么WPF会执行一种特殊的演算步骤——把表达式转换为具体的结果。在WPF 3.0中,表达式仅在使用动态资源(在第8章中有讲解)或数据绑定(第9章中的话题)时起作用。在WPF的未来版本中,可能会允许使用其他类型的表达式。
3.第三步:应用动画
如果一个或者多个动画在运行,它们有能力改变当前的属性值(使用第二步计算出来的值作为输入)或者完全替代当前的属性值。因此,动画(第13章的话题)胜过其他任何属性值提供程序——就连本地值也不是它的“对手”!这经常是一些初学WPF的人容易感到困惑的地方。
4.第四步:限制(Coerce)
在所有属性值提供程序处理过之后,WPF将拿到一个几乎是终值的属性值,如果依赖属性已经注册了CoerceValueCallback,还会把这个属性值传递给CoerceValueCallback委托。该回调函数负责返回一个新的值,它是基于自定义逻辑实现的。例如,内建的WPF控件,像ProgressBar,使用这个回调来限制一个叫作Value的依赖属性,这个值大于Minimum(最小值)常数,小于Maximum(最大值)常数,如果输入值小于Minimum,则返回Minimum;如果输入值大于Maximum,则返回Maximum。
5.第五步:验证
最后,如果依赖属性已经注册了ValidateValueCallback,之前的限制中的值将被传入ValidateValueCallback委托。如果输入值有效,该回调函数必须返回true;否则就返回false。返回false将会导致抛出一个异常,并使整个流程被取消。
提示 如果没办法判断依赖属性从哪里获得当前的值,那么可以使用静态方法Dependency- PropertyHelper.GetValueSource作为调试助手。该方法将返回一个ValueSource结构,其中包括了以下一些数据:一个BaseValueSource枚举值,它反映的是基础值从哪里来的(流程中的第一步);IsExpression、IsAnimated和IsCoerced几个布尔类型属性,它反映了第二步到第四步的信息。
当在代码清单3-1中调用StatusBar实例中的这个方法,或者在代码清单3-4中使用FontSize或FontStyle属性,返回的BaseValueSource是DefaultStyle,表示这个值是从一个主题样式设置程序中来的。(主题样式有时也叫作默认样式,它的触发器的枚举值是DefaultStyleTrigger。)
请不要在程序代码中使用这个方法!WPF以后的版本中将打破值计算的假设,会根据它的源类型采用不同的方式处理属性值,而不是根据假设WPF应用程序中的方式来处理。
清除本地值(Local Value)
之前的3.2.2节中演示了如何使用过程式代码来把Button的Foreground(前景色)变为蓝色,来响应MouseEnter事件,然后再把它变回黑色去响应MouseLeave事件。这种方式的问题在于MouseLeave中本地值被设置为黑色,这与Button的初始状态是很不同的,因为初始状态中黑色是通过主题样式中的一个设置程序来完成设置的。如果主题改变,新主题将尝试改变Foreground的默认颜色(如果具有更高优先级的提供程序想做同样的事),本地设置将为黑色。
你很可能需要清除本地值,并让WPF从下一个最高优先级的提供程序中获得值,然后使用这个值来设置最终的属性值。幸运的是,DependencyObject确实提供了这种机制,可以通过调用ClearValue方法来实现。在下面的C#代码中,调用了Button b的ClearValue方法:
(Button.ForegroundProperty是一个DependencyProperty静态成员),在调用ClearValue之后,会重新计算基础值,并把本地值从方程式中删除。
注意,在3.2.2节中IsMouseOver属性的触发器在实现事件处理程序时,没有出现相同的问题。一个触发器可以处于激活状态也可以处于未激活状态,当处于未激活状态时,WPF在属性值计算中会忽略该触发器。
3.2.5 附加属性
附加属性是依赖属性的一种特殊形式,可以被有效地添加到任何对象中。一开始,这可能听上去很奇怪,但是这个机制在WPF中有多种应用。
拿About对话框的例子来说,想象如果不设置整个Windows元素的FontSize和FontStyle(在代码清单3-4中),而是在内部的StackPanel上设置它们的话,这两个属性仅仅会被两个Button继承。然而,把属性特性移到内部的StackPanel元素中没有什么作用,因为StackPanel自己没有任何与字体相关的属性。相反,你必须使用FontSize和FontStyle附加属性,这是在一个叫作TextElement的类中定义的。代码清单3-5演示了如何使用这两个附加属性,其中还介绍了一种新的XAML语法,这是专门为附加属性设计的。这样就能启用一些我们所盼望的属性值继承特性,如图3-6所示。
代码清单3-5 About对话框,把Font属性移到了内部的StackPanel中

TextElement.FontSize和TextElement.FontStyle(而不是简单的FontSize和FontStyle属性)必须在StackPanel元素中使用,因为StackPanel没有这两个属性。当XAML解析器或者编译器遇到这种语法时,它就要求TextElement(有时叫作附加属性提供者)有两个静态方法分别叫作SetFontSize和SetFontStyle,这样它们才可以设置相应的属性值。代码清单3-5中列出的StackPanel的声明与下面的C#代码是一样的:

注意,像FontStyles.Italic、Orientation.Horizontal和HorizontalAlignment.Center这样的枚举值,在之前的XAML中被简单地写成了Italic、Horizontal和Center。之所以可以这么做是因为有.NET Framework中的EnumConverter类型转换器,它可以转换所有不区分大小写的字符串。
虽然代码清单3-5中的XAML很好地把FontSize和FontStyle作为StackPanel的逻辑附加属性,但是C#代码告诉我们这里并没有什么神奇之处,仅仅是调用了与一个元素相关联的方法,而那个元素中有一个与其他东西没有关系的属性。说到附加属性,有趣的事情之一就是.NET属性都不是附加属性!
从内部看,SetFontSize这样的方法会只是调用DependencyObject.SetValue方法,通常每一个普通的依赖属性访问器都会调用同一个方法,但是这里调用的是传进来的DependencyObject上的DependencyObject.SetValue方法,而不是当前实例上的:

与此类似,附加属性也定义了一个静态方法叫作GetXXX(XXX是属性的名称),它会调用我们所熟悉的DependencyObject.GetValue方法:

与普通依赖属性的属性包装器一样,这些GetXXX和SetXXX方法只能调用GetValue和SetValue方法,不能挪作他用。
理解附加属性提供程序
代码清单3-5中所使用的FontSize和FontStyle附加属性,最令人困惑的部分是,它们并不是在Button或者Control中定义的,而这些却是定义普通FontSize和FontStyle依赖属性的基类。它们是看似不相关的TextElement类定义的(还有在TextBlock类中定义,其实在前面的示例中也可以使用TextBlock类)。
既然TextElement.FontSizeProperty是一个与Control.FontSizeProperty无关的Depen- dencyProperty成员(而TextElement.FontStyleProperty也是与Control.FontStyleProperty无关的),那么这一切是如何实现的呢?关键在于这些依赖属性在内部注册的方式。如果你看看TextElement的源代码,就会发现类似下面的东西:
这与之前那个注册Button的IsDefault依赖属性的示例很相似,但这里RegisterAttached方法对附加属性的属性元数据(property metadata)的处理作了优化。
从另一方面讲,Control不能注册FontSize依赖属性!它需要调用TextElement已经注册的属性的AddOwner方法,获得对同一个实例的引用:
因此,所有控件继承的FontSize、FontStyle属性和其他与字体相关的依赖属性都是由TextElement提供的属性!
幸运的是,在大多数情况下,提供附加属性(如GetXXX和SetXXX方法)的类就是定义一些通用依赖属性的类,从而避免了混淆。
虽然About对话框示例为了实现高级属性值的继承使用了附加属性,但附加属性通常都是用于用户界面元素的布局。(事实上,附加属性一开始是为WPF的布局系统而设计的。)派生自Panel的各种类定义了一些附加属性,用来把它们添加到子元素上来控制它们的摆放。通过这种方式,每个Panel可以把自定义行为给任何一个子元素,而不需要所有的子元素都具有自己的相关属性集。这种方式也让像布局这样的系统扩展起来更加简单方便,因为任何人都可以写一个带有自定义附加属性的新Panel控件。这在第6章和第17章中将有详细讲解。
把附加属性作为一种扩展机制
类似于以往Windows Forms那样的技术,许多WPF类定义了一个Tag属性(类型是System.Object),目的是为了存储每一个实例的自定义数据。但是要添加自定义数据给任何一个派生自DependencyObject的对象,附加属性是一种更加强大、更加灵活的机制。通常我们会忽略一点,即你可以用附加属性高效地向密封类(sealed class)的实例添加自定义数据(WPF中到处都是密封类)。
另外,大家对附加属性有一个曲解,虽然在XAML中设置它们依赖于SetXXX静态方法,但是可以在过程式代码中绕过这个方法,直接去调用DependencyObject.SetValue方法。这意味着在过程式代码中,可以把任何一个依赖属性作为一个附加属性。例如,下面这行代码把ListBox的IsTextSearchEnabled属性添加到了Button控件上,并赋予该属性一个值:
虽然这似乎没有任何意义,也不会给这个Button带来一些神奇的新功能,但是你可以用一种对应用程序或者组件有意义的方式来随意使用这个属性值。
这种方法还有很多有趣的方式可以用来扩展元素。例如,FrameworkElement的Tag属性是一个依赖属性,因此可以把一个GeometryModel3D(在12.1节中将会再次见到这个类,它是一个密封类,没有Tag属性)的对象实例附加给它。
这仅仅是WPF提供可扩展性的一种方式之一,但不需要用到传统的继承特性。






