Windows Presentation Foundation (WPF) 提供了一组服务,这些服务可用于扩展公共语言运行时 (CLR) 属性的功能,这些服务通常统称为 WPF 属性系统。由 WPF 属性系统支持的属性称为依赖项属性。

  这段是MSDN上对依赖属性(DependencyProperty)的描述。主要介绍了两个方面,WPF中提供了可用于扩展CLR属性的服务;被这个服务支持的属性称为依赖属性。

  单看描述,云里雾里的,了解一个知识,首先要知道它产生的背景和为什么要有它,那么WPF引入依赖属性是为了解决什么问题呢?

从属性说起

  属性是我们很熟悉的,封装类的字段,表示类的状态,编译后被转化为get_,set_方法,可以被类或结构等使用。 一个常见的属性如下:

   1: public class NormalObject
   2: {
   3:     private string _unUsedField;
   4:  
   5:     private string _name;
   6:     public string Name
   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: public class DependencyProperty
   2: {
   3:     internal static Dictionary<object, DependencyProperty> RegisteredDps = new Dictionary<object, DependencyProperty>();
   4:     internal string Name;
   5:     internal object Value;
   6:     internal object HashCode;
   7:  
   8:     private DependencyProperty(string name, Type propertyName, Type ownerType, object defaultValue)
   9:     {
  10:         this.Name = name;
  11:         this.Value = defaultValue;
  12:         this.HashCode = name.GetHashCode() ^ ownerType.GetHashCode();
  13:     }
  14:  
  15:     public static DependencyProperty Register(string name, Type propertyType, Type ownerType, object defaultValue)
  16:     {
  17:         DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, defaultValue);
  18:         RegisteredDps.Add(dp.HashCode, dp);
  19:         return dp;
  20:     }
  21: }
  22:  

DependencyObject:

   1: public class DependencyObject
   2: {
   3:     private string _unUsedField;
   4:  
   5:     public static readonly DependencyProperty NameProperty = 
   6: DependencyProperty.Register("Name", typeof(string), typeof(DependencyObject), string.Empty);
   7:  
   8:     public object GetValue(DependencyProperty dp)
   9:     {
  10:         return DependencyProperty.RegisteredDps[dp.HashCode].Value;
  11:     }
  12:  
  13:     public void SetValue(DependencyProperty dp, object value)
  14:     {
  15:         DependencyProperty.RegisteredDps[dp.HashCode].Value = value;
  16:     }
  17:  
  18:     public string Name
  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查看:

.load sos
extension C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded
!DumpHeap -stat -type NormalObject
total 1 objects
Statistics:
MT Count TotalSize Class Name
009e30d0 1 DPDemonstration.NormalObject
Total 1 objects
!DumpHeap -stat -type DependencyObject
total 1 objects
Statistics:
MT Count TotalSize Class Name
009e31a0 1 DPDemonstration.DependencyObject
Total 1 objects

  这里在对象中分别建立了一个_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: public class DependencyObject
   2: {
   3:     private List<EffectiveValueEntry> _effectiveValues = new List<EffectiveValueEntry>();
   4:  
   5:     public static readonly DependencyProperty NameProperty = 
   6: DependencyProperty.Register("Name", typeof(string), typeof(DependencyObject), string.Empty);
   7:  
   8:     public object GetValue(DependencyProperty dp)
   9:     {
  10:         EffectiveValueEntry effectiveValue = _effectiveValues.FirstOrDefault((i) => i.PropertyIndex == dp.Index);
  11:         if (effectiveValue.PropertyIndex != 0)
  12:         {
  13:             return effectiveValue.Value;
  14:         }
  15:         else
  16:         {
  17:             return DependencyProperty.RegisteredDps[dp.HashCode].Value;
  18:         }
  19:     }
  20:  
  21:     public void SetValue(DependencyProperty dp, object value)
  22:     {
  23:         EffectiveValueEntry effectiveValue = _effectiveValues.FirstOrDefault((i) => i.PropertyIndex == dp.Index);
  24:         if (effectiveValue.PropertyIndex != 0)
  25:         {
  26:             effectiveValue.Value = value;
  27:         }
  28:         else
  29:         {
  30:             effectiveValue = new EffectiveValueEntry() { PropertyIndex = dp.Index, Value = value };
  31:             _effectiveValues.Add(effectiveValue);
  32:         }
  33:     }
  34:  
  35:     public string Name
  36:     {
  37:         get
  38:         {
  39:             return (string)GetValue(NameProperty);
  40:         }
  41:         set
  42:         {
  43:             SetValue(NameProperty, value);
  44:         }
  45:     }
  46: }

新引进的EffectiveValueEntry:

   1: internal struct EffectiveValueEntry
   2: {
   3:     internal int PropertyIndex { get; set; }
   4:  
   5:     internal object Value { get; set; }
   6: }

改进的DependencyProperty,加入了ProperyIndex:

   1: public class DependencyProperty
   2: {
   3:     private static int globalIndex = 0;
   4:     internal static Dictionary<object, DependencyProperty> RegisteredDps = new Dictionary<object, DependencyProperty>();
   5:     internal string Name;
   6:     internal object Value;
   7:     internal int Index;
   8:     internal object HashCode;
   9:  
  10:     private DependencyProperty(string name, Type propertyName, Type ownerType, object defaultValue)
  11:     {
  12:         this.Name = name;
  13:         this.Value = defaultValue;
  14:         this.HashCode = name.GetHashCode() ^ ownerType.GetHashCode();
  15:     }
  16:  
  17:     public static DependencyProperty Register(string name, Type propertyType, Type ownerType, object defaultValue)
  18:     {
  19:         DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, defaultValue);
  20:         globalIndex++;
  21:         dp.Index = globalIndex;
  22:         RegisteredDps.Add(dp.HashCode, dp);
  23:         return dp;
  24:     }
  25: }

  在DependencyObject加入了一个_effectiveValues,就是把所有修改过的DP都保存在EffectiveValueEntry里,这样,就可以达到只保存修改的属性,未修改过的属性仍然读取DP的默认值,优化了属性的储存。

