码迷,mamicode.com
首页 > Windows程序 > 详细

WPF基础到企业应用系列7——深入剖析依赖属性(WPF/Silverlight核心)

时间:2015-07-22 13:10:07      阅读:289      评论:0      收藏:0      [点我收藏+]

标签:.net

一. 摘要

  首先圣殿骑士很高兴这个系列能得到大家的关注和支持,这个系列从七月份开始到现在才第七篇,上一篇发布是在8月2日,掐指一算有二十多天没有继续更新了,最主要原因一来是想把它写好,二来是因为最近几个月在筹备“云计算之旅”系列,所以一再推迟了发布进度。之前一直都没有想过要录制视频,主要的原因还是怕自己知识有限,从而误导他人,所以前几次浪曦和51CTO邀请录制视频,我都以工作忙、公司内部培训需要时间和自己有待提高等理由委婉的拒绝了,说实在的,自己也知道自己还有很多地方有待提高,还需要向各位学习,所以这几年都会一直努力,相信总有一天自己头上也会长出两只角的。录制视频的事也就这样不了了之了,直到前一段时间MSDN WebCast的再三邀请,我才决定努力试一试,同时也希望各位能够支持,现在都把社区当成自己的坚强后盾了,所以我打算先以博客的形式发布,这样就可以先和大家一起讨论,纠正自己的某些错误认识,这样在录制视频的时候就不会误导他人了。

  前几篇我们讲了WPF的一些基本知识,但是始终没有接触最核心的概念,那么从这篇文章开始的下面几篇文章中,我们会分别深入讨论一下依赖属性、路由事件、命令和绑定等相关概念,希望这几篇文章对大家能有所帮助。由于自己才疏学浅且是对这些技术的使用总结和心得体会,错误之处在所难免,怀着技术交流的心态,在这里发表出来,所以也希望大家能够多多指点,这样在使一部分人受益的同时也能纠正我的错误观点,以便和各位共同提高。

  这篇文章比较多,在开篇之前我们会先介绍比本篇更重要的一些东西,然后插播一段“云计算之旅”的广告(技术分享 这里广告费比较贵哟!),作为最近几个月执着研究的东西,终于可以和大家见面了,希望自己能从实践中深入浅出的讲明白。在前面的两个内容之后我们正式进入本篇的主题——依赖属性。依赖属性是WPF的核心概念,所以我们花费了大量的时间和篇幅进行论述,首先从依赖属性基本介绍讲起,然后过渡到依赖属性的优先级、附加属性、只读依赖属性、依赖属性元数据、依赖属性回调、验证及强制值、依赖属性监听、代码段(自动生成) 等相关知识,最后我们会模拟一个WPF依赖属性的实现,来看看它里面的内部究竟是怎样处理的,这样就可以帮助我们更好的认清它的本质,出现问题的时候我们也可以根据原理快速找到原因了。

二. 本文提纲

· 1.摘要

· 2.本文提纲

· 3.比这篇文章更重要的东西

· 4.云计算广告插播

· 5.依赖属性基本介绍

· 6.依赖属性的优先级

· 7.依赖属性的继承

· 8.只读依赖属性

· 9.附加属性

· 10.清除本地值

· 11.依赖属性元数据

· 12.依赖属性回调、验证及强制值

· 13.依赖属性监听

· 14.代码段(自动生成)

· 15.模拟依赖属性实现

· 16.本文总结

· 17.相关代码下载

· 18.系列进度

三. 比这篇文章更重要的东西

  在讲这篇文章之前,我们先来聊一点更重要的东西,正所谓“授人与鱼不如授人以渔”,那么我们这个“渔”究竟是什么呢?大家做软件也有不少年了,对自己擅长的一门或多门技术都有自己的经验和心得,但总的来说可以分为向内和向外以及扩展三个方面(这里只针对.NET平台):

(一)向外:

  会使用ASP.NET、WinForm、ASP.NET MVC、WPF、Silverlight、WF、WCF等技术,在用这些技术做项目的同时积累了较丰富的经验,那么大家就可以形成自己的一套开发知识库,知道这些技术怎么能快速搭建企业所需要的应用、知道这些技术在使用中会出现什么样的问题以及如何解决。那么在这个时候你就可能已经在团队中起到比较核心的作用,如果项目经理给你一个任务,你也可以很轻松且高效的胜任,同时在项目当中由于你也比较清楚业务逻辑,所以当机会来临的时候,你很快会成为团队的骨干,逐渐你就会带几个初级一点的工程师一起做项目;如果你不喜欢带团队,你就会成为资深的高级开发或者架构师。那么在向外方面我个人认为最重要的是积累经验,对常见的应用要比较熟悉且有自己总结的一套开发库——比如对普通的网站、电子商务系统、ERP、OA、客户端应用等等有比较丰富的经验。

(二)向内:

  在前面你使用了这些技术开发项目之后,你会遇到很多问题,为了解决这些问题,你会逐渐研究一些比较底层次的东西。比如对于ASP.NET,你会逐渐的去深入理解ASP.NET的整个处理过程、页面的生命周期、自定义控件的开发等等,由于自己最初是由C、C++、Java这样过渡到.NET的,所以对一些细节总喜欢钻牛角尖,这也浪费了不少时间,但同时也得到了很多意外之喜。

对于C#语言上,你也会逐渐去刨根问底,想看看这些语法糖背后到底隐藏着什么秘密,很多对.NET技术比较痴迷的人都会选择对C# 1.0 语言通过IL代码来深层次认识,然后对C#2.0、C#3.0、C#4.0都编译为C# 1.0 来学习,这样他们就能认识到语言的内部到底是怎么执行的,正所谓知道的同时也知道其所以然。

对于WF,你不仅要知道各 Activity的使用,你也得知道其内部的原理,比如WF 内部就是依靠依赖属性来在工作流中的各 Activity 间传递属性值的,如果你细心,你还原WF的依赖属性源码,你会发现它和WPF、Silverlight中的依赖属性大同小异,原理基本一样、只是针对特定技术进行了适当的调整。

对于数据底层操作也一样,不管你是用的拼接SQL、存储过程、IBATIS.NET、Nhibernate,Active Record,Linq to sql、Entity framework还是自己开发的ORM组件,你得明白它内部的原理,你要知道这些开源的代码还是很值得研究的,我的经验是先熟练使用这些功能,然后再剖析它的源码,然后自己写一套自己的框架,现在我也在精简自己的ORM框架,因为之前把重点放在了实现尽可能多的功能,所以对性能等细节没有做过多优化,后面也会向大家慢慢学习。

对WPF和Silverlight一样,你不仅要知道怎么用这些技术,你要知道它的原理,比如对依赖属性,你知道它的内部原理,就可以对平时出现的诸如我设置的值怎么没有起作用、我Binding的元素怎么没有出现等等问题; 对路由事件,你也会经常遇到我的事件怎么没有执行、我的自定义控件事件怎么处理不对、路由传递怎么没有起作用等等,这个时候你如果深入理解了路由事件的内部处理,这些问题就迎刃而解了;对WPF和Silverlight新多出来的命令特性,大家很多时候也是比较疑惑,也会遇到命令失效等等问题,其实如果你深入的了解了它的原理,你就会知道,它其实在内部也是事件,只不过微软在里面做了很多封装而已;对Binding就更是如此,我们也不想在这篇文章谈开去,毕竟在下面的几篇文章会详细的对这些技术进行涉及。

(三)扩展:

  通过前面的向内和向外的修炼以后,接下来要做的就是不断实践,不断总结经验,在这个过程中更重要的是要懂得分享,有分享才会使自己和他人共同提高,有分享才能让自己摆脱狂妄的井底之蛙思想,还记得自己刚做技术的一两年里,天天喜欢提及大型架构、大型数据处理、操作系统底层代码如何如何,甚至把AOP、IOC、SSH、OO及设计模式、SOA等词语时常挂在嘴边,生怕别人不知道自己不懂。但随着自己技术实质上的提高以及经验的积累,自己也就逐渐成熟起来,对这些技术逐渐深入理解且理解了其内部实现原理,这样反而自己变得谦虚起来了,对之前的那些思想感到无比的羞愧。同时也明白自己在慢慢成长了,现在都习惯戏称自己为打杂工,其实更多时候用打字员会合理一些,所以希望大家能多多指教,这样我才能更快地摆脱打字员的生活。我在这里也对扩展做一点小的总结:

记录学习:这是学习很重要的一步,你不一定要写技术博客,你也可以做一些例子来记录,你也可以在学习之后写一个总结,毕竟人的精力十分有限,在很多时候,它并不能像硬盘一样存储起来就不会丢失,更多的时候它更像一块内存。

