依赖项属性是标准.NET属性的全新实现——具有大量新增价值。在WPF的核心特性(如动画、数据绑定以及样式)中需要嵌入依赖项属性。WPF元素提供的大多数属性都是依赖项属性。到目前位置所见到的所有示例都用到了依赖项属性,但你可能还没有意识到这一点。这是因为依赖项属性的用法和普通属性的是相同的。
然而,依赖项属性并非普通属性。可能乐意认为依赖项属性是添加了一套WPF功能的常规属性(采用典型的.NET方式进行定义)。从概念上讲,依赖项属性确实以这种方式工作,但它们的背后的实现方式并非如此。原因十分简单:出于性能的考虑。如果WPF设计者只是在.NET属性系统上添加额外功能,就需要为编写代码创建一个复杂庞大的层次。如果不承受这一额外的负担,普通属性就不能支持这些依赖项属性的所有功能。
依赖项属性是专门针对WPF创建的。但WPF库中的依赖项属性都使用普通的.NET属性过程(property procedure)进行了封装。这样便可以通过常规方式使用它们,即使使用它们的代码不理解WPF依赖项属性系统也同样如此。用旧技术封装新技术看起来有些奇怪,但这正是WPF能够改变基础组成部分(如属性),而不会扰乱.NET领域中其他部分的原因。
一、定义依赖项属性
相对于创建依赖项属性,大多数情况下只是使用它们。但是,仍然有许多原因需要创建自己的依赖项属性。显然,如果正在设计自定义的WPF元素,它们肯定是关键部分。然而,当希望为原本不支持数据绑定、动画或其他WPF功能的部分代码添加这些功能时,也需要创建依赖项属性。创建依赖项属性并不难,但需要使用一些特殊语法。这与创建普通的.NET属性完全不同。
第一步是定义表示属性的对象,它是DependencyProperty类的实例。属性信息应该始终保持可用,甚至可能需要在多个类之间共享这些信息(在WPF元素中这是十分普遍的)。因此,必须将DependencyProperty对象为与其相关联的类的静态字段。
例如,FrameworkElement类定义了Margin属性,所有元素都共享该属性。Margin属性是依赖项属性。这意味着,在FrameworkElement类中需要使用类似下面的代码来定义Margin属性:
public class FrameworkElement:UIElement.... { public static readonly DependencyProperty MarginProperty .... }
根据约定,定义依赖项属性的字段的名称是在普遍属性的末尾处加上单词"Property"。根据这种命名方式,可从实际属性的名称中区分出依赖项属性的定义。字段的定义使用了readonly关键字,这意味着只能在FrameworkElement类的静态构造函数中对其进行设置,这就是接下来将完成的任务。
二、注册依赖项属性
定义DependencyProperty对象只是第一步而已。为了使用依赖项属性,还需要使用WPF注册创建的依赖项属性。这一步骤需要在任何使用属性的代码之前完成,因此必须在与其关联的类的静态构造函数中进行。
WPF确保DependencyProperty对象不能被直接实例化,因为DependencyProperty类没有公有的构造函数。相反,只能使用静态的DependencyProperty.Register()方法创建DependencyProperty实例。WPF还确保在创建DependencyProperty对象后不能改变该对象,因为所有DependencyProperty成员都是只读的。它们的值必须作为Register()方法的参数来提供。
下面的代码显示了如何创建DependencyProperty对象。在此,FrameworkElement类使用静态函数来初始化MarginProperty:
static FrameworkElement(){ FrameworkPropertyMetadata metadata=new FrameworkPropertyMetadata(new Thickness(),FrameworkPropertyMetadataOptions.AffectsMeasure); MarginProperty=DependencyProperty.Register("Margin",typeof(Thickness),typeof(FrameworkElement),metadata,new ValidateValueCallback(FrameworkElement.IsMarginValid)); .... }
注册依赖项属性需要经历两个步骤。首先创建FrameworkPropertyMetadata对象,该对象指示希望通过依赖项属性使用什么服务(如支持数据库绑定、动画以及日志)。接下来通过调用DependencyProperty.Register()静态方法注册属性。在这一步骤中,你负责提供以下几个要素:
- 属性名(在该例中为Margin)
- 属性使用的数据类型(在该例中为Thickness结构)
- 拥有该属性的类型(在该例中为FrameworkElement类)
- 一个具有附加属性设置的FrameworkPropertyMetadata对象,该要素是可选的。
- 一个用于验证属性的回调函数,该要素是可选的
前三个要素都很直观。FrameworkPropeMetadata对象和属性验证回调函数更有趣一些。
使用FrameworkPropertyMetadata对象配置创建的依赖项属性的附加功能。FrameworkPropertyMetadata类的大多数属性是简单的Boolean标记,通过设置这些属性来翻转某项功能(每个Boolean标记的默认值为false)。只有少数几个是指向用于执行特定任务的自定义方法的回调函数,其中一个是FrameworPropertyMetadata.Defaultvalue,用于设置在第一次初始化属性时WPF将要应用的默认值。下表列出了FrameworkPropertyMetadata类的所有属性。
名称 | 说明 |
AffectsArrange、AffectsMeasure、AffectsParentArrange和AffectsParentMeasure | 如果为true,依赖项属性会影响在布局操作的测量过程和排列过程中如何放置相邻的元素或父元素。例如,Margin依赖项属性将AffectsMeasure属性设置为true,表面如果一个元素的边距发生变化,那么布局容器需要重新执行测量步骤以确定元素新的布局。 |
AffectsRender | 如果为true,依赖项属性会对元素的绘制方式造成一定的影响,要求重新绘制元素。 |
BindsTwoWayByDefault | 如果为true,默认情况下,依赖项属性将使用双向数据绑定而不是单向数据绑定。不过,当创建数据绑定时,可以明确指定所需的绑定行为。 |
Inherits | 如果为true,就通过元素树传播该依赖项属性值,并且可以被嵌套的元素继承。例如,Font属性是可继承的依赖项属性——如果在更高层次的元素中为Font属性设置了值,那么该属性值就会被嵌套的元素继承(除非使用自己的字体设置明确地覆盖继承而来的值) |
IsAnimationProhibited | 如果为true,就不能将依赖项属性用于动画 |
IsNotDataBindale | 如果为true,就不能使用绑定表达式设置依赖项属性 |
Journal | 如果为true,在基于页面的应用程序中,依赖项属性将被保存到日志(浏览过的页面的历史记录)中 |
SubPropertiesDoNotAffectRender | 如果为true,并且对象的某个子属性(属性的属性)发生了变化,WPF将不会重新渲染该对象 |
DefaultUpdateSourceTrigger | 当该属性用于绑定表达式时,该属性用于为Binding.UpdateSourceTrigger属性设置默认值。UpdateSourceTrigger属性决定了数据绑定值在何时应用自身的变化。当创建绑定时,可以手动设置UpdateSourceTrigger属性。 |
DefaultValue | 该属性用于为依赖项属性设置默认值 |
CoerceValueCallback | 该属性提供了一个回调函数,用于在验证依赖项属性之前尝试“纠正”属性值 |
PropertyChangedCallback | 该属性提供了一个回调函数,当依赖项属性的值变化时调用该回调函数 |
三、添加属性包装器
创建依赖项属性的最后一个步骤就是使用传统的.NET属性封装WPF依赖项属性。但典型的属性过程是检索或设置某个私有字段的值,而WPF属性的属性过程是使用在DependencyObject基类中定义的GetValue()和SetValue()方法。下面列举一个示例:
public Thickness Margin { get{ return (Thickness)GetValue(MarginProperty);} set{ SetValue(MarginProperty,value);} }
当创建属性封装器时,应当只包含对SetValue()和GetValue()方法的调用,如上面的示例所示。不应当添加任何验证属性值和额外代码、引发事件的代码等。这是因为WPF中的其他功能可能会忽略属性封装器,并直接调用SetValue()和GetValue()方法(一个例子是,在运行时解析编译过的XAML文件)。SetValue()和GetValue()方法都是公有的。
现在已经拥有了一个功能完备的依赖项属性,可以使用属性封装器像设置其他任何.NET属性那样设置该依赖项属性了:
myElement.Margin=new Thickness(5);
还有一个额外的细节。依赖项属性遵循严格的优先规则来确定他们的当前值。即使你没有直接设置依赖项属性,它也可能已经有了数值——该数值可能是由数据绑定、样式或动画提供的,也可能是通过元素树继承来的。不过,只要直接设置了属性值,设置的属性值就会覆盖所有其他的影响。
以后,可能希望删除本地值设置,并像从来没有设置过那样确定属性值。显然,这不能通过设置一个新值来实现。反而需要使用另外一个继承自DependencyObject类的方法:ClearValue()。下面是该方法的用法:
myElement.ClearValue(FrameworkElement.MarginProperty);
四、WPF使用依赖项属性的方式
WPF的许多功能都需要使用依赖项属性。但是,所有这些功能都是通过每个依赖项属性都支持的两个关键行为进行工作的——更改通知和动态值识别。
可能与你所期望的相反,当属性值发生变化时,依赖项属性不会自动引发事件以通知属性值发生了变化。相反,他们会触发受保护的名为OnPropertyChangedCallback()的方法。该方法通过两个WPF服务(数据绑定和触发器)传递信息,并调用PropertyChangedCallback()回调函数(如果已经定义了该函数)。
换句话说,当属性变化时,如果希望进行响应,有两种选择——可以使用属性值创建绑定,也可以编写能够自动改变其他属性或开始动画的触发器。但依赖项属性没有提供一种通用的方法以触发一些代码,从而对属性的变化进行响应。
对于依赖项属性工作很重要的第二个功能就是动态值识别。这意味着当从依赖项属性检索值时,WPF需要考虑多个功能。
依赖项属性因该行为得名——本质上,依赖项属性依赖于多个属性提供者,每个提供者都有各自的优先级。当从属性检索值时,WPF属性系统会通过一系列步骤获取最终值。首先通过考虑以下因素(按优先级从低到高的顺序排列)来决定基本值(base value):
(1)默认值(由FrameworkPropertyMetadata对象设置的值)。
(2)继承而来的值(假设设置了FrameworkPropertyMetadata.Inherits标志,并为包含层次中的某个元素提供了值)。
(3)来自主题样式的值。
(4)来自项目样式的值。
(5)本地值(使用代码或XAML直接为对象设置的值)。
如上面的列表所示,可通过直接应用一个值来覆盖整个层次。如果不这么做,属性值可由上面列表中的下一个可用项确定。
WPF按照上面的列表确定依赖项属性的基本值。但基本值未必就是最后从属性中检索的值。这是因为WPF还需要考虑其他几个可能改变属性值得提供者。
下面列出WPF决定属性值得四步骤过程:
(1)确定基本值
(2)如果属性是使用表达式设置的,就对表达式进行求值。当前,WPF支持两类表达式:数据绑定和资源。
(3)如果属性是动画的目标,就应用动画。
(4)运动CoerceValueCallback回调函数来修正属性值。
本质上,依赖项属性被硬编码连接到一小部分WPF服务中。如果并非用于这个基础结构,这些功能就会无谓地增加复杂性并带来沉重负担。
五、共享的依赖项属性
尽管一些类具有不同的继承层次,但他们回共享同一依赖项属性。例如,TextBlock.FontFamily属性和Control.FontFamily属性指向同一个静态的依赖项属性,该属性实际上是在TextElement类中定义的TextElement.FontFamilyProperty依赖项属性。TextElement类的静态构造函数注册该函数,而TextBlock类和Control类的静态构造函数只是通过调用DependencyProperty.AddOwner()方法重用该属性:
TextBlock.FontFamilyProperty=TextElement.FontFamilyProperty.AddOwner(typeof(TextBlock));
可以使用相同的基础来创建自己的自定义类(假定在所继承的父类中还没有提供属性,否则直接重用即可)。还可以使用重载的AddOwner()方法来提供验证回调函数以及仅应用于依赖项属性用法的新FrameworkPropertyMetadata对象。
在WPF中重用依赖项属性可得到一些奇异的效果,最有名得是样式。例如,如果使用样式自动设置TextBlock.FontFamily属性,样式也会影响Control.FontFamily属性,因为在后台这两个类使用同一个依赖项属性。
六、附加的依赖项属性
附加属性是一种依赖项属性,由WPF属性系统管理。不同之处在于附加属性被应用到得类并非定义附加属性的那个类。
例如,Grid类定义了Row和Column附加属性,这两个属性被用于设置Grid面板包含的元素,以指明这些元素应被放到哪个单元格中。类似地,DockPanel类定义了Dock附加属性,而Canvas类定义了Left、Right、Top和Bottom附加属性。
为了定义附加属性,需要使用RegisterAttached()方法,而不是使用Register()方法。下面列举了一个注册Grid.Row属性的例子:
FrameworkPropertyMetadata metadata=new FrameworkPropertyMetadata(0,new PropertyChangedCallback(Grid.OnCellAttachedPropertyChanged)); Grid.RowProperty=DependencyProperty.RegisterAttached("Row",typeof(int),typeof(Grid),metadata,new ValidateValueCallback(Grid.IsIntValueNotNegative));
与普遍的依赖项属性一样,可提供FrameworkPropertyMetadata对象和ValidateValueCallback回调函数。
当创建附加属性时,不必定义.NET属性封装器。这是因为附加属性可以被用于任何依赖对象。例如,Grid.Row属性可能被用于Grid对象(如果在Grid控件中嵌套了另一个Grid控件),也可能被用于其他元素。实际上,Grid.Row属性甚至可以被用于并不位于Grid控件中的元素——甚至在元素树中根本就不存在Grid对象。
不是使用.NET属性封装器,反而附加属性需要调用两个静态方法来设置和获取属性值,这两个方法使用了为人熟知的SetValue()和GetValue()方法(继承自DependencyObject类)。这两个静态方法应当命名为SetPropertyName()和GetPropertyName()。
下面是实现Grid.Row附加属性的静态方法:
public static int GetRow(UIElement element) { if(element==null) { throw new ArgumentNullException(...); } return (int)element.GetValue(Grid.RowProperty); } public static void SetRow(UIElement element,int value) { if(element==null) { throw new ArgumentNullException(...); } element.SetValue(Grid.RowProperty,value); }
下面的示例使用代码将元素放到Grid控件中的第一行:
Grid.SetRow(txtElement,0);
也可直接调用SetValue()或GetValue()方法,从而绕过这两个静态方法:
txtElement.SetValue(Grid.RowProperty,0);
显然,使用SetValue()方法设置附加属性的过程不符合一般人的思维习惯。尽管XAML不允许,但可在代码中使用重载版本的SetValue()方法,为任何依赖项属性附加一个值,即使该属性没有被定义为附加属性也同样如此。例如,下面的代码是完全合法的:
ComboBox comboBox=new ComboBox(); .... comboBox.SetValue(PasswordBox.PasswordCharProperty,"*");
这里为ComboBox对象设置了PasswordBox.PasswordChar属性值,尽管PasswordBox.PasswordCharProperty属性被注册为普通的依赖项属性而不是附加属性。该操作不会改变ComboBox的工作方式——毕竟,ComboBox的内部代码不会去查找它并不知道的属性的值——但在你自己的代码中可以对PasswordChar值进行操作。
尽管很少使用,但该技巧提供了WPF属性系统内部工作方式的更多细节,还演示了其非凡的可扩展性。它还表明,尽管使用不同的方法注册附加属性和常规的依赖项属性,但对于WPF而言它们没有实质性区别。唯一的区别是XAML解析器是否允许。除非将属性注册为附加属性,否则在标记的其他元素中无法设置。