更进一步的发展

  到目前为止,从属性到依赖属性的改造一切顺利。但随着实际的使用,又一个问题暴露出来了。使用继承,子类可以重写父类的字段,换句话说,这个默认值应该是可以子类化的。那么怎么处理,子类重新注册一个DP,传入新的默认值?

  当然,不会实现的这么丑陋。同一个DP,要想支持不同的默认值,那么内部就要维护一个对应不同DependencyObjectType的一个List,可以根据传入的DependencyObject的类型来读取它对应的默认值。

  DP内需要维护一个自描述的List,按照微软的命名规则,添加新的类型属性元数据(PropertyMetadata):

   1: public class PropertyMetadata
   2: {
   3:     public Type Type { get; set; }
   4:     public object Value { get; set; }
   5:  
   6:     public PropertyMetadata(object defaultValue)
   7:     {
   8:         this.Value = defaultValue;
   9:     }
  10: }

对应修改DependencyProperty

   1: public class DependencyProperty
   2: {
   3:     private static int globalIndex = 0;
   4:     internal static Dictionary<object, DependencyProperty> RegisteredDps = new Dictionary<object, DependencyProperty>();
   5:     internal string Name;
   6:     internal object Value;
   7:     internal int Index;
   8:     internal object HashCode;
   9:     private List<PropertyMetadata> _metadataMap = new List<PropertyMetadata>();
  10:     private PropertyMetadata _defaultMetadata;
  11:  
  12:     private DependencyProperty(string name, Type propertyName, Type ownerType, object defaultValue)
  13:     {
  14:         this.Name = name;
  15:         this.Value = defaultValue;
  16:         this.HashCode = name.GetHashCode() ^ ownerType.GetHashCode();
  17:  
  18:         PropertyMetadata metadata = new PropertyMetadata(defaultValue) { Type = ownerType };
  19:         _metadataMap.Add(metadata);
  20:         _defaultMetadata = metadata;
  21:     }
  22:  
  23:     public static DependencyProperty Register(string name, Type propertyType, Type ownerType, object defaultValue)
  24:     {
  25:         DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, defaultValue);
  26:         globalIndex++;
  27:         dp.Index = globalIndex;
  28:         RegisteredDps.Add(dp.HashCode, dp);
  29:         return dp;
  30:     }
  31:  
  32:     public void OverrideMetadata(Type forType, PropertyMetadata metadata)
  33:     {
  34:         metadata.Type = forType;
  35:         _metadataMap.Add(metadata);
  36:     }
  37:  
  38:     public PropertyMetadata GetMetadata(Type type)
  39:     {
  40:         PropertyMetadata medatata = _metadataMap.FirstOrDefault((i) => i.Type == type) ??
  41:             _metadataMap.FirstOrDefault((i) => type.IsSubclassOf(i.Type));
  42:         if (medatata == null)
  43:         {
  44:             medatata = _defaultMetadata;
  45:         }
  46:         return medatata;
  47:     }
  48: }

修改DenpendencyObject中的GetValue并更改_effectiveValues,为了简洁去掉了NameProperty以及SetValue.

   1: public class DependencyObject
   2: {
   3:    private List<EffectiveValueEntry> _effectiveValues = new List<EffectiveValueEntry>();
   4:  
   5:    public object GetValue(DependencyProperty dp)
   6:    {
   7:        EffectiveValueEntry effectiveValue = _effectiveValues.FirstOrDefault((i) => i.PropertyIndex == dp.Index);
   8:        if (effectiveValue.PropertyIndex != 0)
   9:        {
  10:            return effectiveValue.Value;
  11:        }
  12:        else
  13:        {
  14:            PropertyMetadata metadata;
  15:            metadata = DependencyProperty.RegisteredDps[dp.HashCode].GetMetadata(this.GetType());
  16:            return metadata.Value;
  17:        }
  18:    }
  19: }

这样,就可以定义一个SubDependencyObject,调用OverrideMedata向DP的_metadataMap中加入新的Metadata。

   1: public class SubDependencyObject : DependencyObject
   2: {
   3:     static SubDependencyObject()
   4:     {
   5:         NameProperty.OverrideMetadata(typeof(SubDependencyObject), new PropertyMetadata("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,现在定义多个值就可以了。

   1: internal struct EffectiveValueEntry
   2: {
   3:     private object _value;
   4:  
   5:     internal int PropertyIndex { get; set; }
   6:  
   7:     internal object Value 
   8:     {
   9:         get
  10:         {
  11:             return _value;
  12:         }
  13:         set
  14:         {
  15:             _value = value;
  16:         }
  17:     }
  18:  
  19:     internal ModifiedValue ModifiedValue 
  20:     {
  21:         get
  22:         {
  23:             if (this._value != null)
  24:             {
  25:                 return (this._value as ModifiedValue);
  26:             }
  27:             return null;
  28:         }
  29:     }
  30: }

对应的ModifiedValue:

   1: internal class ModifiedValue
   2: {
   3:     internal object AnimatedValue { get; set; }
   4:     internal object BaseValue { get; set; }
   5:     internal object CoercedValue { get; set; }
   6:     internal object ExpressionValue { get; set; }
   7: }

  当属性没有被修改过,ModifiedValue为空,当修改过后,ModifiedValue被赋值。这里EffectiveValueEntry定义了很多方法如SetExpressionValue(object value), SetAnimatedValue(object value)等来向ModifiedValue中写入对应值;并且EffectiveValueEntry提供了IsAnimated,IsExpression等属性来表示当前的状态。当然,这个赋值的操作比较复杂,这个优先级分两大类:一 ModifiedValue中各属性的优先级;二 对于ExpressionValue来说,它又有自己的优先级,Local>Style>Template…这里就不详细解释了。

依赖属性的优点

  回过头来,总结一下依赖属性的优点:

  • 优化了属性的储存,减少了不必要的内存使用。
  • 加入了属性变化通知,限制、验证等,
  • 可以储存多个值,配合Expression以及Animation等,打造出更灵活的使用方式。

总结

  借助于依赖属性,WPF提供了强大的属性系统,可以支持数据绑定、样式、动画、附加属性等功能。这篇文章主要是简略的实现了一个从属性到依赖属性的发展过程,当然,具体和WPF的实现还有偏差,希望朋友们都能抓住这个主要的脉络,更好的去玩转它。

  除了依赖属性的实现,还有一些很重要的部分,比如借助于依赖属性提出的附加属性,以及如何利用依赖属性来更好的设计实现程序,使用依赖属性有哪些要注意的地方。呵呵,那就,下篇吧。

示例程序下载

作者:周永恒 
出处:http://www.cnblogs.com/Zhouyongh  
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

一站式WPF--依赖属性(DependencyProperty)一的更多相关文章

  1. WPF依赖属性DependencyProperty

    写在之前: 依赖属性算是WPF醉醉基础的一个组成了.平时写代码的时候,简单的绑定很轻松,但是遇到复杂的层次比较多的绑定,真的是要命.所以,我觉得深刻认识依赖属性是很有必要的.本篇只是个人学习的记录,学 ...

  2. WPF 依赖属性源码 洞察微软如何实现DependencyProperty

    依赖属性DependencyProperty是wpf最重要的一个类,理解该类如何实现对学习wpf帮助很大! 终于找到了该类的源码!仔细阅读源码,看看微软如何玩的花招! File: Base\Syste ...

  3. WPF 使用依赖属性(DependencyProperty) 定义用户控件中的Image Source属性

    原文:WPF 使用依赖属性(DependencyProperty) 定义用户控件中的Image Source属性 如果你要自定义一个图片按钮控件,那么如何在主窗体绑定这个控件上图片的Source呢? ...

  4. WPF依赖属性详解

    WPF依赖属性详解 WPF 依赖属性 英文译为 Dependency Properties,是WPF引入的一种新类型的属性,在WPF中有着极为广泛的应用,在WPF中对于WPF Dependency P ...

  5. WPF自学入门(五)WPF依赖属性

    在.NET中有事件也有属性,WPF中加入了路由事件,也加入了依赖属性.最近在写项目时还不知道WPF依赖属性是干什么用的,在使用依赖项属性的时候我都以为是在用.NET中的属性,但是确实上不是的,通过阅读 ...

  6. WPF依赖属性值源(BaseValueSource)

    原文:WPF依赖属性值源(BaseValueSource)   WPF依赖属性提供一个机制,可以获取依赖属性提供值的来源 其以BaseValueSource枚举表示 1.Default public ...

  7. WPF依赖属性(续)(3)依赖属性存储

    原文:WPF依赖属性(续)(3)依赖属性存储          在之前的两篇,很多朋友参与了讨论,也说明各位对WPF/SL计数的热情,对DP系统各抒已见,当然也出现了一些分歧. 以下简称DP为依赖属性 ...

  8. WPF依赖属性(续)(2)依赖属性与附加属性的区别

    原文:WPF依赖属性(续)(2)依赖属性与附加属性的区别        接上篇,感谢各位的评论,都是认为依赖属性的设计并不是为了节省内存,从大的方面而讲是如此.样式,数据绑定,动画样样都离不开它.这篇 ...

  9. WPF依赖属性的正确学习方法

    前言 我在学习WPF的早期,对依赖属性理解一直都非常的不到位,其恶果就是,我每次在写依赖属性的时候,需要翻过去的代码来复制黏贴. 相信很多朋友有着和我相同的经历,所以这篇文章希望能帮助到那些刚刚开始学 ...

  10. WPF依赖属性(续)(1)

    原文:WPF依赖属性(续)(1)                 之前有写过几篇文章,详细地介绍了依赖属性的基本使用方法,如果你不想了解其内部实现机制的话,那么通过那两篇文章的介绍,足以应付平时的应用 ...

随机推荐

  1. win10系统,vbox下安装centos6/7,挂载实现目录共享

    用下载好的iso文件,新建虚拟机(所有步骤默认下一步即可). 我用的centos版本:CentOS-7-x86_64-Minimal-1908.iso ISO下载地址 设置虚拟机(指定好镜像后,先不要 ...

  2. System.Web.Mvc.HttpUnauthorizedResult.cs

    ylbtech-System.Web.Mvc.HttpUnauthorizedResult.cs 1.程序集 System.Web.Mvc, Version=5.2.3.0, Culture=neut ...

  3. Sping中的AOP

    AOP(Aspect Oriented Programming)面向切面编程,什么是切面.形象的说,我们编写的代码都是一种有序的流程,比如产品管理,订单管理,而切面就是垂直于这些流程的. 比如日志服务 ...

  4. PageBarHelper分页显示类

    一共有两个分页类,都可以使用(单独使用) using System;using System.Collections.Generic;using System.Linq;using System.Te ...

  5. 从0开始学习ssh之岗位管理

    岗位应该有如下属性,id.名称.说明.新建role.java.添加id,name,description并增加三个的get set方法. 之后建立Role.hbm.xml 并加入到applicatio ...

  6. mysql设置密码登录

    参考: https://blog.csdn.net/Light_Breeze/article/details/82070222 https://www.jianshu.com/p/d979df2791 ...

  7. Selenium浏览器自动化测试使用(1)

    Selenium - 介绍 Selenium是一个开源的和便携式的自动化软件测试工具,用于测试Web应用程序有能力在不同的浏览器和操作系统运行.Selenium真的不是一个单一的工具,而是一套工具,帮 ...

  8. Luogu P3209 [HNOI2010]平面图判定(2-SAT)

    P3209 [HNOI2010]平面图判定 题意 题目描述 若能将无向图\(G=(V,E)\)画在平面上使得任意两条无重合顶点的边不相交,则称\(G\)是平面图.判定一个图是否为平面图的问题是图论中的 ...

  9. deepfake安装python常用命令

    pip install -r requirements.txt -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/ python -m p ...

  10. HZOI20190906模拟38 金,斯诺,赤

    题面:https://www.cnblogs.com/Juve/articles/11479415.html T1:高精度gcd,其实不用写高精度取模,gcd还有一种求法 int gcd(int a, ...