同道交流:在这一层里我觉得最重要的就是和一些技术较好的人成为朋友,和他们经常探讨一些技术,这样可以缩短学习的周期,同时也能快速的解决问题,毕竟人的精力十分有限,你没有遇到过的问题,说不定其他人遇到过。在这方面自己也体会颇深,也很感谢之前几个公司及现在公司的同事、社区朋友以及一些志同道合之士,感谢你们的指点,没有你们的指点,我也不可能从小鸟进化成逐鹿程序界的菜鸟,我也为自己能成为一只老菜鸟感到自豪!

少考证、多务实:在扩展的这一层里,我们要谨记不要为了考证而去考证,那样是没有任何实际作用的。对MVP也一样,一切顺其自然为好,记得大学时候身边就有人连续四次荣获MVP称号,这叫我在当时是相当的佩服,在佩服之余我们要切记务实,没有务实的东西都是很虚拟飘渺的。还记得自己当初在大学里面受到考证风气的影响,神经兮兮的去考过了什么国家计算机四级和MCP等一大堆证件,后来到公司面试兴高采烈拿着20多张证书,才知道那些东西根本就没有什么价值,反而让自己去学习了最不喜欢的技术,同时也给自己挂上了考证族的名号。所以后来总结就是劳民伤财、徒添伤悲!

技术分享:在自己公司及其他公司进行一些技术培训或者讨论,其实重要的不是什么荣誉,而是在把这个培训看成是一些技术交流和分享,因为在这个过程中,你可能会重新认识你所掌握的技术、你可能会遇到一些志同道合的人、你可能会在分享过程中纠正以前的错误认识、你可能会在各方面得到提高从而完善自己的知识体系,但是最重要的是你要认真对待每一次培训,知之为知之不知为不知,不要不能教导他人反而误导了他人。记得有一次在公司培训OO与设计模式,我知道这个专题想在一下午的时间把它讲清楚是非常困难的,这个不像之后培训的WPF、WCF和Silverlight那么单纯,并且每个人的基础都不一样,当中有还没有毕业的实习生、刚毕业不久的毕业生、工作了数年的工程师及技术大牛们,所以如何把这些知识很好的插入到每个人的知识树上面成了我考虑的重点。同时我的心里也比较矛盾,一方面希望参加培训的同事多一些,另一方面希望人越少越好。前者则是按照常理来考虑的,毕竟培训者都希望自己培训,越受欢迎越好,这样才能使自己的思想得到更多人的认可,自己也能实现分享知识的目的。后者则是担心怕讲不好,少一点人就少一点罪过。可是恰巧这一次是历次培训中最多的一次,来参加培训的同事有一百多人,不过幸好由于会议室坐不下,才分成了两批,这样就可以让我具备了更充分的时间和更好的心态。总之培训是向内和向外的提炼与升华,正所谓“自己理解的知识未必能使人家理解”,这不仅考验的是技术,还考验了一个人的综合能力。

(四)结论:

  前面从向内和向外以及扩展三个方面进行了简单阐述,用一句话概括就是:向内深不可测、向外漫无边际、扩展才能超越极限。由于这里只是对本文及下面的三篇文章做一些铺垫工作,所以我们也不细细分解,那么我也得稍微推荐一点资料才对得起大家:第一还是研究微软的类库,对我们常见的应用进行研究,可以结合Reflector+VS调试内部代码功能一起研究(IL能帮我们看清楚一些内部原理,但是不推荐细究,因为它会浪费我们很多时间,毕竟是微软搞出来的这么一套东西,说不定微软哪天就换了)。其次就是研究MONO源码(www.mono-project.com),这个是个非常好的东西,对.NET的功能大部分都进行了实现,我之前研究它不是因为它的跨平台,是感兴趣它的源码,大家也可以在线查看它的源码(www.java2s.com),说到java2s这个网站,也是我平时去得比较多的网站,因为它比较全面和方便,同时也会给我们带来意想不到的收获。再其次就是研究一些开源的框架和项目,比如pet shop 4.0(http://software.informer.com/getfree-net-pet-shop-4.0-download/)、BlogEngine.NET(http://www.dotnetblogengine.net/)、Spring.NET(http://www.springframework.net/)、Castle(http://www.castleproject.org)、log4net(http://logging.apache.org/log4net/)、NHibernate(http://www.hibernate.org/343.html)、iBATIS.NET(http://ibatis.apache.org)、Caliburn(http://caliburn.codeplex.com/)、MVVM Light Toolkit(http://mvvmlight.codeplex.com/)、Prism(http://compositewpf.codeplex.com/)等等。这里要注意的是:在研究的过程中一定要先熟悉功能,再研究它内部的源码和实现,然后再创造出自己的框架。这样才能激发我们研究的欲望,才会产生作用和反作用力,从而才会使我们真正受益。

四. 云计算广告插播

  由于这段时间白天要研究云计算专题(公司项目原因,最主要还是自己的兴趣使然),晚上和闲暇时间又要写WPF,所以感觉有点心猿意马。原打算写完WPF这个系列以后才继续”云计算之旅“这个系列,但是经过慎重的思考,同时也考虑到录制MSDN WebCast视频,所以决定两个系列同时进行,经过几个月的筹备(期间包括折腾公司的云计算项目、研究相关云计算的电子书、国外技术视频和国外各技术社区和博客等),自己也颇有收获。期间最重要的还是自己写例子,写完了以后再分析它的原理直至最后总结,这样才能把它变成自己的东西,现在回过头来感觉云计算终于在自己心目中走下了神坛,逐渐揭开了那一层神秘面纱,所以才有下面这个系列的分享,也希望大家能给出建议,从而达到技术交流、共同提高的目的。

云计算之旅1—开篇有益

云计算之旅2—云计算总览

云计算之旅3—云计算提供商综合对比

云计算之旅4—Windows Azure总览

云计算之旅5—第一个Windows Azure程序

云计算之旅6—剖析Windows Azure程序内部原理

云计算之旅7—ASP.NET Web Role

云计算之旅8—ASP.NET MVC Web Role

云计算之旅9—WCF Service Web Role

云计算之旅10—Work Role Castle

云计算之旅11—CGI Web Role

云计算之旅12—云存储之Blob

云计算之旅13—云存储之Table

云计算之旅14—云存储之Quee

云计算之旅15—云存储之Dive

云计算之旅16—SQL Azure(一)

云计算之旅17—SQL Azure(二)

云计算之旅18—SQL Azure(三)

云计算之旅19—AppFabric(一)

云计算之旅20—AppFabric(二)

云计算之旅21—AppFabric(三)

云计算之旅22—云平台安全问题

云计算之旅23—老技术兼容问题

云计算之旅24—ASP.NET+SQL项目移植到云平台

云计算之旅25—WinForm/WPF项目移植到云平台(云/端模式)

云计算之旅26—ASP.NET+Silverlight项目移植到云平台

云计算之旅27—Amazon云计算

云计算之旅28—Google云计算

云计算之旅29—SalesForce云计算

云计算之旅30—云计算开发总结

  上面的分类是按照最近学习的总结归类的,在这几个月中也先后写了一些文章和代码示例,同时有些知识没有罗列上去,在后面可能会有一些小的修改。总之,我们坚决抵制”忽悠“,争取以实际代码说话,在此过程中希望大家能够积极踊跃的加入进来,如果有什么不对的地方,也希望向大家学习,最重要的是大家有所收获就好!

五. 依赖属性基本介绍

  前面废话了这么久,到现在才真正进入今天的主题,对此感到非常抱歉,如果各位不喜欢,可以直接跳到这里阅读。大家都知道WPF带来了很多新的特性,它的一大亮点是引入了一种新的属性机制——依赖属性。依赖属性基本应用在了WPF的所有需要设置属性的元素。依赖属性根据多个提供对象来决定它的值(可以是动画、父类元素、绑定、样式和模板等),同时这个值也能及时响应变化。所以WPF拥有了依赖属性后,代码写起来就比较得心应手,功能实现上也变得非常容易了。如果没有依赖属性,我们将不得不编写大量的代码。关于WPF的依赖属性,主要有下面三个优点,我们的研究也重点放在这三点上: 
1、新功能的引入:加入了属性变化通知,限制、验证等等功能,这样就可以使我们更方便的实现我们的应用,同时也使代码量大大减少了,许多之前不可能的功能都可以轻松的实现了。 
2、节约内存:在WinForm等项目开发中,你会发现UI控件的属性通常都是赋予的初始值,为每一个属性存储一个字段将是对内存的巨大浪费。WPF依赖属性解决了这个问题,它内部使用高效的稀疏存储系统,仅仅存储改变了的属性,即默认值在依赖属性中只存储一次。 
3、支持多个提供对象:我们可以通过多种方式来设置依赖属性的值。同时其内部可以储存多个值,配合Expression、Style、Animation等可以给我们带来很强的开发体验。 
  在.NET当中,属性是我们很熟悉的,封装类的字段,表示类的状态,编译后被转化为对应的get和set方法(在JAVA里面没有属性的概念,通常都是写相应的方法来对字段进行封装)。属性可以被类或结构等使用。 一个简单的属性如下,也是我们常用的写法:

 

private string sampleProperty;
public string SampleProperty
{
    get
    {
        return sampleProperty;
    }
    set
    {
        if (value != null)
        {
            sampleProperty = value;
        }
        else
        {
            sampleProperty = "Knights Warrior!";
        }
    }
}

属性是我们再熟悉不过的了,那么究竟依赖属性怎么写呢?依赖属性和属性到底有什么区别和联系呢?其实依赖属性的实现很简单,只要做以下步骤就可以实现: 
第一步: 让所在类型继承自 DependencyObject基类,在WPF中,我们仔细观察框架的类图结构,你会发现几乎所有的 WPF 控件都间接继承自DependencyObject类型。 
第二步:使用 public static 声明一个 DependencyProperty的变量,该变量才是真正的依赖属性 ,看源码就知道这里其实用了简单的单例模式的原理进行了封装(构造函数私有),只暴露Register方法给外部调用。 
第三步:在静态构造函数中完成依赖属性的元数据注册,并获取对象引用,看代码就知道是把刚才声明的依赖属性放入到一个类似于容器的地方,没有讲实现原理之前,请容许我先这么陈述。 
第四步:在前面的三步中,我们完成了一个依赖属性的注册,那么我们怎样才能对这个依赖属性进行读写呢?答案就是提供一个依赖属性的实例化包装属性,通过这个属性来实现具体的读写操作。

根据前面的四步操作,我们就可以写出下面的代码:

public class SampleDPClass : DependencyObject
{
    //声明一个静态只读的DependencyProperty字段
    public static readonly DependencyProperty SampleProperty;

    static SampleDPClass()
    {
        //注册我们定义的依赖属性Sample
        SampleProperty = DependencyProperty.Register("Sample", typeof(string), typeof(SampleDPClass),
            new PropertyMetadata("Knights Warrior!", OnValueChanged));
    }

    private static void OnValueChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        //当值改变时,我们可以在此做一些逻辑处理
    }

    //属性包装器,通过它来读取和设置我们刚才注册的依赖属性
    public string Sample
    {
        get { return (string)GetValue(SampleProperty); }
        set { SetValue(SampleProperty, value); }
    }
}

总结:我们一般.NET属性是直接对类的一个私有属性进行封装,所以读取值的时候,也就是直接读取这个字段;而依赖属性则是通过调用继承自DependencyObject的GetValue()和SetValue来进行操作,它实际存储在DependencyProperty的一个IDictionary的键-值配对字典中,所以一条记录中的键(Key)就是该属性的HashCode值,而值(Value)则是我们注册的DependencyProperty。

六. 依赖属性的优先级

  由于WPF 允许我们可以在多个地方设置依赖属性的值,所以我们就必须要用一个标准来保证值的优先级别。比如下面的例子中,我们在三个地方设置了按钮的背景颜色,那么哪一个设置才会是最终的结果呢?是Black、Red还是Azure呢?

<Window x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">
    <Grid>
        <Button x:Name="myButton" Background="Azure">
            <Button.Style>
                <Style TargetType="{x:Type Button}">
                    <Setter Property="Background" Value="Black"/>
                    <Style.Triggers>
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter Property="Background" Value="Red" />
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </Button.Style>
            Click
        </Button>
    </Grid>
</Window>

通过前面的简单介绍,我们了解了简单的依赖属性,每次访问一个依赖属性,它内部会按照下面的顺序由高到底处理该值。详细见下图

技术分享  

  由于这个流程图偏理想化,很多时候我们会遇到各种各样的问题,这里也不可能一句话、两句话就能够把它彻底说清楚,所以我们就不过多纠缠。等遇到问题之后要仔细分析,在找到原因之后也要不断总结、举一反三,只有这样才能逐渐提高。

七. 依赖属性的继承

  依赖属性继承的最初意愿是父元素的相关设置会自动传递给所有层次的子元素 ,即元素可以从其在树中的父级继承依赖项属性的值。这个我们在编程当中接触得比较多,如当我们修改窗体父容器控件的字体设置时,所有级别的子控件都将自动使用该字体设置 (前提是该子控件未做自定义设置),如下面的代码:

<Window x:Class="Using_Inherited_Dps.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    WindowStartupLocation="CenterScreen" 
    FontSize="20"
    Title="依赖属性的继承" Height="400" Width="578">
    <StackPanel >
        <Label Content="继承自Window的FontSize" />
        <Label Content="重写了继承" 
               TextElement.FontSize="36"/>
        <StatusBar>没有继承自Window的FontSize,Statusbar</StatusBar>
    </StackPanel>
</Window> 

  Window.FontSize 设置会影响所有的内部元素字体大小,这就是所谓的属性值继承,如上面代码中的第一个Label没有定义FontSize ,所以它继承了Window.FontSize的值。但一旦子元素提供了显式设置,这种继承就会被打断,如第二个Label定义了自己的FontSize,所以这个时候继承的值就不会再起作用了。

  这个时候你会发现一个很奇怪的问题:虽然StatusBar没有重写FontSize,同时它也是Window的子元素,但是它的字体大小却没有变化,保持了系统默认值。那这是什么原因呢?作为初学者可能都很纳闷,官方不是说了原则是这样的,为什么会出现表里不一的情况呢?其实仔细研究才发现并不是所有的元素都支持属性值继承。还会存在一些意外的情况,那么总的来说是由于以下两个方面:

1、有些Dependency属性在用注册的时候时指定Inherits为不可继承,这样继承就会失效了。

2、有其他更优先级的设置设置了该值,在前面讲的的“依赖属性的优先级”你可以看到具体的优先级别。

  这里的原因是部分控件如StatusBar、Tooptip和Menu等内部设置它们的字体属性值以匹配当前系统。这样用户通过操作系统的控制面板来修改它们的外观。这种方法存在一个问题:StatusBar等截获了从父元素继承来的属性,并且不影响其子元素。比如,如果我们在StatusBar中添加了一个Button。那么这个Button的字体属性会因为StatusBar的截断而没有任何改变,将保留其默认值。所以大家在使用的时候要特别注意这些问题。

技术分享

前面我们看了依赖属性的继承,当我们自定义的依赖属性,应该如何处理继承的关系呢? 请看下面的代码(注释很详细,我就不再费口水了):

public class MyCustomButton : Button
 {
     static MyCustomButton()
     {
         //通过MyStackPanel依赖属性MinDateProperty的AddOwner方式实现继承,注意FrameworkPropertyMetadataOptions的值为Inherits
         MinDateProperty = MyStackPanel.MinDateProperty.AddOwner(typeof(MyCustomButton),
         new FrameworkPropertyMetadata(DateTime.MinValue, FrameworkPropertyMetadataOptions.Inherits));
     }

     public static readonly DependencyProperty MinDateProperty;

     public DateTime MinDate
     {
         get { return (DateTime)GetValue(MinDateProperty); }
         set { SetValue(MinDateProperty, value); }
     }
 }


 public class MyStackPanel : StackPanel
 {
     static MyStackPanel()
     {
         //我们在MyStackPanel里面注册了MinDate,注意FrameworkPropertyMetadataOptions的值为Inherits
         MinDateProperty = DependencyProperty.Register("MinDate",
         typeof(DateTime),
         typeof(MyStackPanel),
         new FrameworkPropertyMetadata(DateTime.MinValue, FrameworkPropertyMetadataOptions.Inherits));
     }

     public static readonly DependencyProperty MinDateProperty;

     public DateTime MinDate
     {
         get { return (DateTime)GetValue(MinDateProperty); }
         set { SetValue(MinDateProperty, value); }
     }
 }

那么就可以在XAML中进行使用了

<Window x:Class="Custom_Inherited_DPs.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:Custom_Inherited_DPs" 
    xmlns:sys="clr-namespace:System;assembly=mscorlib"   
    WindowStartupLocation="CenterScreen" 
    Title="使用自动以依赖属性继承" Height="300" Width="300">
    <Grid>
        <local:MyStackPanel x:Name="myStackPanel" MinDate="{x:Static sys:DateTime.Now}">
            <!-- myStackPanel的依赖属性 -->
            <ContentPresenter Content="{Binding Path=MinDate, ElementName=myStackPanel}"/>
            <!-- 继承自myStackPanel的依赖属性 -->
            <local:MyCustomButton 
                    Content="{Binding RelativeSource={x:Static RelativeSource.Self},
                    Path=MinDate}" 
                Height="20"/>
        </local:MyStackPanel>
   </Grid>
