WPF里的DependencyProperty(4)

首先回顾一下上一篇Post提出来的三个问题。

  • 我只定义了一个静态的DependencyProperty类,在实例中并没有提供一个成员变量或是什么地方存储这个属性的值,那么这个值存储到什么地方了呢?
  • 上一篇Post提到的“反射”问题。不论在数据绑定还是动画中我们都只提供了一个属性名字符串,难道WPF真的需要使用反射读取属性数据?
  • 如果我们在动画进行时同时通过Slider来改变DependencyProperty的值,会发生什么?此时Property中到底存储了来自哪里的值,是原始我们赋给的值?是数据绑定的值?还是当前动画的值?此时我们通过GetValue方法,会得到什么样的值呢?

    解决这些问题,我们需要了解一些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看看源码,还是很有意思的,呵呵。

    P.S.

    这个关于WPF DependencyProperty还会继续,我计划全部写完之后将其整理一下。其实到这里,我已经介绍了大部分DependencyProperty的功能了,后续主要还有两方面内容需要讨论,一是关于分离Property和Value也就是AttachedProperty,这一块我在那篇《WPF/Silverlight为什么要使用Canvas.SetLeft()这样的方法?》中已经稍微介绍了一些,另一块是关于属性元数据(Property Metadata)的。由于某些原因,本周我可能没空发布我的下一篇Post了。因此我先将这几天内我发布的几篇文章做一个简单的汇总,供大家参考:

    .Net3.0里的DependencyProperty(1):引入了DP的一些基本概念(还是去年写的 呵呵)
    WPF里的DependencyProperty(2):DP的应用,主要想解释我们为什么要用它
    WPF里的DependencyProperty(3):主要想演示下怎么用DP
    WPF/Silverlight为什么要使用Canvas.SetLeft()这样的方法? :附属,从布局应用上解释了下为什么要有AttachedProperty
    WPF里的DependencyProperty(4):介绍了DP的工作机制

  • posted on 2008-04-24 09:53  Yuxin Yang  阅读(7401)  评论(10编辑  收藏  举报