前言

接触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的存储方式有了清晰的了解,如有不清楚的地方欢迎留言沟通交流,谢谢!

WPF中的DependencyProperty存储方式详解的更多相关文章

  1. Android开发笔记之: 数据存储方式详解

    无论是神马平台,神马开发环境,神马软件程序,数据都是核心.对于开发平台来讲,如果对数据的存储有良好的支持,那么对应用程序的开发将会有很大的促进作用.总体的来讲,数据存储方式有三种:一个是文件,一个是数 ...

  2. JavaScript中定义类的方式详解

    本文实例讲述了JavaScript中定义类的方式.分享给大家供大家参考,具体如下: Javascript本身并不支持面向对象,它没有访问控制符,它没有定义类的关键字class,它没有支持继承的exte ...

  3. [转]VirtualBox中的网络连接方式详解

    如果出现主机无法ping通虚拟机的情况,请首先确认虚拟机防火墙已关闭. 一.NAT模式 特点: 1.如果主机可以上网,虚拟机可以上网 2.虚拟机之间不能ping通 3.虚拟机可以ping通主机(此时p ...

  4. Python中Paramiko协程方式详解

    什么是协程 协程我们可以看做是一种用户空间的线程. 操作系统对齐存在一无所知,需要用户自己去调度. 比如说进程,线程操作系统都是知道它们存在的.协程的话是用户空间的线程,操作系统是不知道的. 为什么要 ...

  5. Android笔记——Android中数据的存储方式(三)

    Android系统集成了一个轻量级的数据库:SQLite,所以Android对数据库的支持很好,每个应用都可以方便的使用它.SQLite作为一个嵌入式的数据库引擎,专门适用于资源有限的设备上适量数据存 ...

  6. JAVA中的四种JSON解析方式详解

    JAVA中的四种JSON解析方式详解 我们在日常开发中少不了和JSON数据打交道,那么我们来看看JAVA中常用的JSON解析方式. 1.JSON官方 脱离框架使用 2.GSON 3.FastJSON ...

  7. MySQL数据库的各种存储引擎详解

    原文来自:MySQL数据库的各种存储引擎详解   MySQL有多种存储引擎,每种存储引擎有各自的优缺点,大家可以择优选择使用: MyISAM.InnoDB.MERGE.MEMORY(HEAP).BDB ...

  8. 多表连接的三种方式详解 hash join、merge join、 nested loop

    在多表联合查询的时候,如果我们查看它的执行计划,就会发现里面有多表之间的连接方式.多表之间的连接有三种方式:Nested Loops,Hash Join 和 Sort Merge Join.具体适用哪 ...

  9. [ 转载 ] Java开发中的23种设计模式详解(转)

    Java开发中的23种设计模式详解(转)   设计模式(Design Patterns) ——可复用面向对象软件的基础 设计模式(Design pattern)是一套被反复使用.多数人知晓的.经过分类 ...

随机推荐

  1. 流程引擎的API和服务基础

    RepositoryService :  管理和控制 发布包 和 流程定义(包含了一个流程每个环节的结构和行为) 的操作 除此之外,服务可以 查询引擎中的发布包和流程定义. 暂停或激活发布包,对应全部 ...

  2. 梯度下降之随机梯度下降 -minibatch 与并行化方法

    问题的引入: 考虑一个典型的有监督机器学习问题,给定m个训练样本S={x(i),y(i)},通过经验风险最小化来得到一组权值w,则现在对于整个训练集待优化目标函数为: 其中为单个训练样本(x(i),y ...

  3. 【英语】Bingo口语笔记(29) - Run系列

  4. 安装Oracle 11g RAC R2 之Linux DNS 配置

    Oracle 11g RAC 集群中引入了SCAN(Single Client Access Name)的概念,也就是指集群的单客户端访问名称.SCAN 这个特性为客户端提供了单一的主机名,用于访问集 ...

  5. Android Studio Check for Update

    Android Studio 当前版本1.0.1, 官网新版本1.1.0, 通过 Check for Update...升级, 提示 Connection failed. Please check y ...

  6. linux 下安装flash player

    或者直接下载:i386系统wget http://linuxdownload.adobe.com/adobe-release/adobe-release-i386-1.0-1.noarch.rpmrp ...

  7. ubuntu 查看系统版本信息

    查看cpu信息cat /proc/cpiinfo 查看ubuntu版本:cat /etc/issue 查看系统是32位还是64位方法1:#查看long的位数,返回32或64 getconf LONG_ ...

  8. iostat的深入理解

    问题背景 iostat -xdm 1 通常用来查看机器磁盘IO的性能. 我们一般会有个经验值,比如,ioutil要小于80%, svctm要小于2ms. 前几天碰到一个奇怪的现象:有一台SSD机器,磁 ...

  9. 使用python三方库xlrd解析excel数据

    excel是平常用的比较多的一种数据格式,而在自动化测试过程中,解析其数据以供脚本使用就是一个重要的工作,幸好已有现存的三方库供使用,而不必重新造轮子. 一.安装xlrd模块 到python官网下载h ...

  10. [转]linux 如何改变文件属性与权限

    转自:http://www.cnblogs.com/yangjinjin/p/3165076.html 我们知道档案权限对于一个系统的安全重要性,也知道档案的权限对于使用者与群组的相关性, 那如何修改 ...