</Window>    

最后的效果如下:

技术分享

八. 只读依赖属性

  我们以前在对简单属性的封装中,经常会对那些希望暴露给外界只读操作的字段封装成只读属性,同样在WPF中也提供了只读属性的概念,如一些WPF控件的依赖属性是只读的,它们经常用于报告控件的状态和信息,像IsMouseOver等属性, 那么在这个时候对它赋值就没有意义了。 或许你也会有这样的疑问:为什么不使用一般的.Net属性提供出来呢?一般的属性也可以绑定到元素上呀?这个是由于有些地方必须要用到只读依赖属性,比如Trigger等,同时也因为内部可能有多个提供者修改其值,所以用.Net属性就不能完成天之大任了。 
  那么一个只读依赖属性怎么创建呢?其实创建一个只读的依赖属性和创建一个一般的依赖属性大同小异(研究源码你会发现,其内部都是调用的同一个Register方法)。仅仅是用DependencyProperty.RegisterReadonly替换了DependencyProperty.DependencyProperty而已。和前面的普通依赖属性一样,它将返回一个DependencyPropertyKey。该键值在类的内部暴露一个赋值的入口,同时只提供一个GetValue给外部,这样便可以像一般属性一样使用了,只是不能在外部设置它的值罢了。

下面我们就用一个简单的例子来概括一下:

public partial class Window1 : Window
 {
     public Window1()
     {
         InitializeComponent();

         //内部用SetValue的方式来设置值
         DispatcherTimer timer =
             new DispatcherTimer(TimeSpan.FromSeconds(1),
                                 DispatcherPriority.Normal,
                                 (object sender, EventArgs e)=>
                                 {
                                     int newValue = Counter == int.MaxValue ? 0 : Counter + 1;
                                     SetValue(counterKey, newValue);
                                 },
                                 Dispatcher);
         
     }

     //属性包装器,只提供GetValue,这里你也可以设置一个private的SetValue进行限制
     public int Counter
     {
         get { return (int)GetValue(counterKey.DependencyProperty); }
     }

     //用RegisterReadOnly来代替Register来注册一个只读的依赖属性
     private static readonly DependencyPropertyKey counterKey =
         DependencyProperty.RegisterReadOnly("Counter",
                                             typeof(int),
                                             typeof(Window1),
                                             new PropertyMetadata(0));
 }

   
XAML中代码:

<Window x:Name="winThis" x:Class="WpfApplication1.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Read-Only Dependency Property" Height="300" Width="300">
    <Grid>
        <Viewbox>
            <TextBlock Text="{Binding ElementName=winThis, Path=Counter}" />
        </Viewbox>
    </Grid>
</Window>

效果如下图所示: 

技术分享

九. 附加属性

  前面我们讲了依赖属性。现在我们再继续探讨另外一种特殊的Dependency属性——附加属性。附加属性是一种特殊的依赖属性。他允许给一个对象添加一个值,而该对象可能对此值一无所知。

  最好的例子就是布局面板。每一个布局面板都需要自己特有的方式来组织它的子元素。如Canvas需要Top和left来布局,DockPanel需要Dock来布局。当然你也可以写自己的布局面板(在上一篇文章中我们对布局进行了比较细致的探讨,如果有不清楚的朋友也可以再回顾一下)。

下面代码中的Button 就是用了CanvasCanvas.TopCanvas.Left="20" 来进行布局定位,那么这两个就是传说中的附加属性。

<Canvas>
    <Button Canvas.Top="20" Canvas.Left="20" Content="Knights Warrior!"/>
</Canvas>

  在最前面的小节中,我们是使用DependencyProperty.Register来注册一个依赖属性,同时依赖属性本身也对外提供了 DependencyProperty.RegisterAttached方法来注册附加属性。这个RegisterAttached的参数和 Register是完全一致的,那么Attached(附加)这个概念又从何而来呢?

  其实我们使用依赖属性,一直在Attached(附加)。我们注册(构造)一个依赖属性,然后在DependencyObject中通过 GetValue和SetValue来操作这个依赖属性,也就是把这个依赖属性通过这样的方法关联到了这个DependencyObject上,只不过是通过封装CLR属性来达到的。那么RegisterAttached又是怎样的呢?

下面我们来看一个最简单的应用:首先我们注册(构造)一个附加属性

public class AttachedPropertyChildAdder
{
    //通过使用RegisterAttached来注册一个附加属性
    public static readonly DependencyProperty IsAttachedProperty =
        DependencyProperty.RegisterAttached("IsAttached", typeof(bool), typeof(AttachedPropertyChildAdder),
            new FrameworkPropertyMetadata((bool)false));

    //通过静态方法的形式暴露读的操作
    public static bool GetIsAttached(DependencyObject dpo)
    {
        return (bool)dpo.GetValue(IsAttachedProperty);
    }

    //通过静态方法的形式暴露写的操作
    public static void SetIsAttached(DependencyObject dpo, bool value)
    {
        dpo.SetValue(IsAttachedProperty, value);
    }
}

在XAML中就可以使用刚才注册(构造)的附加属性了:

技术分享  

  在上面的例子中,AttachedPropertyChildAdder 中并没有对IsAttached采用CLR属性形式进行封装,而是使用了静态SetIsAttached方法和GetIsAttached方法来存取IsAttached值,当然如果你了解它内部原理,你就会看到实际上还是调用的SetValue与GetValue来进行操作(只不过拥有者不同而已)。这里我们不继续深入下去,详细在后面的内容会揭开谜底。

十. 清除本地值

  在很多时候,由于我们的业务逻辑和UI操作比较复杂,所以一个庞大的页面会进行很多诸如动画、3D、多模板及样式的操作,这个时候页面的值已经都被改变了,如果我们想让它返回默认值,可以用ClearValue 来清除本地值,但是遗憾的是,很多时候由于WPF依赖属性本身的设计,它往往会不尽如人意(详细就是依赖属性的优先级以及依赖属性EffectiveValueEntry 的影响)。ClearValue 方法为在元素上设置的依赖项属性中清除任何本地应用的值提供了一个接口。但是,调用 ClearValue 并不能保证注册属性时在元数据中指定的默认值就是新的有效值。值优先级中的所有其他参与者仍然有效。只有在本地设置的值才会从优先级序列中移除。例如,如果您对同时也由主题样式设置的属性调用 ClearValue,主题值将作为新值而不是基于元数据的默认值进行应用。如果您希望取消过程中的所有属性值,而将值设置为注册的元数据默认值,则可以通过查询依赖项属性的元数据来最终获得默认值,然后使用该默认值在本地设置属性并调用 SetValue来实现,这里我们得感得PropertyMetadata类为我们提供了诸如DefaultValue这样的外部可访问的属性。

上面讲了这么多,现在我们就简单用一个例子来说明上面的原理(例子很直观,相信大家能很容易看懂)

XAML中代码如下:

<Window  x:Class="WpfApplication1.DPClearValue"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="400" Width="400">
    <StackPanel Name="root">
        <StackPanel.Resources>
            <Style TargetType="Button">
                <Setter Property="Height" Value="20"/>
                <Setter Property="Width" Value="250"/>
                <Setter Property="HorizontalAlignment" Value="Left"/>
            </Style>
            <Style TargetType="Ellipse">
                <Setter Property="Height" Value="50"/>
                <Setter Property="Width" Value="50"/>
                <Setter Property="Fill" Value="LightBlue"/>
            </Style>
            <Style TargetType="Rectangle">
                <Setter Property="Height" Value="50"/>
                <Setter Property="Width" Value="50"/>
                <Setter Property="Fill" Value="MediumBlue"/>
            </Style>
            <Style x:Key="ShapeStyle" TargetType="Shape">
                <Setter Property="Fill" Value="Azure"/>
            </Style>
        </StackPanel.Resources>
        <DockPanel Name="myDockPanel">
            <Ellipse Height="100"   Width="100" Style="{StaticResource ShapeStyle}"/>
            <Rectangle Height="100" Width="100"  Style="{StaticResource ShapeStyle}" />
        </DockPanel>
        <Button Name="RedButton" Click="MakeEverythingAzure" Height="39" Width="193">改变所有的值</Button>
        <Button Name="ClearButton" Click="RestoreDefaultProperties" Height="34" Width="192"> 清除本地值</Button>
    </StackPanel>
</Window>

后台代码:

