WPF里的DependencyProperty

  • A+
所属分类:WPF
见识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。

摘自:https://www.cnblogs.com/yayx/archive/2008/06/03/1213126.html

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: