WPF里的DependencyProperty(4)
首先回顾一下上一篇Post提出来的三个问题。
解决这些问题,我们需要了解一些DependencyProperty的工作机制,这也就是今天的主要内容。下面就开始。
尝试DIY一个DependencyProperty
Microsoft总是封装好了一切,并给我们一个“最佳实践”,让我们照着MSDN抄代码。照猫画虎写代码谁都会,但是DependencyProperty到底是如何工作的呢?
从上面的例子我们已经看到,DependencyProperty需要处理例如:TargetProperty="Transparency"这样对属性的访问,也就是通过字符串获取属性的值。
下面我们先假设我们自己是WPF team的一员,我们如何来实现上面的功能呢?如果是我,我可能会考虑写出类似下面这样的代码:
public static object GetValueUsingReflaction(object obj, string propertyName)
{
return obj.GetType().GetProperty(propertyName).GetValue(obj, null);
}
public static void SetValueUsingReflaction(object obj, string propertyName,object value)
{
obj.GetType().GetProperty(propertyName).SetValue(obj,value,null);
}
我使用反射读取除了属性的值,这样没有问题,不过也许你开始考虑性能问题了。前面中说过,反射是“贵族”功能,十分损耗性能,而我们这边仅仅是使用了反射的众多功能中的一样:寻找属性的元数据,然后GetValue。因此,我们也不用考虑比如如何获得方法签名等等复杂的问题。
除了性能问题,其实还有另一方面问题,也就是上一篇Post提到的第三个问题,如果同时有多个值被应用在这个属性上,我们应当如何处理呢?
为了解决这些问题,我们自己可以实现一个简单而高效的“类似反射”。思路其实很简单,就是使用一个HashTable利用"Key-Value Pair"(键-值对)存储性质,很容易想到,Key的类型是String,Value的类型是Object,类似这样:
private IDictionary<String, object> _innerProperties = new Dictionary<String, object>();
这时候,我们该开始思考一些问题了,这个Dictionary是用来存储属性值的,好像应该属于具体的实例而不是属性本身。我们需要一个类来定义这个容器,这个类就是WPF中的DependencyObject。其实我在前面并没有提到,所有需要使用DependencyProperty的类都应该继承自DependencyObject,就好象上一篇Post中的MyBorderEx类,它继承自Border,而Border再向上n层,就是继承自DependencyObject的。
DependencyObject在WPF中是很基础的一个类,绝大多数的对象都继承于他(可以参考MSDN中一篇文章"WPF Architecture")。
我试着实现了简单的MyDependencyObject:
public class MyDependencyObject
{
private IDictionary<String, object> _innerProperties = new Dictionary<String, object>();
public object GetValue(string propertyName)
{
return _innerProperties[propertyName];
}
public void SetValue(string propertyName, object value)
{
_innerProperties[propertyName] = value;
//do other thing like "onPropertyChanged",
}
首先注明这里只是示意代码,什么线程安全之类的一概没有考虑。
我发现似乎还缺少一个“注册属性”的功能,我们不希望所谓的“属性”真的就成了一个字符串了,其实这就是WPF中的DependencyProperty.Regist方法。我顺便把我们的MyDependencyProperty简单实现一下:
public class MyDependencyProperty
{
public static MyDependencyProperty Regist(string propertyName, Type valueType,Type ownerType )
{
MyDependencyProperty result = new MyDependencyProperty();
result._name = propertyName;
result._valueType = valueType;
result._ownerType = ownerType;
return result;
}
private string _name;
public string Name
{
get
{ return _name; }
}
private Type _valueType;
public Type ValueType
{
get
{ return _valueType; }
}
private Type _ownerType;
public Type OwnerType
{
get
{ return _ownerType; }
}
}
很简单吧,其实这已经开始接近WPF DependencyProperty的实现了,下面我们继续考虑我们的代码,来看看还有什么问题。
你肯定会觉得这个代码很奇怪,MyDependencyProperty和MyDependencyObject类似乎没有任何关系,为什么我们还要有MyDependencyProperty的存在呢?答案是我们不能真的只用一个字符串去标识属性,字符串仅仅代表了属性的名称,我们还需要了解更多的信息,比如上面写的ValueType和OwnerType,MyDependencyProperty类在这里起到了一个属性标识的作用。其实我们只需要对MyDependencyObject做很小的改动
public object GetValue(MyDependencyProperty mdp)
{
return _innerProperties[mdp.Name];
}
public void SetValue(MyDependencyProperty mdp, object value)
{
_innerProperties[mdp.Name] = value;
//do other thing like "onPropertyChanged",
}
你会发现好像我们已经重新实现了WPF DependencyProperty,不过WPF的源代码可远比这个复杂(真正的源代码现在可以下载到了,你也可以通过Reflactor等工具查看),呵呵。不过,这个程序已经代表了WPF DependencyProperty数据存储的基本方式。
好了,到现在,对于上一篇Post中提出的第一个问题,你应该有一些答案了吧。
DependencyProperty自身的存储方式
下面来说说WPF是如何从由XAML的描述读取到DependencyProperty的值的。就用前面的例子,我们获得了这样一个字符串"Transparency",然后我们当然可以通过XAML解析出MyBorderEx这个对象,如果通过GetValue方法,我们需要传入一个DependencyProperty对象,现在的问题就是如何通过Transparency"这个字符串得到我们定义好的那个TransparencyProperty全局变量。
顺便插一句,上一篇Post提到的必须定义"CLR Wapper"的问题和我们现在讨论没有关系,之所以WPF会去寻找set方法,是因为XAML编译器在编译时并不能确定系统中存在名称为"Transparency"的DependencyProperty,看了下问你会知道,这个名称"Transparency"是运行期才确定的。换句话说,也就是编译时系统并不知道我们定义的_innerProperties容器中是否存在"Transparency"这个Key,为了保证正确,必须至少有一个"CLR Wapper"存在。这样即使没有定义TransparencyProperty,XAML也可以通过传统CLR属性来读写值。
继续上面的问题,来说说这个“字符串”到"DependencyProperty"的过程。
WPF绝对不是使用组合字符串 "属性名"+"Property" 然后反射来获得这个Property的,而是更加高效的方法。你可以试着把TransparencyProperty改名,程序依然能够运行。
那么WPF是如何进行查找的呢?很简单,WPF还维护了另一个容器,里面是系统中所有的DependencyProperty,类似这样的一个Dictionary:
private static IDictionary<Int32, DependencyProperty> PropertiesFromName = new Dictionary<Int32, DependencyProperty>();
没错,静态的,维护了系统中所有存在的DependencyProperty,在程序一开始就产生了这个。
这里的Key是一个Int32类型整数,实际上是一个Hashcode,这个Hashcode是怎么算出来的呢?
this._hashCode = this._name.GetHashCode() ^ this._ownerType.GetHashCode();
可以把_name和_ownerType两个参数就是DependencyProperty的名称和所有者类型,这正是DependencyProterty.Regist方法中提供了的。
这样我们就清楚了,原来WPF还在系统中维护了一个全局Hashtable,用来保存整个应用程序中所有的DependencyProperty,Key是由DependencyProperty名称(字符串类型),和这个DependencyProperty所有者的Type类型共同产生的。换句话说,Regist方法中传入的name和ownType两者共同作为了WPF维护的这个全局Hashtable的“主键”。
也就是说,我们如何定义“一个DependencyProperty”? 答案是如果他拥有全局不重复的Name,还有一个全局不重复的ownerType,他就是独立的“一个DependencyProperty”。
这样我们就可以回答第二个问题了,如何由字符串获得DependencyProperty呢?先得到属性字符串的Hashcode,再寻找所属于的类型,获得ownType的Hashcode,然后将两者异或,用结果在全局的哈希列表中作为Key找到对应的Value,这就是要找的DependencyProperty了。
DependencyProperty中值的存放方式
我们DIY自己的DependencyProperty的时候,值只是用了一个object对象存放在Hashtable中的,虽然WPF中的值存储更加复杂,但基本原理也差不多。大体上就是通过比较复杂的Hash机制,来完成属性到值的映射过程。
例如有这么一句语句:xxxObj.GetValue(xxxxProperty);
在DependencyObject中维护了一个HashTable,这个HashTable不是静态的,也就是每个实例都有一个(和开始我们实现的那个Dictionary类似),我们首先计算xxxxProperty的Hash,就是第二个小结里提到的那个Key,然后利用这个Key可以通过某些算法找到Value存储的位置。
这里要注明一下,实际上WPF内部在存储值时并没有使用Hashtable,而是实现了一个数组,并且通过一定的映射方式去找到具体的值,相当于自己重新实现了Hash机制,我想这应当也是为了效率着想吧。
那么,对于上面的第三个问题,一个DependencyProperty中又是如何存储多个值的呢?
答案是计算出最后的对应了Value的Key后,并不会直接的到一个Value,而是得到一个被封装了的对象,这个对象中包含了,包括属性当前的状态(是否在动画中,是否被强制限制)以及每个状态对应的值。所以这个对象可以存储多个值,并且能够根据属性当前的状态返回适当的值。
这里提一下:WPF的为每个DependencyProperty预留了四个存放值值的位置,分别是AnimatedValue, BaseValue, CoercedValue, ExpressionValue,他们的作用从字面上就能看出来。平时我们Set和Get的,都是BaseValue。
最后一个问题是如果我们同时对DependencyProperty进行了动画,数据绑定或其它操作,GetValue时会得到一个什么样的值呢?
这个问题的答案其实MSDN中已经说的很清楚了,这是一个值的“优先级别”的问题,具体的内容参考MSDN,我就不再解释了。如果你了解了上面的值存储机制,也许你很快就会清楚的理解这个“优先级别”的问题了。
我认为,深入的理解关于DependencyProperty中值得“优先级别”,对理解DependecyProperty以及灵活的在程序中使用DependencyProperty是非常有帮助的。
参考: Dependency Property Value Precedence
不过其中另外的一些功能实现,例如继承值,取得默认值,这都是通过属性元数据(Property Metadata)实现的,现在我先不讨论这个。
小结
今天我们以上一篇Post留下的三个问题为线索,比较详细的介绍了DependencyProperty的部分工作机制,包括DependencyProperty自身的存储方式
和DependencyProperty中值的存放方式……由于我的表达能力限制(本来想画些图,可是怎么都画不出来 我比较缺乏想象力 呵呵),可能完全理解这些机制还不是非常容易。如果你想深入的理解DependencyProperty,推荐大家没事儿的时候多写写代码,逛逛国内外的坛子,或者用Reflactor看看源码,还是很有意思的,呵呵。