public partial class DPClearValue
 {
     //清除本地值,还原到默认值
     void RestoreDefaultProperties(object sender, RoutedEventArgs e)
     {
         UIElementCollection uic = myDockPanel.Children;
         foreach (Shape uie in uic)
         {
             LocalValueEnumerator locallySetProperties = uie.GetLocalValueEnumerator();
             while (locallySetProperties.MoveNext())
             {
                 DependencyProperty propertyToClear = (DependencyProperty)locallySetProperties.Current.Property;
                 if (!propertyToClear.ReadOnly) { uie.ClearValue(propertyToClear); }
             }
         }
     }

     //修改本地值
     void MakeEverythingAzure(object sender, RoutedEventArgs e)
     {
         UIElementCollection uic = myDockPanel.Children;
         foreach (Shape uie in uic) { uie.Fill = new SolidColorBrush(Colors.Azure); }
     }
 }

当按下”改变所有的值“按钮的时候,就会把之前的值都进行修改了,这个时候按下”清除本地值“就会使原来的所有默认值生效。

技术分享

十一. 依赖属性元数据

前面我们看到一个依赖属性的注册最全的形式是下面这样子的:

public static DependencyProperty Register(string name, 
                                          Type propertyType,
                                          Type ownerType, 
                                          PropertyMetadata typeMetadata,
                                          ValidateValueCallback validateValueCallback);

  第一个参数是该依赖属性的名字,第二个参数是依赖属性的类型,第三个参数是该依赖属性的所有者的类型,第五个参数就是一个验证值的回调委托,那么最使我们感兴趣的还是这个可爱的 PropertyMetadata ,也就是我们接下来要讲的元数据。 提到WPF属性元数据,大家可能第一想到的是刚才的PropertyMetadata,那么这个类到底是怎样的呢?我们应该怎样使用它呢?首先我们看它的构造函数(我们选参数最多的来讲):

public PropertyMetadata(object defaultValue,
                        PropertyChangedCallback propertyChangedCallback, 
                        CoerceValueCallback coerceValueCallback);

  其中的第一个参数是默认值,最后两个分别是PropertyChanged(变化通知)以及Coerce(强制)的两个委托变量,我们在实例化的时候,只需要把这两个委托变量关联到具体的方法上即可。

  事实上,除了PropertyMetadata以外,常见的还有FrameworkPropertyMetadata,UIPropertyMetadata。他们的继承关系是F->U->P。其中以FrameworkPropertyMetadata参数最多,亦最为复杂。

FrameworkPropertyMetadata的构造函数提供了很多重载,我们挑选最为复杂的重载来看它到底有哪些参数以及提供了哪些功能:

public FrameworkPropertyMetadata(object defaultValue,
                                 FrameworkPropertyMetadataOptions flags,
                                 PropertyChangedCallback propertyChangedCallback, 
                                 CoerceValueCallback coerceValueCallback,
                                 bool isAnimationProhibited,
                                 UpdateSourceTrigger defaultUpdateSourceTrigger);

  其中第一个参数是默认值,最后两个参数分别是是否允许动画,以及绑定时更新的策略(在Binding当中相信大家并不陌生),这个不详细解释了。重点看一下里第三、四两个参数,两个 CallBack的委托。结合前面Register的时候提到的ValidateValueCallback共组成三大”金刚“,这三个Callback分别代表Validate(验证),PropertyChanged(变化通知)以及Coerce(强制)。当然,作为 Metadata,FrameworkPropertyMetadata只是储存了该依赖属性的策略信息,WPF属性系统会根据这些信息来提供功能并在适当的时机回调传入的delegate,所以最重要的还是我们定义的这些方法,通过他们传入委托才能起到真正的作用。

上面讲了元数据暴露给我们的构造函数,其实在其内部还提供了两个方法,这个在做自定义控件的时候,也很值得注意:

protected virtual void Merge(PropertyMetadata baseMetadata, DependencyProperty dp)
{
    // 实现元数据继承之间的合并
}

protected virtual void OnApply(DependencyProperty dependencyProperty, Type targetType)
{
    // 当元数据被这个属性应用,OnApply就会被触发,在此时元数据也将被密封起来。
}

前面讲了这么多,那么我们现在就来看看依赖属性回调、验证及强制值到底是怎么使用的呢?大家千万要坚持住,后面内容更加精彩!

十二. 依赖属性回调、验证及强制值

我们通过下面的这幅图,简单介绍一下WPF属性系统对依赖属性操作的基本步骤:

技术分享

  • 第一步,基础值就是上面“六.依赖属性的优先级”提供的那些显示设置,所以它的优先级比较好确定,但有些不会按常规出牌,所以也需要注意总结。
  • 第二步,如果依赖属性值是计算表达式 (如前面示例中的绑定等语法特性),这个时候就会计算表达式的结果作为第二步的值。
  • 第三步,动画是一种优先级很高的特殊行为,很多时候,我们都会听到动画优先的声音,所以它的优先级高于其他基础设置;
  • 第四步,强制则是注册时提供的 CoerceValueCallback 委托,它负责验证属性值是否在允许的限制范围之内,和我们对属性的验证一样,比如强制设置该值必须大于于0小于10等等;
  • 第五步,验证是指我们注册依赖属性所提供的 ValidateValueCallback 委托方法,它最终决定了属性值设置是否有效,当数据无效时会抛出异常来通知。

前面我们讲了基本的流程,下面我们就用一个小的例子来进行说明:

namespace SampleProcess_DPs
{
    class Program
    {
        static void Main(string[] args)
        {
            SimpleDPClass sDPClass = new SimpleDPClass();
            sDPClass.SimpleDP = 8;
            Console.ReadLine();
        }
    }

    public class SimpleDPClass : DependencyObject
    {
        public static readonly DependencyProperty SimpleDPProperty =
            DependencyProperty.Register("SimpleDP", typeof(double), typeof(SimpleDPClass),
                new FrameworkPropertyMetadata((double)0.0,
                    FrameworkPropertyMetadataOptions.None,
                    new PropertyChangedCallback(OnValueChanged),
                    new CoerceValueCallback(CoerceValue)),
                    new ValidateValueCallback(IsValidValue));

        public double SimpleDP
        {
            get { return (double)GetValue(SimpleDPProperty); }
            set { SetValue(SimpleDPProperty, value); }
        }

        private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Console.WriteLine("当值改变时,我们可以做的一些操作,具体可以在这里定义: {0}", e.NewValue);
        }

        private static object CoerceValue(DependencyObject d, object value)
        {
            Console.WriteLine("对值进行限定,强制值: {0}", value);
            return value;
        }

        private static bool IsValidValue(object value)
        {
            Console.WriteLine("验证值是否通过,返回bool值,如果返回True表示严重通过,否则会以异常的形式暴露: {0}", value);
            return true;
        }

    }
}

结果如下:

技术分享

  当SimpleDP属性变化之后,PropertyChangeCallback就会被调用。可以看到结果并没有完全按照我们先前的流程先Coerce后Validate的顺序执行,有可能是WPF内部做了什么特殊处理,当属性被修改时,首先会调用Validate来判断传入的value是否有效,如果无效就不继续后续的操作,这样可以更好的优化性能。从上面的结果上看出,CoerceValue后面并没有立即ValidateValue,而是直接调用了PropertyChanged。这是因为前面已经验证过了value,如果在Coerce中没有改变value,那么就不用再验证了。如果在 Coerce中改变了value,那么这里还会再次调用ValidateValue操作,和前面的流程图执行的顺序一样,在最后我们会调用ValidateValue来进行最后的验证,这就保证最后的结果是我们希望的那样了(正如打游戏一样,打了小怪,在最后过总关的时候还是需要打大怪才能闯关的)。

  上面简单介绍了处理流程,下面我们就以一个案例来具体看一看上面的流程到底有没有出入,这个例子改编于Sacha Barber 的Dependency Properties代码示例,我相信通过这段代码你会对这个上面讲的概念有更清晰地认识。

  UI很简单,黄色部分显示当前值,我们在初始化的时候把它设置为100,然后它的最小值和最大值分别设置为0和500,按钮”设置为-100“企图把当前值设为-100,按钮”设置为1000“试图把当前值设为1000。具体大家看代码(我都写了注释,很容易理解的).

技术分享

依赖属性代码文件如下:

namespace Callback_Validation_DPs
{
    public class Gauge : Control
    {
        public Gauge() : base() { }
        //注册CurrentReading依赖属性,并添加PropertyChanged、CoerceValue、ValidateValue的回调委托
        public static readonly DependencyProperty CurrentReadingProperty = DependencyProperty.Register(
            "CurrentReading",
            typeof(double),
            typeof(Gauge),
            new FrameworkPropertyMetadata(
                Double.NaN,
                FrameworkPropertyMetadataOptions.None,
                new PropertyChangedCallback(OnCurrentReadingChanged),
                new CoerceValueCallback(CoerceCurrentReading)
            ),
            new ValidateValueCallback(IsValidReading)
        );

        //属性包装器,通过它来暴露CurrentReading的值
        public double CurrentReading
        {
            get { return (double)GetValue(CurrentReadingProperty); }
            set { SetValue(CurrentReadingProperty, value); }
        }

        //注册MinReading依赖属性,并添加PropertyChanged、CoerceValue、ValidateValue的回调委托
        public static readonly DependencyProperty MinReadingProperty = DependencyProperty.Register(
        "MinReading",
        typeof(double),
        typeof(Gauge),
        new FrameworkPropertyMetadata(
            double.NaN,
            FrameworkPropertyMetadataOptions.None,
            new PropertyChangedCallback(OnMinReadingChanged),
            new CoerceValueCallback(CoerceMinReading)
        ),
        new ValidateValueCallback(IsValidReading));

        //属性包装器,通过它来暴露MinReading的值
        public double MinReading
        {
            get { return (double)GetValue(MinReadingProperty); }
            set { SetValue(MinReadingProperty, value); }
        }

        //注册MaxReading依赖属性,并添加PropertyChanged、CoerceValue、ValidateValue的回调委托
        public static readonly DependencyProperty MaxReadingProperty = DependencyProperty.Register(
            "MaxReading",
            typeof(double),
            typeof(Gauge),
            new FrameworkPropertyMetadata(
                double.NaN,
                FrameworkPropertyMetadataOptions.None,
                new PropertyChangedCallback(OnMaxReadingChanged),
                new CoerceValueCallback(CoerceMaxReading)
            ),
            new ValidateValueCallback(IsValidReading)
        );

        //属性包装器,通过它来暴露MaxReading的值
        public double MaxReading
        {
            get { return (double)GetValue(MaxReadingProperty); }
            set { SetValue(MaxReadingProperty, value); }
        }

        //在CoerceCurrentReading加入强制判断赋值
        private static object CoerceCurrentReading(DependencyObject d, object value)
        {
            Gauge g = (Gauge)d;
            double current = (double)value;
            if (current < g.MinReading) current = g.MinReading;
            if (current > g.MaxReading) current = g.MaxReading;
            return current;
        }


        //当CurrentReading值改变的时候,调用MinReading和MaxReading的CoerceValue回调委托
        private static void OnCurrentReadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            d.CoerceValue(MinReadingProperty);  
            d.CoerceValue(MaxReadingProperty); 
        }

        //当OnMinReading值改变的时候,调用CurrentReading和MaxReading的CoerceValue回调委托
        private static void OnMinReadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            d.CoerceValue(MaxReadingProperty);
            d.CoerceValue(CurrentReadingProperty);
        }

        //在CoerceMinReading加入强制判断赋值
        private static object CoerceMinReading(DependencyObject d, object value)
        {
            Gauge g = (Gauge)d;
            double min = (double)value;
            if (min > g.MaxReading) min = g.MaxReading;
            return min;
        }

        //在CoerceMaxReading加入强制判断赋值
        private static object CoerceMaxReading(DependencyObject d, object value)
        {
            Gauge g = (Gauge)d;
            double max = (double)value;
            if (max < g.MinReading) max = g.MinReading;
            return max;
        }

        //当MaxReading值改变的时候,调用MinReading和CurrentReading的CoerceValue回调委托
        private static void OnMaxReadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            d.CoerceValue(MinReadingProperty);  
            d.CoerceValue(CurrentReadingProperty); 
        }

        //验证value是否有效,如果返回True表示验证通过,否则会提示异常
        public static bool IsValidReading(object value)
        {
            Double v = (Double)value;
            return (!v.Equals(Double.NegativeInfinity) && !v.Equals(Double.PositiveInfinity));
       }
    }
}

XAML代码如下:

<Window x:Class="Callback_Validation_DPs.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:Callback_Validation_DPs" 
    WindowStartupLocation="CenterScreen" 
    Title="Callback_Validation_DPs" Height="400" Width="400">
    <StackPanel Orientation="Vertical">
        <local:Gauge x:Name="gauge1" MaxReading="100" MinReading="0" />
        <Label Content="可以设置最小值为0和最小大值为500" Height="30"/>
        <StackPanel Orientation="Horizontal" Height="60">
            <Label Content="当前值为 : "/>
            <Label Background="Yellow" BorderBrush="Black" BorderThickness="1" 
                   IsEnabled="False" Content="{Binding ElementName=gauge1, Path=CurrentReading}" Height="25" VerticalAlignment="Top" />
        </StackPanel>
        <Button x:Name="btnSetBelowMin" Content="设置为 -100" 
                Click="btnSetBelowMin_Click"/>
        <Button x:Name="btnSetAboveMax" Content="设置为 1000" 
                Click="btnSetAboveMax_Click"/>
    </StackPanel>
</Window>

XAML的后台代码如下:

public partial class Window1 : Window
{
    public Window1()
    {
        InitializeComponent();
        //设置CurrentReading的值,这个时候会触发哪些变化?调试代码吧!
        gauge1.CurrentReading = 100;
    }

    private void btnSetBelowMin_Click(object sender, RoutedEventArgs e)
    {
        //设置CurrentReading的值,这个时候会触发哪些变化?调试代码吧!
        gauge1.CurrentReading = -100;
    }

    private void btnSetAboveMax_Click(object sender, RoutedEventArgs e)
    {
        //设置CurrentReading的值,这个时候会触发哪些变化?调试代码吧!
        gauge1.CurrentReading = 10000;

    }
}

  在上面的例子中,一共有三个依赖属性相互作用——CurrentReading、MinReading和MaxReading,这些属性相互作用,但它们的规则是MinReading≤CurrentReading≤MaxReading。根据这个规则,当其中一个依赖属性变化时,另外两个依赖属性必须进行适当的调整,这里我们要用到的就是CoerceValue这个回调委托,那么实现起来也非常的简单,注册MaxReading的时候加入CoerceValueCallback,在CoerceMaxReading函数中做处理:如果Maximum的值小于MinReading,则使MaxReading值等于MinReading;同理在CurrentReading中也加入了CoerceValueCallback进行相应的强制处理。然后在MinReading的ChangedValueCallback被调用的时候,调用CurrentReading和MaxReading的CoerceValue回调委托,这样就可以达到相互作用的依赖属性一变应万变的”千机变“。

  换句话说,当相互作用的几个依赖属性其中一个发生变化时,在它的PropertyChangeCallback中调用受它影响的依赖属性的CoerceValue,这样才能保证相互作用关系的正确性。 前面也提高ValidateValue主要是验证该数据的有效性,最设置了值以后都会调用它来进行验证,如果验证不成功,则抛出异常。

十三. 依赖属性监听

如果想监听依赖属性的改变,可以用两种方法实现,在很多时候,我们两种方法都会用到: 
用DependencyPropertyDescriptor 比较简便,在代码里面写起来也比较便捷; 
用OverrideMetadata的方式主要在自定义控件以及处理一些类间关系的时候; 
第一种方法:派生自这个类,然后定义它的属性,重写属性的原数据并传递一个PropertyChangedCallBack参数即可,如下代码:

public class MyTextBox : TextBox
{
    public MyTextBox(): base()
    {
    }

    static MyTextBox()
    {
        //第一种方法,通过OverrideMetadata
        FlowDirectionProperty.OverrideMetadata(typeof(MyTextBox), new FrameworkPropertyMetadata(new PropertyChangedCallback(FlowDirectionPropertyChanged)));
    }

    private static void FlowDirectionPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
    {
        ((MyTextBox)sender).FontWeight = (((MyTextBox)sender).FlowDirection == FlowDirection.LeftToRight) ? FontWeights.Bold : FontWeights.Normal;

    }
}

第二种方法:这个方法更加简单,获取DependencyPropertyDescriptor并调用AddValueChange()为其挂接一个回调函数,如下代码:

private void Window1_Loaded(object sender, RoutedEventArgs e)
 {
     //第二种方法,通过OverrideMetadata
     DependencyPropertyDescriptor descriptor = DependencyPropertyDescriptor.FromProperty(TextBox.TextProperty, typeof(TextBox));
     descriptor.AddValueChanged(tbxEditMe, tbxEditMe_TextChanged);
 }

 private void tbxEditMe_TextChanged(object sender, EventArgs e)
 {
     MessageBox.Show("", "Changed");
 }

十四. 代码段(自动生成)

  代码段可以说是一个非常普遍且实用的功能,我们可以利用它来简化和规范我们的代码。在项目当中我们通常会定义大量的代码段,如怎样写一个类、怎样定义一个方法、公用代码库等等都可以定义成代码段,今天不着重讲这一块,下面就来看看在默认的VS中有哪些代码段

