一站式WPF--依赖属性(DependencyProperty)一
Windows Presentation Foundation (WPF) 提供了一组服务,这些服务可用于扩展公共语言运行时 (CLR) 属性的功能,这些服务通常统称为 WPF 属性系统。由 WPF 属性系统支持的属性称为依赖项属性。 这段是MSDN上对依赖属性(DependencyProperty)的描述。主要介绍了两个方面,WPF中提供了可用于扩展CLR属性的服务;被这个服务支持的属性称为依赖属性。 单看描述,云里雾里的,了解一个知识,首先要知道它产生的背景和为什么要有它,那么WPF引入依赖属性是为了解决什么问题呢?
从属性说起属性是我们很熟悉的,封装类的字段,表示类的状态,编译后被转化为get_,set_方法,可以被类或结构等使用。 一个常见的属性如下: 1:publicclassNormalObject 2:{ 3:privatestring_unUsedField; 4: 5:privatestring_name; 6:publicstringName 7:{ 8:get 9:{ 10:return_name; 11:} 12:set 13:{ 14:_name=value; 15:} 16:} 17:} 在面向对象的世界里,属性大量存在,比如Button,就大约定义了70-80个属性来描述其状态。那么属性的不足又在哪里呢? 当然,所谓的不足,要针对具体环境来说。拿Button来讲,它的继承树是Button->ButtonBase->ContentControl->Control->FrameworkElement->UIElement->Visual->DependencyObject->… 每次继承,父类的私有字段都被继承下来。当然,这个继承是有意思的,不过以Button来说,大多数属性并没有被修改,仍然保持着父类定义时的默认值。通常情况,在整个Button对象的生命周期里,也只有少部分属性被修改,大多数属性一直保持着初始值。每个字段,都需要占用4K等不等的内存,这里,就出现了期望可以优化的地方:
依赖属性的原型根据前面提出的需求,依赖属性就应运而生了。一个简单的依赖属性的原型如下: DependencyProperty: 1:publicclassDependencyProperty 3:internalstaticDictionary<object,DependencyProperty>RegisteredDps=newDictionary<object,DependencyProperty>(); 4:internalstringName; 5:internalobjectValue; 6:internalobjectHashCode; 7: 8:privateDependencyProperty(stringname,TypepropertyName,TypeownerType,objectdefaultValue) 10:this.Name=name; 11:this.Value=defaultValue; 12:this.HashCode=name.GetHashCode()^ownerType.GetHashCode(); 13:} 14: 15:publicstaticDependencyPropertyRegister(stringname,TypepropertyType,244);">16:{ 17:DependencyPropertydp=newDependencyProperty(name,propertyType,ownerType,defaultValue); 18:RegisteredDps.Add(dp.HashCode,dp); 19:returndp; 20:} 21:} 22: DependencyObject: 1:publicclassDependencyObject 5:publicstaticreadonlyDependencyPropertyNameProperty= 6:DependencyProperty.Register("Name",typeof(string),typeof(DependencyObject),string.Empty); 8:publicobjectGetValue(DependencyPropertydp) 10:returnDependencyProperty.RegisteredDps[dp.HashCode].Value; 12: 13:publicvoidSetValue(DependencyPropertydp,objectvalue) 14:{ 15:DependencyProperty.RegisteredDps[dp.HashCode].Value=value; 17: 18:publicstringName 19:{ 20:get 21:{ 22:return(string)GetValue(NameProperty); 23:} 24:set 25:{ 26:SetValue(NameProperty,value); 27:} 28:} 29:} 30: 这里,首先定义了依赖属性DependencyProperty,它里面存储前面我们提到希望抽出来的字段。DP内部维护了一个全局的Map用来储存所有的DP,对外暴露了一个Register方法用来注册新的DP。当然,为了保证在Map中键值唯一,注册时需要根据传入的名字和注册类的的HashCode取异或来生成Key。这里最关键的就是最后一个参数,设置了这个DP的默认值。 然后定义了DependencyObject来使用DP。首先使用DependencyProperty.Register方法注册了一个新的DP(NameProperty),然后提供了GetValue和SetValue两个方法来操作DP。最后,类似前面例子中的NormalObject,同样定义了一个属性Name,和NormalObject的区别是,实际的值不是用字段来保存在DependencyObject中的,而是保存在NameProperty这个DP中,通过GetValue和SetValue来完成属性的赋值取值操作。 当然,作为一个例子,为了简洁,很多情况没有考虑,现在来测试一下是否解决了前面的问题。 新建两个对象,NormalObject和DependencyObject,在VS下打开SOS查看: .loadsos extensionC:WINDOWSMicrosoft.NETFrameworkv2.0.50727sos.dllloaded !DumpHeap-stat-typeNormalObject total1objects Statistics: MTCountTotalSizeClassName 009e30d0116DPDemonstration.NormalObject Total1objects !DumpHeap-stat-typeDependencyObject total1objects Statistics: MTCountTotalSizeClassName 009e31a0112DPDemonstration.DependencyObject Total1objects
这里在对象中分别建立了一个_unUsedField的字段,.Net的GC要求对象的最小Size为12字节。如果对象的Size不足12字节,则会自动补齐。默认的Object对象占用8字节,Syncblk(4字节)以及TypeHandle(4字节),为了演示方便,加入了一个_unUsedField(4字节)来补齐。 这里,DependencyObject相比NormalObject,减少了_name的储存空间4字节。 再进一步 万里长征第一步,这个想法可以解决我们希望的问题,这个做法还不能让人接受。在这个实现中,所有DependencyObject共用一个DP,这个可以理解,但修改一个对象的属性后,所有对象的属性相当于都被修改了,这个就太可笑了。 所以对象属性一旦被修改,这个还是要维护在自己当中的,修改一下前面的DependencyObject,引入一个有效(Effective)的概念。 改进的DependencyObject,加入了_effectiveValues: 1:publicclassDependencyObject 2:{ 3:privateList<EffectiveValueEntry>_effectiveValues=newList<EffectiveValueEntry>(); 4: 5:publicstaticreadonlyDependencyPropertyNameProperty= 7: 8:publicobjectGetValue(DependencyPropertydp) 9:{ 10:EffectiveValueEntryeffectiveValue=_effectiveValues.FirstOrDefault((i)=>i.PropertyIndex==dp.Index); 11:if(effectiveValue.PropertyIndex!=0) 12:{ 13:returneffectiveValue.Value; 14:} 15:else 16:{ 17:returnDependencyProperty.RegisteredDps[dp.HashCode].Value; 18:} 19:} 20: 21:publicvoidSetValue(DependencyPropertydp,objectvalue) 22:{ 23:EffectiveValueEntryeffectiveValue=_effectiveValues.FirstOrDefault((i)=>i.PropertyIndex==dp.Index); 24:if(effectiveValue.PropertyIndex!=0) 25:{ 26:effectiveValue.Value=value; 27:} 28:else 29:{ 30:effectiveValue=newEffectiveValueEntry(){PropertyIndex=dp.Index,Value=value}; 31:_effectiveValues.Add(effectiveValue); 32:} 33:} 34: 35:publicstringName 36:{ 37:get 38:{ 39:return(string)GetValue(NameProperty); 40:} 41:set 42:{ 43:SetValue(NameProperty,value); 44:} 45:} 46:} 新引进的EffectiveValueEntry: 1:internalstructEffectiveValueEntry 3:internalintPropertyIndex{get;set;} 5:internalobjectValue{get;set;} 6:} 改进的DependencyProperty,加入了ProperyIndex: 3:privatestaticintglobalIndex=0; 4:internalstaticDictionary<object,244);">5:internalstringName; 6:internalobjectValue; 7:internalintIndex; 8:internalobjectHashCode; 9: 10:privateDependencyProperty(stringname,244);">11:{ 12:this.Name=name; 13:this.Value=defaultValue; 14:this.HashCode=name.GetHashCode()^ownerType.GetHashCode(); 16: 17:publicstaticDependencyPropertyRegister(stringname,244);">18:{ 19:DependencyPropertydp=newDependencyProperty(name,244);">20:globalIndex++; 21:dp.Index=globalIndex; 22:RegisteredDps.Add(dp.HashCode,244);">23:returndp; 24:} 25:} 在DependencyObject加入了一个_effectiveValues,就是把所有修改过的DP都保存在EffectiveValueEntry里,这样,就可以达到只保存修改的属性,未修改过的属性仍然读取DP的默认值,优化了属性的储存。 更进一步的发展 到目前为止,从属性到依赖属性的改造一切顺利。但随着实际的使用,又一个问题暴露出来了。使用继承,子类可以重写父类的字段,换句话说,这个默认值应该是可以子类化的。那么怎么处理,子类重新注册一个DP,传入新的默认值? 当然,不会实现的这么丑陋。同一个DP,要想支持不同的默认值,那么内部就要维护一个对应不同DependencyObjectType的一个List,可以根据传入的DependencyObject的类型来读取它对应的默认值。 DP内需要维护一个自描述的List,按照微软的命名规则,添加新的类型属性元数据(PropertyMetadata): 1:publicclassPropertyMetadata 3:publicTypeType{get;set;} 4:publicobjectValue{get;set;} 5: 6:publicPropertyMetadata(objectdefaultValue) 8:this.Value=defaultValue; 9:} 10:} 对应修改DependencyProperty 1:publicclassDependencyProperty 3:privatestaticintglobalIndex=0; 5:internalstringName; 6:internalobjectValue; 7:internalintIndex; 8:internalobjectHashCode; 9:privateList<PropertyMetadata>_metadataMap=newList<PropertyMetadata>(); 10:privatePropertyMetadata_defaultMetadata; 11: 12:privateDependencyProperty(stringname,objectdefaultValue) 13:{ 14:this.Name=name; 15:this.Value=defaultValue; 16:this.HashCode=name.GetHashCode()^ownerType.GetHashCode(); 17: 18:PropertyMetadatametadata=newPropertyMetadata(defaultValue){Type=ownerType}; 19:_metadataMap.Add(metadata); 20:_defaultMetadata=metadata; 21:} 22: 23:publicstaticDependencyPropertyRegister(stringname,244);">24:{ 25:DependencyPropertydp=newDependencyProperty(name,defaultValue); 26:globalIndex++; 27:dp.Index=globalIndex; 28:RegisteredDps.Add(dp.HashCode,dp); 29:returndp; 30:} 31: 32:publicvoidOverrideMetadata(TypeforType,PropertyMetadatametadata) 33:{ 34:metadata.Type=forType; 35:_metadataMap.Add(metadata); 36:} 37: 38:publicPropertyMetadataGetMetadata(Typetype) 39:{ 40:PropertyMetadatamedatata=_metadataMap.FirstOrDefault((i)=>i.Type==type)?? 41:_metadataMap.FirstOrDefault((i)=>type.IsSubclassOf(i.Type)); 42:if(medatata==null) 43:{ 44:medatata=_defaultMetadata; 46:returnmedatata; 47:} 48:} 修改DenpendencyObject中的GetValue并更改_effectiveValues,为了简洁去掉了NameProperty以及SetValue. 3:privateList<EffectiveValueEntry>_effectiveValues=newList<EffectiveValueEntry>(); 5:publicobjectGetValue(DependencyPropertydp) 6:{ 7:EffectiveValueEntryeffectiveValue=_effectiveValues.FirstOrDefault((i)=>i.PropertyIndex==dp.Index); 8:if(effectiveValue.PropertyIndex!=0) 10:returneffectiveValue.Value; 12:else 14:PropertyMetadatametadata; 15:metadata=DependencyProperty.RegisteredDps[dp.HashCode].GetMetadata(this.GetType()); 16:returnmetadata.Value; 17:} 18:} 19:} 这样,就可以定义一个SubDependencyObject,调用OverrideMedata向DP的_metadataMap中加入新的Metadata。 1:publicclassSubDependencyObject:DependencyObject 3:staticSubDependencyObject() 4:{ 5:NameProperty.OverrideMetadata(typeof(SubDependencyObject),newPropertyMetadata("SubName")); 6:} 7:} 创建一个DependencyObject以及SubDependencyObject,可以发现,Name的值已经被改为”SubName”了。当然,实际DP中对Metadata的操作比较繁琐,当子类调用OverrideMetadata时会涉及到Merge操作,把新的Metadata与父类的合二为一。并且在GetMetadata中,要取得自己或者是与它最近的父类的Metadata,为了可以获得最近的父类,WPF引入了一个DependencyObjectType的类,在构造时传入BaseType=this.base.GetType(),这里为了简单,忽略不计。 WPF对依赖属性的扩展 前面的例子里,依据优化储存的思想,我们打造了一个DependencyProperty。当然,有了这样一门利器,不好好打磨打磨真是对不起它,WPF在这个基础上对DP进行了扩展,使其更加的强大。 对通常的CLR属性来说,在Set中加入一些逻辑判断是很正常的,当然也可以在Set中发出一些事件或者更改其他一些属性。那么依赖属性,它对此又有什么支持呢? 顺水推舟,WPF在DP的PropertyMedata中加入了PropertyChangedCallback以及CoerceValueCallback等。这些Delegate可以在构造PropertyMetadata时传入,在SetValue过程中,会取得对应的PropertyMetadata,然后回调PropertyChangedCallback。这个PropertyMetadata可以在构建DP时传入,也可以在子类调用OverrideMetadata时传入,这就保证了同一个DP不同的DependencyObject可以有不同的应用。WPF对此进行了很多扩展,定义了一套属性赋值的规则,包括计算(calculate)、限制(Coerce)、验证(Validate)等等。 当然,这些扩展说开了会很多,WPF对此也进行了精巧的设计,这也就是我们开篇提到的WPF提供了一组服务,用于扩展CLR属性。 多属性值 发展都是由需求来推动的,在WPF的实现过程中,又产生了这样一个需要: WPF是原生支持动画的,一个DP属性,比如Button的Width,你可以加入动画使他在1秒内由100变为200,在动画结束后,又希望它能恢复原来的属性值。同理,你可以在XAML表达式中对属性进行赋值,当表达式失效时同样期望他恢复成原来的属性值。这个需求来自于,对同一个属性的赋值可能发生在不同的场合,当对象状态改变时属性也要发生相应的变化,这里就产生了两个需要:
同一个属性有多个值,这个对CLR属性来说有些难为它了。但是对DP来说却很简单,本来DP的值就是保存在我们定义的EffectiveValueEntry中的,以前是保存一个Value,现在定义多个值就可以了。 3:privateobject_value; 5:internalintPropertyIndex{get;set;} 6: 7:internalobjectValue 8:{ 9:get 10:{ 11:return_value; 12:} 13:set 15:_value=value; 18: 19:internalModifiedValueModifiedValue 20:{ 21:get 22:{ 23:if(this._value!=null) 24:{ 25:return(this._valueasModifiedValue); 26:} 27:returnnull; 30:} 对应的ModifiedValue: 1:internalclassModifiedValue 3:internalobjectAnimatedValue{get;set;} 4:internalobjectBaseValue{get;set;} 5:internalobjectCoercedValue{get;set;} 6:internalobjectExpressionValue{get;set;} 当属性没有被修改过,ModifiedValue为空,当修改过后,ModifiedValue被赋值。这里EffectiveValueEntry定义了很多方法如SetExpressionValue(object value),SetAnimatedValue(object value)等来向ModifiedValue中写入对应值;并且EffectiveValueEntry提供了IsAnimated,IsExpression等属性来表示当前的状态。当然,这个赋值的操作比较复杂,这个优先级分两大类:一 ModifiedValue中各属性的优先级;二 对于ExpressionValue来说,它又有自己的优先级,Local>Style>Template…这里就不详细解释了。
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |