WPF - 属性系统 (4 of 4)
依赖项属性的重写
在基于C#的编程中,对属性的重写常常是一种行之有效的解决方案:在基类所提供的属性访问符实现不能满足当前要求的时候,我们就需要重新定义属性的访问符。
但对于依赖项属性而言,属性执行逻辑的重新定义并不能存在于CLR属性包装中:WPF内部对依赖项属性的实现要求依赖项属性的CLR包装实现仅仅调用GetValue()以及SetValue()属性,而不能提供其它的自定义逻辑。相反地,我们需要通过更改创建时所传入的元数据来指定自定义属性执行逻辑,甚至在某些更苛刻的要求下,如更改依赖项属性的类型,重新定义一个具有相同名称的依赖项属性。
对一个依赖项属性的重写非常简单。如果一个类型从其基类中继承了一个依赖项属性,那么软件开发人员可以在派生类中通过OverrideMetadata()方法完成对属性元数据的覆盖。在重写元数据的时候,系统会将新的元数据与之前的依赖项属性元数据中的各信息进行合并或替换:
1) 合并PropertyChangedCallback。
2) 替换DefaultValue。
3) 替换CoerceValueCallback的实现。
4) 合并ValidationCallback。
5) 对于FrameworkPropertyMetadata而言,FrameworkPropertyMetadataOptions的标志组合为按位或运算。
这里所提到的操作主要分为两种:合并和替换。在这里,合并的意思就是在类型的继承层次中的所有对该组成的赋值都将会被保留。在需要执行该组成的时候,WPF属性系统会按照类型的继承层次依次调用该组成。而替换则表示当前对该组成的声明将会完全替换其所有基类中所声明的该组成。接下来,WPF属性系统仅仅会调用类型继承层次中最高层次的类型所声明的该组成。
现在我们来看看元数据中各个组成采取合并或是替换的理由。首先要讨论的就是PropertyChangedCallback。该回调所做的事情就是在一个依赖项属性发生了更改的时候刷新其它该类型所包含的依赖项属性。当然,这种逻辑在依赖项属性声明的类型中实现是最正常的一种想法:在同一个类型中调用该函数,刷新其它依赖项属性值可以保证该类型实例处于正常的状态。
在属性发生更改的时候,系统将首先调用最高层次派生类中所设置的PropertyChangedCallback回调,并沿类型层次结构依次调用各基类实现所提供的各个回调。就像前几节中的实例代码所展示的那样,这些回调常常通过调用CoerceValue来完成其它相关联依赖项属性的更新。通过这一系列回调,该类型继承层次中的各个类型都将处于一个正常的状态。
对DefaultValue的替换则非常容易理解:由于一个属性不能同时拥有多个默认值,因此使用新的默认值替换基类中所声明的默认值是一种非常正常的选择。
接下来则是CoerceValueCallback。在依赖项属性发生变化的时候,属性系统将仅调用最直接元数据的CoerceValueCallback。这是因为基类中的CoerceValueCallback回调并不了解派生类中的各个属性,因此一旦定义了新的CoerceValueCallback回调,基类中所定义的逻辑将不再适合对依赖项属性的值进行约束。
下一个需要讨论的组成则是ValidationCallback回调。由于该函数是在属性注册时传入的,而不是作为元数据中所储存的数据存储在属性系统中。因此它无法被新的属性注册所覆盖。同时不将其添加到元数据中的理由:万一覆盖了,那还需要将所有原ValidationCallback回调中的逻辑重写一遍。
最后一个需要说明的则是元数据选项的处理。在通过OverrideMetadata()方法操作一个元数据所记录的各个元数据选项的时候,所有的元数据选项将被合并。当然,这里有一种情况就是消除之前设置的元数据选项。在需要达到该目的的时候,我们需要将该元数据选项所对应的属性设置为false。举例来说,软件开发人员可以在元数据中通过NotDataBindable标记设置一个依赖项属性不能被绑定。但是如果需要通过OverrideMetadata()函数清除该选项的时候,软件开发人员就需要在传入的元数据上将IsNotDataBinable属性设置为false。
当然,OverrideMetadata()函数仅仅是一种重用原有依赖项属性的方法。另一种重用的方法则是AddOwner()。该函数将其它类型中的依赖项属性添加到当前类型中。该函数的签名如下:
public DependencyProperty AddOwner(Type ownerType, PropertyMetadata typeMetadata);
该函数用来将一个DependencyProperty添加到ownerType所表示的类型上,并可以通过typeMetadata更改该依赖项属性的行为。
在使用标示依赖项属性的DependencyProperty类型的标记时,我们最好使用AddOwner()函数所返回的依赖项属性标记,而不是原注册类型中所保存的依赖项属性标记。这样做的最主要目的更多是基于语义的考虑。实际上,通过原本的依赖项属性标记以及AddOwner()所返回的依赖项属性标记进行操作所返回的运行结果是相同的。
与OverrideMetadata()函数明显不同的是,该函数并不继承原属性的元数据。因此在使用AddOwner()函数时,软件开发人员最需要考虑的事情就是是否需要自行指定新属性的元数据。当然,如果软件开发人员对基类的依赖项对象调用AddOwner,那么元数据将被继承并和新元数据合并。
引用类型的依赖项属性
实现一个引用类型的依赖项属性与实现普通的依赖项属性的步骤并没有什么不同:定义一个CLR属性包装,并在该属性包装中通过GetValue()以及 SetValue() 函数完成对依赖项属性值的获取和设置。唯一一点不同的是,软件开发人员不应该在依赖项属性注册的时候为该依赖项属性提供一个默认值,而是在类型的初始化函数中为该依赖项属性显式地赋值。
为什么要这样做呢?这是因为在这种情况下,多个实例上的引用类型依赖项属性可能会返回一个相同的引用类型实例。产生该问题的原因是由依赖项属性的两个特性共同作用产生的:1. 在没有经过赋值的情况下,一个依赖项属性所返回的值就是在依赖项属性注册时传入的默认值。2.在依赖项属性注册过程中所传入的值实际上是引用类型实例的引用,并将作为所有该依赖项属性的默认值,指向同一个引用类型实例。
因此在实现一个引用类型的依赖项属性时,我们需要在构造函数中显式地为该引用类型的依赖项属性赋值。在这种情况下,您有两种选择:首先查看依赖项属性的类型是否自定义类型,并可以由class更改成为struct。如果不能,那么在依赖项属性注册过程中将默认值标为null,而在构造函数中再将其设置为所需要的默认值。
第一种方法在WPF实现中非常常见。就以Control类的Padding属性为例:
public static readonly DependencyProperty PaddingProperty = DependencyProperty
.Register("Padding", typeof(Thickness), typeof(Control),
new FrameworkPropertyMetadata(new Thickness(),
FrameworkPropertyMetadataOptions.AffectsParentMeasure));
上面的代码注册了一个类型为Thickness的依赖项属性PaddingProperty,并在该属性的元数据中传入了一个默认值。在查看Thickness类型的定义后可以发现,其实际上是一个结构体。在C#中,结构体会在栈上被分配,从而避免了多个该属性所在UI元素引用同一个引用类型实例的情况。
但事情不能总是这么幸运。首先,依赖项属性的类型可能并不是一个用户自定义类型,因此我们并没有机会将其转化为结构体。另外,一个类型所包含的信息可能非常多,在那种情况下,将一个类型实现为结构体是并不合适的。因此在必须创建一个引用类型的依赖项属性时,我们需要在构造函数中对该属性分别赋值。例如ItemsControl就提供了一个ItemsPanel依赖项属性。如果该依赖项属性通过构造函数进行初始化,那么创建依赖项属性的函数调用以及构造函数定义将如下代码所示:
public static readonly DependencyProperty ItemsPanelProperty = DependencyProperty
.Register("ItemsPanel", typeof(ItemsPanelTemplate), typeof(ItemsControl),
new FrameworkPropertyMetadata(null, ……)); public ItemsControl()
{
SetValue(ItemsPanelProperty, new StackPanel());
}
但是这违反了WPF对于依赖项属性容器类型构造函数定义的最佳实践。在一个依赖项属性的注册过程中,以及在派生类对该属性的覆盖过程中,软件开发人员都可以为依赖项属性设置回调函数。同时在每次依赖项属性发生变化的时候,这些回调函数都将被执行。由于这些回调函数是在基类的构造函数中被触发,但其所调用的函数可能被派生类重写,所以这些函数的执行可能处于派生类并没有完全初始化的情况。
为了避免这种问题,WPF提出了一个定义安全的构造函数的标准:
1. 为您的类型提供一个默认构造函数:
public MyClass : SomeBaseClass {
public MyClass() : base() {
// 所有成员的初始化,包括其它构造函数可能赋值的数据成员或回调函数
// 将会使用的数据成员
}
}
2. 如果一个类型提供了非默认构造函数,那么该构造函数首先需要调用该类型的默认构造函数,然后再使用SetValue()等函数设置各依赖项属性的值。
public MyClass : SomeBaseClass {
public MyClass(object toSetProperty1) : this() {
// 注意,这里调用的是默认构造函数,而不是基类的构造函数
Property1 = toSetProperty1;
}
}
只是谁又能保证用户都熟知这些规则并在编写自定义类型的时候按照这些规则对类型进行编写呢?
如果依赖项属性的类型是一个集合,那么另外一点需要注意的地方则是:XAML解析器无法知道如何调用一个泛型函数。也就是说,如果一个依赖项属性的类型是List<T>,那么WPF并不知道如何调用List<T>.Add(T item),而只知道如何调用非泛型接口成员。因此可知如果软件开发人员希望一个属性是一个集合,那么该集合类型需要实现非泛型的IList接口,如Collection<T>或List<T>。
而在实现一个集合类型的属性时,到底是将其实现为一个只读依赖项属性还是可读写依赖项属性则会影响该属性在XAML中的使用方法。就以下面两种XAML标记为例:
<Toolbar>
<Toolbar.Items>
<ToolbarItem .../>
</Toolbar.Items >
</Toolbar> <Toolbar>
<Toolbar.Items>
<ToolbarItemCollection>
<ToolbarItem/>
</ToolbarItemCollection>
</Toolbar.Items>
</Toolbar>
当然,上面的XAML代码仅仅是用作示例,而并非是实际的WPF代码。假设这里的Toolbar类型拥有一个Items属性,其用来记录所有的ToolbarItem类型的子元素。在XAML分析第一段XAML的时候,WPF将首先调用Toolbar.Items属性的get访问符,并依次将该段XAML中所声明的子元素添加到Items属性所记录的集合中。而在分析第二段XAML的时候,WPF将首先创建一个ToolbarItemCollection,并将所有的子元素添加到该集合之中。在该集合创建完毕之后,WPF将调用Toolbar.Items属性的set访问符,以将该集合设置为Toolbar.Items属性的值。
转载请注明原文地址:http://www.cnblogs.com/loveis715/p/4343374.html
商业转载请事先与我联系:silverfox715@sina.com,我只会要求添加作者名称以及博客首页链接。
WPF - 属性系统 (4 of 4)的更多相关文章
- WPF - 属性系统 (3 of 4)
依赖项属性元数据 在前面的章节中,我们已经介绍了WPF依赖项属性元数据中的两个组成:CoerceValueCallback回调以及PropertyChangedCallback.而在本节中,我们将对其 ...
- WPF - 属性系统 (2 of 4)
属性更改回调 前一章的示例中,对各个参数的设置都非常容易理解.如果我们仅仅需要创建一个独立的依赖项属性,那么上面所提到的创建依赖项属性的基础知识足以满足需求.但是事情往往并非如此完美.在一个系统中,很 ...
- WPF - 属性系统 (1 of 4)
本来我希望这一系列文章能够深入讲解WPF属性系统的实现以及XAML编译器是如何使用这些依赖项属性的,并在最后分析WPF属性系统的实际实现代码.但是在编写的过程中发现对WPF属性系统代码的讲解要求之前的 ...
- WPF - 属性系统 - APaas(AttachedProperty as a service)
是的,文章的题目看起来很牛,我承认. 附加属性是WPF中的一个非常重要的功能.例如在设置布局的过程中,软件开发人员就常常通过DockPanel的Dock附加属性来设置其各个子元素所处的布局位置.同样地 ...
- WPF 属性系统 依赖属性之内存占用分析
关于WPF的属性系统园子内有不少这方面的文章.里面大都提到了WPF依赖属性的在内存方面的优化.但是里面大都一笔带过.那么WPF到底是怎么样节约内存的.我们通过WPF属性和普通的CLR属性对比来看一下W ...
- wpf控件开发基础(3) -属性系统(2)
原文:wpf控件开发基础(3) -属性系统(2) 上篇说明了属性存在的一系列问题. 属性默认值,可以保证属性的有效性. 属性验证有效性,可以对输入的属性进行校验 属性强制回调, 即不管属性有无发生变化 ...
- wpf控件开发基础(4) -属性系统(3)
原文:wpf控件开发基础(4) -属性系统(3) 知识回顾 接上篇,上篇我们真正接触到了依赖属性的用法,以及依赖属性的属性元数据的用法,并且也实实在在地解决了之前第二篇提到的一系列问题.来回顾一下 属 ...
- wpf控件开发基础(2) -属性系统(1)
原文:wpf控件开发基础(2) -属性系统(1) 距离上篇写的时间有1年多了.wpf太大,写的东西实在太多,我将依然围绕着自定义控件来展开与其相关的技术点. 也欢迎大家参与讨论.这篇我们将要讨论的是W ...
- WPF布局系统[转]
转自:http://www.cnblogs.com/niyw/archive/2010/10/31/1863908.html前言 前段时间忙了一阵子Google Earth,这周又忙了一阵子架构师论文 ...
随机推荐
- 消息队列——RabbitMQ学习笔记
消息队列--RabbitMQ学习笔记 1. 写在前面 昨天简单学习了一个消息队列项目--RabbitMQ,今天趁热打铁,将学到的东西记录下来. 学习的资料主要是官网给出的6个基本的消息发送/接收模型, ...
- 关于 Chrome 浏览器中 onresize 事件的 Bug
我在写插件时用到了 onresize 事件,在反复地测试后发现该事件在 Chrome 及 Opera(内核基本与 Chrome 相同,以下统称 Chrome)浏览器打开时就会执行,这种情况也许不能算作 ...
- 虾扯蛋:Android View动画 Animation不完全解析
本文结合一些周知的概念和源码片段,对View动画的工作原理进行挖掘和分析.以下不是对源码一丝不苟的分析过程,只是以搞清楚Animation的执行过程.如何被周期性调用为目标粗略分析下相关方法的执行细节 ...
- Javascript生成二维码(QR)
网络上已经有非常多的二维码编码和解码工具和代码,很多都是服务器端的,也就是说需要一台服务器才能提供二维码的生成.本着对服务器性能的考虑,这种小事情都让服务器去做,感觉对不住服务器,尤其是对于大流量的网 ...
- 0-1背包问题蛮力法求解(c++版本)
// 0.1背包求解.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #include <iostream> #define ...
- Kooboo CMS技术文档之五:站点配置管理
站点关系 管理站点间的关系,站点可以有子站点,子站点继承父站点的部分配置数据,同时子站点还可以根据需要,本地化由父站点继承而来的数据.通过继承和本地化,可以让子站点在用最小的改动代价,来完成一个与父站 ...
- Mac OS 使用 Vagrant 管理虚拟机(VirtualBox)
Vagrant(官网.github)是一款构建虚拟开发环境的工具,支持 Window,Linux,Mac OS,Vagrant 中的 Boxes 概念类似于 Docker(实质是不同的),你可以把它看 ...
- JAVA面试题
在这里我将收录我面试过程中遇到的一些好玩的面试题目 第一个面试题:ABC问题,有三个线程,工作的内容分别是打印出"A""B""C",需要做的 ...
- iOS 小知识点(持续更新)
1.如何通过代码设置Button title的字体大小 设置Button.titleLabel.font = [UIFont systemFontOfSize:<#(CGFloat)#> ...
- iOS之开发中常用的颜色及其对应的RGB值
R G B 值 R G B 值 R G B 值 黑色 0 0 0 #000000 黄色 255 255 0 #FFFF00 浅灰蓝色 176 224 230 #B0E0E6 象牙黑 41 ...