技术分享

  上面看到的是visual basic的代码段(一不小心截图截错了,呵呵),但不幸的是针对C#的代码段却很少。不过没关系,既然默认没有提供那么多代码段,我们可以自己动手嘛,正所谓自己动手丰衣足食嘛!相信大家都有自定义代码段的经历,同时在网上也有很多好的代码段下载,我用得最多的是DrWPFSnippets,由于接触WPF和Silverlight是在07年,所以当时自己也定义过一些代码段,由于自己主要精力还是在技术架构、ASP.NET、WCF、OO等方面,所以在08年以后就开始使用网上的代码段资源了,当然由于之前项目也自己写了一些代码段,所以很多时候都是混合起来使用,大家可以到http://drwpf.com/blog/2010/04/30/updated-code-snippets/去下载,这个代码段包最早从2007年11月就提供下载了,在今年四月份进行了升级,同时支持VS2005/VS2008/VS2010,所以大家可以下载下来体验一下,很不错的哦!下载以后点击DrWPFSnippets.vsi就会自动安装,安装完成以后,你会看到如下界面,图中的Shortcut就是你要按的快捷键,不过生成的代码会出现有些帮助类找不到的情况,如RoutedEvent会生成一个RoutedEventHelper的类,这个是没有关系的,你到网上一搜就可以把这个类加入到你的代码当中。那么运行就十分正常了。在安装的时候提醒一下,最好一次安装成功,否则你会为众多的弹窗口感到十分厌恶,呵呵!

技术分享

那么现在你就可以在项目当中使用了,如按下re+TAB键两次,你就会看到如下界面,然后选择你的选项即可生成需要的代码(这里re就是Routed event的简写)。

技术分享

如下是生成的代码,你可以直接使用或者经过适当修改使用。

    #region Swindle

/// <summary>
/// Swindle Routed Event
/// </summary>
public static readonly RoutedEvent SwindleEvent = EventManager.RegisterRoutedEvent("Swindle", 
    RoutingStrategy.Bubble, typeof(TrioEventHandler), typeof(Window1));

/// <summary>
/// Occurs when ...
/// </summary>
public event TrioEventHandler Swindle

{
    add { AddHandler(SwindleEvent, value); }
    remove { RemoveHandler(SwindleEvent, value); }
}

/// <summary>
/// A helper method to raise the Swindle event.
/// </summary>
/// <param name="arg"> </param>
/// <param name="arg2"> </param>
/// <param name="arg3"> </param>
protected TrioEventArgs RaiseSwindleEvent(bool arg, bool arg2, bool arg3)
{
    return RaiseSwindleEvent(this, arg, arg2, arg3);
}

/// <summary>
/// A static helper method to raise the Swindle event on a target element.
/// </summary>
/// <param name="target">UIElement or ContentElement on which to raise the event</param>
/// <param name="arg"> </param>
/// <param name="arg2"> </param>
/// <param name="arg3"> </param>
internal static TrioEventArgs RaiseSwindleEvent(DependencyObject target, bool arg, bool arg2, bool arg3)
{
    if (target == null) return null;
    
    TrioEventArgs args = new TrioEventArgs(arg, arg2, arg3);
    args.RoutedEvent = SwindleEvent;
    RoutedEventHelper.RaiseEvent(target, args);
    return args;
}

#endregion

十五. 模拟依赖属性实现

  古人有”不入虎穴焉得虎子“的名句,我们今天也试着入一入虎穴,探探依赖属性里面到底藏着什么不可告人的秘密,在往下讲之前,我们先来看一下DependencyObject DependencyProperty 以及PropertyMetadata到底包含哪些功能,如下面三幅图

技术分享

 

技术分享

 

技术分享

  通过前面三幅图,我们就可以了解WPF依赖属性系统的大体结构以及主要功能,再者通过前面我们对它的使用,对它的内部实现也有一个相对比较清晰的认识,那么接下来要做的就是:借助Reflector+VS调试内部代码功能一起来研究其内部的实现原理。 本来想详细写清楚开发的过程,但是有点多,所以我打算直接讲这几个类。大家也可以通过这个思路来试一试,同时还可以参考Mono的源码、WF的依赖属性源码等。这里要推荐的是周永恒的博客,此人对技术的理解很是透彻,博文虽少,但每篇都堪称经典,所以他的文章,我都通读三遍。虽然大多概念都懂,并且读到深处也能产生共鸣,其最主要目的还是学习他这种”阐述问题的思路“,后来也和此人MSN聊过几次。所以这个依赖属性的框架在某些程度上也借鉴了他的一些写法。

  有了前面的思路,首先定义DependencyProperty这个类,它里面存储前面我们提到希望抽出来的字段。DependencyProperty内部维护了一个全局的Map用来储存所有的DependencyProperty,对外暴露了一个Register方法用来注册新的DependencyProperty。当然,为了保证在Map中键值唯一,注册时需要根据传入的名字和注册类的的 HashCode取异或来生成Key。 所以我们就可以完成DependencyProperty类了,代码如下,介绍详见代码注释。:

public sealed class DependencyProperty
 {
     //全局的IDictionary用来储存所有的DependencyProperty
     internal static IDictionary<int, DependencyProperty> properties = new Dictionary<int, DependencyProperty>();
     //存储元数据的集合
     private List<PropertyMetadata> _metadataMap = new List<PropertyMetadata>();
     private static int globalIndex = 0;
     private PropertyMetadata def_metadata;
     private bool attached;
     private string name;
     private int _index;
     private Type owner_type;
     private Type property_type;
     private Type validator_type;

     // 构造函数
     private DependencyProperty()
     {

     }

     //构造函数私有,保证外界不会对它进行实例化
     private DependencyProperty(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata)
     {
         this.name = name;
         property_type = propertyType;
         owner_type = ownerType;
         def_metadata = defaultMetadata;
     }

     // 常用属性
     public PropertyMetadata DefaultMetadata
     {
         get { return def_metadata; }
     }

     public bool IsAttached
     {
         get { return attached; }
     }

     public int Index
     {
         get { return _index; }
         set { _index = value; }
     }

     public string Name
     {
         get { return name; }
     }

     public Type OwnerType
     {
         get { return owner_type; }
     }

     public Type PropertyType
     {
         get { return property_type; }
     }

     public Type ValidatorType
     {
         get { return validator_type; }
     }


     public override int GetHashCode()
     {
         return name.GetHashCode() ^ owner_type.GetHashCode();
     }

     //注册依赖属性
     public static DependencyProperty Register(string name, Type propertyType, Type ownerType)
     {
         return Register(name, propertyType, ownerType, new PropertyMetadata());
     }

     //注册的公用方法,把这个依赖属性加入到IDictionary的键值集合中,Key为name和owner_type的GetHashCode取异,Value就是我们注册的DependencyProperty
     public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata)
     {
         DependencyProperty property = new DependencyProperty(name, propertyType, ownerType, defaultMetadata);
         globalIndex++;
         property.Index = globalIndex;

         if (properties.ContainsKey(property.GetHashCode()))
         {
             throw new InvalidOperationException("A property with the same name already exists");
         }

         //把刚实例化的DependencyProperty添加到这个全局的IDictionary种
         properties.Add(property.GetHashCode(), property);
         return property;
     }

     //注册只读依赖属性
     public static DependencyProperty RegisterReadOnly(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata)
     {
         DependencyProperty property = Register(name, propertyType, ownerType, typeMetadata);
         return property;
     }

     //注册附加依赖属性
     public static DependencyProperty RegisterAttached(string name, Type propertyType, Type ownerType)
     {
         return RegisterAttached(name, propertyType, ownerType, new PropertyMetadata(), null);
     }

     public static DependencyProperty RegisterAttached(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata)
     {
         return RegisterAttached(name, propertyType, ownerType, defaultMetadata, null);
     }

     public static DependencyProperty RegisterAttached(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata, Type validatorType)
     {
         DependencyProperty property = Register(name, propertyType, ownerType, defaultMetadata);
         property.attached = true;
         property.validator_type = validatorType;
         return property;
     }

     //子类继承重写以及其他需要重写Metadata的时候使用
     public void OverrideMetadata(Type forType, PropertyMetadata metadata)
     {
         metadata.Type = forType;
         _metadataMap.Add(metadata);
     }

     //获取元数据信息
     public PropertyMetadata GetMetadata(Type type)
     {
         PropertyMetadata medatata = _metadataMap.FirstOrDefault((i) => i.Type == type) ??
             _metadataMap.FirstOrDefault((i) => type.IsSubclassOf(i.Type));
         if (medatata == null)
         {
             medatata = def_metadata;
         }
         return medatata;
     }

 }

  有了DependencyProperty ,那么接下来就需要定义DependencyObject 来使用这个DependencyProperty 。首先使用DependencyProperty .Register方法注册了一个新的DependencyProperty ,然后提供了GetValue和SetValue两个方法来操作刚刚构造的DependencyProperty 。这个时候我们看到一个简单的依赖属性系统已初见端倪了,详见代码注释。

