• WPF中的DependencyProperty存储方式详解


    前言

    接触WPF有一段时间了,之前虽然也经常使用,但是对于DependencyProperty一直处于一知半解的状态。今天花了整整一下午将这个概念梳理了一下,自觉对这个概念有了较为清晰的认识,之前很多很混沌的概念和理解也变得比较清晰,因此想把那些问题和不解的解决过程都清晰地还原展示出来,期望对那些也在学习WPF的朋友有所帮助。

    这里还要说句题外话,在博客园上有很多非常出色的介绍WPF的文章,为什么我还要去写这个呢?一方面对我个人而言是总结归纳,另一方面,也是最重要的一点,我一直认为最适合教授解答某个问题的人是刚理解这个问题的那些人,而不是有很丰富经验的人,因为这个人刚刚经历了从不理解到理解的思维过程,那些困扰他,让他欲罢不能苦恼万分的关键问题(key point,很多时候是无法理解某个问题的关键)的思维过程还非常新鲜。这些经验有时候才是最珍贵的,因为人的思维方式都是大同小异的,在对同一个比较困难的问题的理解上很多人的思维路径基本都是一样的,如果能循着自己思维惯性向前推进将一个个难点消除,这样理解的程度和学习的效果肯定远比被动接受一连串概念和知识要强的多。那些经验很丰富的大师因为对这个问题已经有了很深刻的理解,那些我们看来很难理解的地方对他们而言已经变得如常识本能一般,他认为理所当然的东西往往在新手看来其实非常费解,所以他们更倾向于将他们所知道的知识瀑布式地写下来,新手看完后除了依稀记得几个概念,对于理解过程仍是一头雾水。说了这么多废话是希望那些对这个概念仍有不解的朋友们能耐心看完本文,我会尽力带您和我一起解决这块难啃的骨头。

    DP的存储方式

    Dependency Property(下文简称为DP) 是WPF的基础,WPF的很多非常关键的特性都依赖于DP,比如DataBinding,Animation,Style等。和普通的.net属性相比(clr property),DP有如下特点:

    1. 一个属性可以有多个值(local,default,animation等),而且可以根据优先级确定具体的值。也就是是多数据源支持(multiple providers)。同时DP内置了对新值的验证,如果不符合要求可以取消更改(如slider的value如果超过了上下界就可以取消更改)
    2. 改变通知(DP的改变会自动通知界面更新,clr属性若要实现该功能需要实现INotifyPropertyChange接口并处罚propertychange事件通知界面更新)
    3. 属性继承(沿着逻辑树继承)

    我们先来看看DP的基本用法

     public static readonly DependencyProperty myProperty =
                DependencyProperty.Register("my", typeof (string), typeof (UserControl1),new PropertyMetadata("default"));
          public string my
            {
                get { return (string) GetValue(myProperty); }
                set { SetValue(myProperty, value); }
            }
    

    这是在visualstudio中键入dependencyproperty时帮我们自动生成的代码,从中我们可以看到dp是静态属性,而根据我们的使用经验,我们知道每个实例的DP的值是不同的(不然我们也不可能在XAML里使用DP来定义每个窗体实例的诸如宽度,颜色等属性了),也就是说按照我们理解DP应该是个实例属性才对,那问题到底出在哪呢?理解这个问题也是理解DP的关键。我们不妨先来猜测一下WPF是如何实现的,很容易想到使用DP的每个实例应该都有一个数据结构用来存储DP真正的值,静态的DP属性用来存储DP的默认值(在之前的代码中是”default”),当然这只是我们的初步设想,到底是不是这样呢?让我们去WPF的代码里一窥究竟吧。

    我们先来看一下DependencyObject(下文简称DO)的代码(只有继承自该类的类才能使用DP),我们在DO中发现了如下代码:

     在属性定义中有一个数组:

    private EffectiveValueEntry[] _effectiveValues;
    EffectiveValueEntry结构的代码:
    Internal struct EffectiveValueEntry
    {
    Internal int PropertyIndex{get;set;}
    Internal object Value{get;set;}
    }
    

     这个数组是不是就是用来保存我们的具体DP值的数据结构呢?我们要去DO的GetValue和SetValue看一下:

    public object GetValue(DependencyProperty dp)
        {
          this.VerifyAccess();
          if (dp == null)
            throw new ArgumentNullException("dp");
          else
            return this.GetValueEntry(this.LookupEntry(dp.GlobalIndex), dp, (PropertyMetadata) null, RequestFlags.FullyResolved).Value;
        }
    

    我们可以看到getvalue方法里面根据传进来的DP实例得到DP的index,然后再用getvalueentry方法在effectiveValueEntry数组里查找这个DO实例里有没有存储这个DP的值,如果没有则返回DP的默认值,如果有,说明这个DO实例的这个DP属性值修改过,则返回EffectiveValueEntry数组里保存的DP值。

    public void SetValue(DependencyProperty dp, object value)
        {
          this.VerifyAccess();
          PropertyMetadata metadata = this.SetupPropertyChange(dp);
          this.SetValueCommon(dp, value, metadata, false, false, OperationType.Unknown, false);
        }
    

    我们可以看到setvalue方法根据传入的DP实例的meta信息调用setvaluecommon方法进行一系列诸如安全,操作类型等的检查后,在setvaluecommon的最后执行了如下代码:

    int num = (int) this.UpdateEffectiveValue(entryIndex1, dp, metadata, oldEntry, ref newEntry, coerceWithDeferredReference, coerceWithCurrentValue, operationType);
    

    更新了effectiveValueEntry数组里的的dp的值。

    好了看完上述过程,我们对DP的值存储有了一定的了解,然而离DP的真面目还有一点距离,因为前文里看似简单的描述中其实遗漏了一个关键的信息:
    我们平时在XAML里使用DP时,是采用类似Width=”**”的方式,这个Width是怎么转化成我们setvalue方法传进来的dp实例的呢?难道是根据width字符串反射到DO的DP属性字段?

    要解决上述疑问则要去DP的代码里一窥究竟了,首先要看的当然是DP的register方法,DP的register方法是一个重载的方法,最后都调用了如下方法:

    public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata, ValidateValueCallback validateValueCallback)
        {
          DependencyProperty.RegisterParameterValidation(name, propertyType, ownerType);
          PropertyMetadata defaultMetadata = (PropertyMetadata) null;
          if (typeMetadata != null && typeMetadata.DefaultValueWasSet())
            defaultMetadata = new PropertyMetadata(typeMetadata.DefaultValue);
          DependencyProperty dependencyProperty = DependencyProperty.RegisterCommon(name, propertyType, ownerType, defaultMetadata, validateValueCallback);
          if (typeMetadata != null)
            dependencyProperty.OverrideMetadata(ownerType, typeMetadata);
          return dependencyProperty;
        }
    

     我们可以看到首先调用registerparametervalidation检查了一下几个参数是否为空,然后调用registercommon方法去注册这个DP,这个方法是DP注册的关键,因此我把这个方法的代码也列了出来:

    private static DependencyProperty RegisterCommon(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata, ValidateValueCallback validateValueCallback)
        {
          DependencyProperty.FromNameKey fromNameKey = new DependencyProperty.FromNameKey(name, ownerType);
          lock (DependencyProperty.Synchronized)
          {
            if (DependencyProperty.PropertyFromName.Contains((object) fromNameKey))
              throw new ArgumentException(MS.Internal.WindowsBase.SR.Get("PropertyAlreadyRegistered", (object) name, (object) ownerType.Name));
          }
          if (defaultMetadata == null)
          {
            defaultMetadata = DependencyProperty.AutoGeneratePropertyMetadata(propertyType, validateValueCallback, name, ownerType);
          }
          else
          {
            if (!defaultMetadata.DefaultValueWasSet())
              defaultMetadata.DefaultValue = DependencyProperty.AutoGenerateDefaultValue(propertyType);
            DependencyProperty.ValidateMetadataDefaultValue(defaultMetadata, propertyType, name, validateValueCallback);
          }
          DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, defaultMetadata, validateValueCallback);
          defaultMetadata.Seal(dp, (Type) null);
          if (defaultMetadata.IsInherited)
            dp._packedData |= DependencyProperty.Flags.IsPotentiallyInherited;
          if (defaultMetadata.UsingDefaultValueFactory)
            dp._packedData |= DependencyProperty.Flags.IsPotentiallyUsingDefaultValueFactory;
          lock (DependencyProperty.Synchronized)
            DependencyProperty.PropertyFromName[(object) fromNameKey] = (object) dp;
          if (TraceDependencyProperty.IsEnabled)
            TraceDependencyProperty.TraceActivityItem(TraceDependencyProperty.Register, (object) dp, (object) dp.OwnerType);
          return dp;
        }
    

     其中FromNameKey是DP里定义的一个类,代码如下:

    private class FromNameKey
        {
          private string _name;
          private Type _ownerType;
          private int _hashCode;
    
          public FromNameKey(string name, Type ownerType)
          {
            this._name = name;
            this._ownerType = ownerType;
            this._hashCode = this._name.GetHashCode() ^ this._ownerType.GetHashCode();
          }
    
          public override int GetHashCode()
          {
            return this._hashCode;
          }
          }
        }
    

    我们可以看到registercommon方法大致做了以下几件事:

    根据propertyname和ownertype构造的fromnamekey来在DP的PropertyFromName静态属性(一个由DP实例组成的哈希表)中查找DP实例。我们来看一下fromnamekey类的gethashcode方法

    this._hashCode = this._name.GetHashCode() ^ this._ownerType.GetHashCode();
    

    fromnamekey的哈希值是根据propertyname和ownertype的哈希值求异或得到的,也就是说一个WPF程序启动后,这个程序中就有一个全局变量(DependencyProperty.PropertyFromName)保存了系统中所有已注册的DP。这样我们之前那个“如何根据XAML中的属性名称来寻找DP”的问题就有答案了,系统根据属性名(PropertyName)和使用该属性的实体类(ownertype)生成的hashcode在DependencyProperty.PropertyFromName中查找到DP实例,再根据DP的globalindex(全局索引,每新注册一个DP加1)去在DO中调用getvalue方法获取具体的值。

    回答完上面那个问题,我们继续看registercommon方法,如果根据fromnamekey计算出来的hash值在该哈希表中已存在元素,则说明该DP已被注册过(由此我们也可以看到WPF是根据PropertyName和Ownertype来唯一标识一个DP的),方法就会抛出一个提示信息为PropertyAlreadyRegistered的异常。如果该DP未注册,则实例化一个DP,将meta信息封装到DP的_packedData属性中,同时将该DP存到PropertyFromName这个哈希表中。

    至此之前的两个问题都已经得到解决了,我们也弄清楚了DP到底是如何存储的了。这里我们简单做个总结:

    1. DO的EffectiveValueEntry数组保存了这个DO实例中的所有被修改过的DP的值。
    2. DP的PropertyFromName哈希表保存了系统中所有已注册DP的实例以及根据这个DP的propertyname和ownertype计算出来的hash值。这样所有DP的默认值和一些meta信息(比如值改变回调函数,是否继承上级值等一些信息)就能够很好的保存和查询。
    3. 每个DP实例都有一个globalindex,这个globalindex初始值为0,每次注册一个DP就加1,在DO中查找DP的具体值时就是利用这个index来查询的
    4. 在xaml中以如下方式查找DP的具体值:

    根据DP的属性名(就是DP注册方法的第一个参数)和该DP所有者类型(ownertype)计算出来的hash值来在DP的静态变量PropertyFromName中查找DP实例,然后根据DP实例的globalindex查找具体的值(见第3点)

    这里要多说一句,我们常常看到在xaml中用的Width对应的就是WidthProperty

    Color对应的就是ColorProperty,这些都是WPF里的Convention,但并不代表WPF查找DP时只是简单的在属性后面加上”property“字符串而已。

    说了这么多,现在要说说DP这种存储方式相对.net普通属性的好处。拿WPF里的Button来说,这个类有100多个属性(很多是从父类继承过来的),对于大部分Button实例来说,这其中很多属性都不会用到或者只需用默认值,因此如果都采用普通clr属性的话,每个Button就要浪费很多内存空间。而采用DP的话那些不怎么要用的值或者不怎么更改的值就都存在DP里面,而DP是静态属性,是一个类所共有的,因此也就极大地节省了内存空间。

    当然DP所带来的好处绝非只有节省内存而已,笔者本来计划将DP主要的一些特性都在一篇博客里写完,但是为了尽量把问题说清楚写着写着就发现只写了存储方式就已经写了这么多。因此关于DP的其他特性会留待下文分解,最后希望看完本文的你对DP的存储方式有了清晰的了解,如有不清楚的地方欢迎留言沟通交流,谢谢!

    作者:Leo-Yang
    原文都先发布在作者个人博客:http://www.leoyang.net/
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.
  • 相关阅读:
    初始ASP.NET数据控件【续 DataList】
    初始ASP.NET数据控件GridView
    初始ADO.NET数据操作
    初识 Asp.Net数据验证控件
    【Socket编程】Java通信是这样炼成的
    JAVA之I/O 输入输出流详解
    浅入深出之Java集合框架(下)
    浅入深出之Java集合框架(中)
    浅入深出之Java集合框架(上)
    全面解释java中StringBuilder、StringBuffer、String类之间的关系
  • 原文地址:https://www.cnblogs.com/developerY/p/3193309.html
Copyright © 2020-2023  润新知