WPF里的DependencyProperty(5)
好久没更新了,首先是这个月一直在忙于各种杂事……其次是发现WPF中除了DependencyProperty之外很多新东西都很好玩,玩其他的去了,以至没有及时更新这里。废话不多说,现在我们来讨论WPF DependencyProperty的Metadata(元数据)以及AttachedProperty。
见识PropertyMetadata
如果你尝试过自己定义一个DependencyProperty,你一定会发现在DependencyProperty.Regist方法中可以传入一个PropertyMetadata类型的对象,这就是属性的"Metadata"。如果你对.Net框架比较了解,你对"Metadata"这个词应该不陌生,简单地说,Metadata就是一个用来描述对象自身的对象,同理,这里的Metadata也就是我们用来描述DependencyProperty本身的东西。
这么说比较抽象。看些具体的东西吧。
DependencyProperty.Register("Custom", typeof(string), typeof(Window), new PropertyMetadata("Hello"));
我们注册了一个Name为Custom的DependencyProperty,这里的new PropertyMetadata("Hello")创建了一个元数据,"Hello"参数代表“默认值”。顾名思义,我们给这个属性定义了一个默认值,这应该很好理解了吧。如果你接触过Asp.net控件开发,你是不是会想到在属性前使用Attribute(特性,俗称“方括号”)定义默认值?没错,使用Attribute其实就是在.net属性的元数据中添加内容,在使用DependencyProperty时就不用这么麻烦,我们把new一个Metadata传进去就OK了。好吧Metadata就那么简单,下面我们看看PropertyMetadata这个对象除了能定义属性的DefaultValue之外还能做什么。
还有两个属性CoerceValueCallback和PropertyChangedCallback,这两个属性类型都是delegate,也就是可以传入方法的委托,CoerceValueCallback用于定义“强制”属性值时执行的方法,PropertyChangedCallback用于定义属性值发生变化是执行的方法。详细的用法可以参考MSDN,这里不多说。
这里我想解释一下调用DependencyObject的GetValue方法获取属性值时是如何受到Metadata的影响的。
我曾经提到过DependencyProperty的“优先级别”,MSDN链接:Dependency Property Value Precedence
MSDN这篇文档中列举了10个获取值得优先级别,其中拥有最高优先级的是"Property system coercion",而拥有最低优先级的是Default value from dependency property metadata,你应该发现,这两项正是在Metadata中的CoerceValueCallback和DefaultValue属性。
可以想象一下需要获取一个值时的执行过程,系统先从最低优先级别的项(DefaultValue)开始,按优先级由低到高逐个尝试得到值,假若在某一步能得到值,则覆盖当前的值。
优先级最高的是"Property system coercion",我们看一个MSDN中一个简单的CoerceValueCallback例子:
private static object CoerceCurrentReading(DependencyObject d, object value)
{
Gauge g = (Gauge)d;
double current = (double)value;
if (current < g.MinReading) current = g.MinReading;
if (current > g.MaxReading) current = g.MaxReading;
return current;
}
优先级最高,也就是最后才尝试这个方法,我们可以这么理解,之前已经通过各种方法尝试取得一个值了,现在把这个值传到这个方法里来,做一次最后的检查,就好象这里的value,不论前面通过什么方式设置了值,都需要在最后这里限制范围。
至于PropertyChangedCallback就不多说了,实现了一个“属性改变通知”功能。
小结一下,MetaData利用DefaultValue和CoerceValueCallback为DependencyProperty提供了最基本的数据保障和最后的数据把关。在这里建议大家定义自己的DependencyProperty时一定要提供DefaultValue。毕竟是“基本保障”。
有一个细节需要提到,Metadata虽然可以在注册DependencyProperty时候设置,但是千万不要以为一个Metadata是对应一个DependencyProperty对象,这也许不好理解,他不是“属性”的Metadata么?实际上,Metadata是按照一个DependencyProperty对应不同DependencyObject存储的,可以这么理解,单个DependencyProperty并非一个“属性”,只要一个DependencyObject和一个DependencyProperty在一起,才组成一个真正的“属性”(还记得系统全局索引DependecyProperty使用的Hash Key吗? "this._hashCode = this._name.GetHashCode() ^ this._ownerType.GetHashCode();" )
因此通过单个DependencyPropery你只能获得"DefaultMetada",正确获取Metadata方法是使用DependencyProperty.GetMetadata方法,他需要传入一个DependencyObject对象。
WPF专用——FrameworkPropertyMetadata
Metadata除了上面这些基本功能外,就是为特定环境下提供特定的功能服务,前面提到过,这里的DependencyProperty是专门为WPF服务的,自然也就有专门为WPF服务的Metadata,也就是FrameworkPropertyMetadata。他继承于上面提到的PropertyMetadata,加入了许多可定义的属性,专门用来处理WPF界面显示时所需要的一些功能。
P.S. 如果你对WPF框架比较熟悉,你一定不会对FrameworkElement陌生吧,凡是涉及到UI的东西,WPF里都会给一个Framework前缀,呵呵。
前面提到过很多WPF的特性,比如第二篇Post中提到的“值继承”“自动的进行重新布局”都和FrameworkPropertyMetadata中的设置有关,具体的设置在MSDN中。(Frame Property Metadata)已经讲的非常详细了,在这里我只简要的说一些吧。
- WPF的Layout引擎中有Measure,Arrange,Render三个方法,如果你认为当你定义的DependencyProperty值变化时界面上应当调用相应的方法(例如改变Background时需要重新Render),那么你以考虑在Metadata中设置AffectsArrange, AffectsMeasure, AffectsRender这些标志;
- 如果属性值变化时不仅自己需要调用相应的方法,还需要自己的父对象调用相应的方法(例如改变Size时需要同时改变父对象的Size),那么可以考虑在Metadata中设置AffectsParentArrange, AffectsParentMeasure这些标志;
- 属性值继承。我认为这是WPF中的属性系统中最漂亮的一个功能设计;
另外FrameworkPropertyMetadata也为WPF的数据绑定操作提供了一些功能,还有为NavigateWindow提供的功能等,就不详细说了。
看一下FrameworkPropertyMetadataOptions的枚举值就知道,他实现的功能太多了。可以这么说,前面的Post中讨论过的DependencyProperty的属性值存储等机制提供了一个基础,而真正利用这个基础实现的功能,几乎都通过FrameworkPropertyMetadata实现了(现在的WPF DependencyProperty果真完完全全是为WPF框架提供的)。
关于"AttachedProperty"
在第三篇Post(WPF/Silverlight为什么要使用Canvas.SetLeft()这样的方法?)中,我已经涉及到了关于AttachedProperty的介绍和使用,这里就不再赘述了。其实如果你已经完全理解了DependencyProperty中值的存储机制,也许已经不会再对“AttachedProperty”这个奇怪的东西感到困惑了。
我们先回忆一下DependencyProperty中值的存储机制的几个关键点(详细的讨论请参考WPF里的DependencyProperty(4)):
- 每个DependencyProperty都是一个DependencyProperty对象,可以通过DependencyProperty.Register静态方法获得;
- 系统中有一个全局静态的HashTable,通过DependencyPropery的ownnerType和Name两个键值对索引取得某个具体的DependencyProperty;
- 每个DependencyObject中有一个非静态的HashTable,可以通过一个DependencyProperty索引取得某个值。
注意第三点,每个DependencyObject中作为索引的DependencyProperty如果定义在自身内部,那么很好理解,但是如果这个DependencyProperty定义在其他的DependencyObject中呢?这就是AttachedProperty了。
举个最简单的例子,假设我们有如下的语句:
<Button Name="button1" Canvas.Left="100" >button</Button>
这里的LeftProperty定义在Canvas类,不过却在Button的实例中调用,这正是AttachedProperty。
你也许会有疑问,如果是这样的话,"AttachedProperty"和一般的"DependencyProperty"不是没有区别吗?虽然很奇怪,不过的确是这样的,我认为"AttachedProperty"只是Microsoft为了让大家更好理解“为不定义在自己身上的属性赋值”这个行为造出来的词吧。事实上,虽然MSDN中说明需要使用DependencyProperty.RegisterAttached方法来定义AttachedProperty,但是你可以大胆尝试一下,如果你使用DependencyProperty.Register方法也是没有问题的。不过还是推荐大家按照MSDN中说的做,我肯定在某些细节处Register方法和RegisterAttached方法还是不同的(Adam Nathan在"Windows Presentation Foundation Unleashed"中提到,RegisterAttached对Metadata做了某些优化,具体的我也不是很清楚)。
说到这里,这种复杂而奇怪的设计大概已经把你弄得有些晕了吧。呵呵,不过理解了还是很有意思的。下面说一下最后一种同样有些奇怪的方法。
使用AddOwner方法注册属性
我们先看一个现象,先看这句代码。
<Button FontSize="20">Test</Button>
这很简单是吧,那这样呢:
<Button FontSize="20">
<TextBlock>Test</TextBlock>
</Button>
定义的TextBlock同样可以继承到FontSize="20"这个属性,这正是“属性值继承”的特性。我们先不考虑“属性值继承”,我们考虑一下TextBlock.FontSizeProperty和Button.FontSizeProperty是不是一个DependentyProperty呢?也许你的第一反应“肯定不是”,如果不是,那么为什么Button.FontSizeProperty的设置会影响到TextBlock呢?
答案确是“是”,他们确实是同一个DependencyProperty。不信的话,你可以试试看下面的代码:
<Button TextBlock.FontSize="20">Button</Button>
同样设置了Button的FontSize,很有意思吧。为什么会这样呢?
其实真正的FontSizeProperty属性既没有定义在Button中,也没有定义在TextBlock中,而是定义在TextElement对象中的,通过Reflactor可以看出来,Button和TextBlock中使用了这样的语句来定义FontSizeProperty(Button中的FontSizeProperty继承自Control.FontSizeProperty):
FontSizeProperty = TextElement.FontSizeProperty.AddOwner(typeof(TextBlock));
这是什么意思呢?再次回忆DependencyProperty值得存储机制,其实AddOwner方法就是为已经定义的DependencyProperty在全局静态的HastTable中生成了一个新的Key,这个Key使用原来DependencyProperty的Name,但是用了新的ownerType。重点是,这里产生的仅仅是一个新的Key,并没有生成一个新的DependencyProperty对象,换句话说,新旧两个Key都指向了同一个DependencyProperty对象。这样也就很好理解为什么TextBlock.FontSize能设置Button的FontSize了。
如果你还是不相信,你可以直接在程序中使用 bool f = (TextBlock.FontSizeProperty == Button.FontSizeProperty) 语句试试看,返回的是true,这表明他们指向的是同一个DependencyProperty。
实质上,AddOwner方法生成的DependencyProperty可以看成是AttachedProperty的一种变形,如果没有AddOwner方法,也许我们需要把所有的FontSize都得写成TextElement.FontSize,这显然会更加令人费解。因此这是一个很有用的功能,最经典的场景大概就是解决属性继承中的问题吧,就像上面的例子,你只需要在Window对象或Button对象中设置FontSize,他的子元素继承到值后就都知道需要做什么,不需要再去考虑设置的到底是Button.Fontsize还是Window.FontSize或者是TextBlock.FontSize。
总结
今天一口气写了不少东西。首先我们认识了DependencyProperty中的Metadata,也谈到了Metadata的存储,还有专门为WPF提供的FrameworkPropertyMetadata。然后,我们通过DependencyProperty中值得存储机制了解了AttachedProperty到底是个什么东西,还讨论了通过AddOwner方法注册的一种比较特殊的DependencyProperty。
整个系列到这里,我们已经探讨了WPF DependencyProperty的绝大部分设计和功能的实现机制。这个系列也算是可以告一段落了。我还留下了一些例如“属性值继承”,“Metadata Override”,“ValidateCallback”等可以讨论的话题。我觉得相对来说这些话题没有那么重要(因为MSDN里都有),如果有时间我会单独再写一两篇Post来解释他们。
唉,整个系列计划的内容的终于写完了,整整拖了一年多^_^。虽然文章数目不多,篇幅还是挺长的,呵呵。下面我准备整理一下这些文章,顺便看看还有没有什么遗漏的东西,同时修正一下前面文章里犯的错误。
写这些文章的过程,也是我深入学习的过程,还是非常开心的,同时非常希望和大家多多交流~也希望大家能提出一些宝贵的建议。
P.S 其实一直在想研究这些东西有什么用,我觉得只是想多问个“为什么”罢了,同时也学到了不少新东西。比如我觉得DependencyProperty值得存储方式就设计的非常巧妙,HashTable还能这么用,很多地方设计都能值得借鉴。再者,WPF中很多设计都是融会贯通的,比如RoutedEvent系统很多地方与DependencyProperty的设计有异曲同工之妙,搞懂了这些,其他很多东西(特别是一些“奇怪的行为”)也就很容易理解了。总的说来,感觉很有意义,呵呵。