namespace Realize_DPs
{
    public abstract class DependencyObject :  IDisposable
    {
        //添加一个List来记录修改信息
        private List<EffectiveValueEntry> _effectiveValues = new List<EffectiveValueEntry>();

        //属性包装器,通过它来访问依赖属性
        public object GetValue(DependencyProperty dp)
        {
            //首先通过判断是否改动过,以此来决定是读元数据的默认值还是改动了的值
            EffectiveValueEntry effectiveValue = _effectiveValues.FirstOrDefault((i) => i.PropertyIndex == dp.Index);
            if (effectiveValue.PropertyIndex != 0)
            {
                return effectiveValue.Value;
            }
            else
            {
                PropertyMetadata metadata;
                metadata = DependencyProperty.properties[dp.GetHashCode()].DefaultMetadata;
                return metadata.DefaultValue;
            }
        }

        //属性包装器,通过它来设置依赖属性的值
        public void SetValue(DependencyProperty dp, object value)
        {
            //首先通过判断是否改动过,以及改动过,则继续对改动过的元素赋值,否则对_effectiveValues增加元素
            EffectiveValueEntry effectiveValue = _effectiveValues.FirstOrDefault((i) => i.PropertyIndex == dp.Index);
            if (effectiveValue.PropertyIndex != 0)
            {
                effectiveValue.Value = value;
            }
            else
            {
                effectiveValue = new EffectiveValueEntry() { PropertyIndex = dp.Index, Value = value };
                _effectiveValues.Add(effectiveValue);
            }
        }

        public void Dispose()
        {
            //暂时还没有处理
        }
    }

    internal struct EffectiveValueEntry
    {
        internal int PropertyIndex { get; set; }
        internal object Value { get; set; }
    }
}

  前面有了DependencyProperty DependencyObject 类,那我们现在来新建一个比较重要的类 PropertyMetadata ,它的作用和功能很强大,我们这里只是简单进行了构建,如下代码:

namespace Realize_DPs
{
    public delegate void SetValueOverride(DependencyObject d, object value);

    public delegate object GetValueOverride(DependencyObject d);

    public class PropertyMetadata
    {
        private object default_value;
        private DependencyPropertyOptions options = DependencyPropertyOptions.Default;
        private bool _sealed = false;
        private SetValueOverride set_value;
        private GetValueOverride get_value;
        private Attribute[] attributes;
        private Type type;

        // 构造函数重载
        public PropertyMetadata()
        {

        }

        public PropertyMetadata(object defaultValue)
        {
            default_value = defaultValue;
        }

        public PropertyMetadata(DependencyPropertyOptions options)
        {
            this.options = options;
        }

        public PropertyMetadata(params Attribute[] attributes)
        {
            this.attributes = attributes;
        }

        public PropertyMetadata(object defaultValue, params Attribute[] attributes)
        {
            default_value = defaultValue;
            this.attributes = attributes;
        }

        public PropertyMetadata(object defaultValue, DependencyPropertyOptions options)
        {
            default_value = defaultValue;
            this.options = options;
        }

        public PropertyMetadata(DependencyPropertyOptions options, params Attribute[] attributes)
        {
            this.options = options;
            this.attributes = attributes;
        }

        public PropertyMetadata(object defaultValue, DependencyPropertyOptions options, params Attribute[] attributes)
        {
            this.options = options;
            default_value = defaultValue;
            this.attributes = attributes;
        }

        public PropertyMetadata(object defaultValue, DependencyPropertyOptions options, GetValueOverride getValueOverride, SetValueOverride setValueOverride)
        {
            this.options = options;
            default_value = defaultValue;
            set_value = setValueOverride;
            get_value = getValueOverride;
        }

        public PropertyMetadata(object defaultValue, DependencyPropertyOptions options, GetValueOverride getValueOverride, SetValueOverride setValueOverride, params Attribute[] attributes)
        {
            this.options = options;
            default_value = defaultValue;
            set_value = setValueOverride;
            get_value = getValueOverride;
            this.attributes = attributes;
        }

        // 常用属性
        public object DefaultValue
        {
            get { return default_value; }
            set { default_value = value; }
        }

        public GetValueOverride GetValueOverride
        {
            get { return get_value; }
            set { get_value = value; }
        }

        public bool IsMetaProperty
        {
            get { return (options & DependencyPropertyOptions.Metadata) == DependencyPropertyOptions.Metadata; }
        }

        public bool IsNonSerialized
        {
            get { return (options & DependencyPropertyOptions.NonSerialized) == DependencyPropertyOptions.NonSerialized; }
        }

        public bool IsReadOnly
        {
            get { return (options & DependencyPropertyOptions.Readonly) == DependencyPropertyOptions.Readonly; }
        }

        protected bool IsSealed
        {
            get { return _sealed; }
        }

        public DependencyPropertyOptions Options
        {
            get { return options; }
            set { options = value; }
        }

        public SetValueOverride SetValueOverride
        {
            get { return set_value; }
            set { set_value = value; }
        }

        public Type Type
        {
            get { return type; }
            set { type = value; }
        }

        protected virtual void Merge(PropertyMetadata baseMetadata, DependencyProperty dp)
        {
            // 实现元数据继承之间的合并
        }

        protected virtual void OnApply(DependencyProperty dependencyProperty, Type targetType)
        {
            // 当元数据被这个属性应用,OnApply就会被触发,在此时元数据也将被密封起来。
        }
    }
}

前面我们实现了一个简单的依赖属性系统,现在就得先测试一下其功能,代码如下:

class Program : DependencyObject
{
    public static readonly DependencyProperty CounterProperty;
    static Program()
    {
        //注册依赖属性Counter
        CounterProperty = DependencyProperty.Register("Counter",
                                            typeof(double),
                                            typeof(Program),
                                            new PropertyMetadata(8.0));
    }

    //属性包装器,暴露读写接口
    public double Counter
    {
        get { return (double)GetValue(CounterProperty); }
        set {SetValue(CounterProperty, value); }
    }

    static void Main(string[] args)
    {
        Program pro = new Program();
        Console.WriteLine("读取元数据设置的默认值: "+pro.Counter.ToString());

        Program pro2 = new Program();
        pro2.Counter = 22.5;
        Console.WriteLine("通过SetValue设置改变了的值: " + pro2.Counter.ToString());
        Console.ReadLine();
    }
}

那么测试结果为:

技术分享

利用VS自带的类图,可以看到刚才我们实现的这个依赖属性类及类之间的关系图: 技术分享

由于上面的代码在很多方面都很粗糙,所以希望大家能下载代码进行改造,同时也希望给出反馈。

十六. 本文总结

  这篇文章洋洋洒洒写了很多,我们现在简单回顾一下:在开篇之前我们会先介绍比本篇更重要的一些东西,然后插播了一段”云计算之旅“的广告(广告费很昂贵技术分享,所以格外小心),作为最近几个月执着研究的东西,终于可以在下周和大家见面了,所以心中甚是喜悦。在前面的两个内容之后我们正式进入本篇的主题——依赖属性。依赖属性是WPF的核心概念,所以我们花费了大量的时间和篇幅进行论述,首先从依赖属性基本介绍讲起,然后过渡到依赖属性的优先级、附加属性、只读依赖属性、依赖属性元数据、依赖属性回调、验证及强制值、依赖属性监听、代码段(自动生成) 等相关知识,最后我们模拟了一个WPF依赖属性的实现,对内部实现原理进行了一些研究。在接下来的三篇”剖析路由事件”、”剖析命令”、”剖析绑定”也会采用这篇文章的风格,希望能尽量说透,如果有误之处还希望各位能够批评指正!

十七. 相关代码下载

  在文章的最后,我们提供代码的下载,这几篇文章最重要的就是下载代码来细细研究,代码里面也添加了比较详细的注释,如果大家有什么问题,也可以和我联系,如果有不正确的地方也希望多多海涵并能给我及时反馈,我将感激不尽!

技术分享  
上图就是整个代码包的结构图,下载链接:DependencyPropertiesDemo.rar

版权声明:本文为博主http://www.zuiniusn.com原创文章,未经博主允许不得转载。

WPF基础到企业应用系列7——深入剖析依赖属性(WPF/Silverlight核心)

标签:.net

原文地址:http://blog.csdn.net/u013948191/article/details/47000755

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!