首页 > 代码库 > 8 WPF学习之深入浅出话属性
8 WPF学习之深入浅出话属性
转载:http://blog.csdn.net/fwj380891124/article/details/8131080
通过前面的学习,我们已经知道Data Binding是WPF“数据驱动UI”理念的基础。上一章我们将主要的精力放在了Binding的数据源这一端,研究了Binding的Source和Path。本章我们将把目光移向Binding的目标端,研究一下什么样的对象才能作为Binding的Target以及Binding将把数据送往何处。
1.1 属性(Property)的来龙去脉
程序的本质就是“数据+算法”,或者说用算法来操作数据来得到自己想要的结果。在程序中,数据表现为各种各样的变量,算法则表现为各种各样的函数(操作符是函数的简记法)。即使是到了面向对象时代有了类等数据结构的出现,这一本质仍然没有改变---类的作用只是将散落在程序中的变量和函数进行归档封装并控制对它们的访问而已。被封装在类中的变量称为字段,它表示的是类或实例的状态;被封装在类里的函数叫做方法,它表示的是类或实例的功能。字段和类构造出了最原始的面向对象封装,这时候的面向对象中还不包含事件,属性等概念。
我们可以使用Private、Public来控制字段或方法的可访问性:是否使用static关键字来修饰字段或者方法则决定了字段或方法是对类有意义还是对类的实例有意义。所谓“对类有意义”或者“对实例有意义”都是语言范畴的概念。比如对于Human这个类来说,Weight这个字段对于人类个体是有意义的而对于人类这个概念并没有什么意义。Amount这个字段就不一样了,它对于人类个体并没有什么意义,但是它对于人类是有意义的。方法也有类似的情况,Speak这个函数,只有人类个体才能Speak,而Populate(繁衍)这个方法对于人类来说比人类个体更具有意义。
注:为了让程序满足语义要求,C#语言规定:对类有意义的字段或方法用static关键字修饰,称为静态成员。通过类名+访问操作符就可以访问到他们;对类的实例或方法有意义的字段不用static关键字修饰,称为非静态成员或者实例成员。
从语义上来看,静态成员和非静态成员有着很好的对称性,但从程序在内存中的结构来看,这种对称就被打破了。静态字段在内存中只有一个拷贝,非静态字段则每个实例都有一个拷贝,无论方法是静态还是非静态,在内存中都只有一个拷贝,区别只是你能通过类名来访问存在内存中的指令还是通过实例来访问这个实例。
现在让我们来看看属性是怎么演变出来的。字段被封装在实例里,要么能被外接访问,要么不能,如下图所示:
这种直接把数据暴露给外界的做法很不安全,很容易就把错误的值写入字段。如果在每次写入字段的时候先判断一下值的有效性又会增加冗余的代码并违反了面向对象要求的“高内聚”的原则,我们希望对象自己有能力判断将被写入值是否正确,于是,程序员仍然把字段定义为private,而使用一对非private方法来包装它。在这个方法中,一个以Set为前缀负责判断数据的有效性并写入数据。另一个以Get为前缀的负责把字段里的数据读出来。如下图:
以Human为实例,如果我们把类设计成这样:
1: public class Human
2: {
3: public int Age;
4: }
那么当有类进行这样的操作的时候,就有可能不被察觉:
1: Human human = new Human();
2: human.Age = -20;
3: human.Age = 2000;
但把类设计成这样:
1: private int age;
2:
3: public void SetAge(int value)
4: {
5: if (value > 0 && value < 100)
6: {
7: age = value;
8: }
9: else
10: {
11: throw new OverflowException("age overflow!");
12: }
13: }
14:
15: public int GetAge()
16: {
17: return this.age;
18: }
情况就会好很多。但如果去掉SetAge方法,或者将SetAge方法设置为private,那么对数据的访问就变成了只读形式(Read-Only)。很多传统的类库使用的就是这种数据封装和访问方法,例如MFC就是这样。我们称这对Get/Set方法为private的安全包装。
使用两个方法包装一个字段的办法已然不错,但还是很麻烦。书写的代码很分散,使用的使用又在自动提示里面翻动。于是,当Framwork推出以后,微软进一步对Get/Set这对方法进行了封装合并成了属性(Property)。使用属性的时候,格式上很像使用非private字段,保证了语义上的顺畅,同时又不失Get/Set方法的安全性,代码也变的更加紧凑,自动提示菜单也短了很多,可谓一举多得。使用属性,Human类就可以改成这样:
1: public class Human
2: {
3: private int age;
4:
5: public int Age
6: {
7: get
8: {
9: return age;
10: }
11: set
12: {
13: if (age > 0 && age < 100)
14: {
15: age = value;
16: }
17: else
18: {
19: throw new OverflowException("age overflow!");
20: }
21: }
22: }
23: }
这种.netFrameWork中属性又称之为CLR属性(CLR,Common Language Runtime)。我们既可以说CLR属性是private字段的安全访问包装,也可以说一个private字段在后台支持(back)一个CLR属性。这个模式可以用下图进行表示:
最后还有一个小问题,实例的每个private字段都会占用一定的内存,现在字段被属性包装起来,每个实例看上去都带有相同的属性,那么是不是每个对象的CLR属性也会多占一点内存呢?想得到这个答案,使用IL反编译器打开编译结果如图7-4所示:
原来C#代码中属性编译的结果是两个方法!前面已经说过,再多实例方法也只有一个拷贝,所以CLR属性并不会增加内存的负担。同样也说明,属性仅仅是个语法糖衣(Syntax Sugar)。
1.2 依赖属性(Dependency Property)
在WPF中,微软将属性又往前推了一步,推出了“依赖属性”这个新概念。简言之,依赖属性就是可以自己没有值,并能够通过Binding从数据源获取值(依赖在别人身上)的属性。拥有依赖属性的对象被称为“依赖对象”。与传统的CLR属性和面向对象相比依赖属性有很多新颖之处,其中包括:
- 节省实例对内存的开销。
- 属性值可以通过Binding依赖在其它对象上。
1.2.1 依赖属性对内存的使用方式
依赖属性较之CLR属性在内存使用方面迥然不同。前面已经说过,实例的CLR属性都包装着一个非静态的字段(或者说由一个非静态的字段在后台支持)。思考这样一个问题:TextBox有138个属性,假设每个CLR属性都包装着一个4字节的字段,如果程序运行的时候创建了一个10列1000行的的一个TextBox列表,那么这些字段将暂用4*138*10*100=5.26M内存!在这100多个属性里面,最常用的也就Text属性,这就是说大多数内存会被浪费掉。
怎么避免这种浪费呢?让我们去思考一个现实世界中存在的问题:一个登山队员,他的全套装备有很多,包括登山服、登山靴、登山仗、护目镜、绳索、无线电、水、食品甚至还有氧气瓶等。倘若是去等珠穆朗玛峰,这些装备都要带上,要是去登香山呢?如果也背着氧气瓶岂不怪哉!所以,实际的一点办法就是---用得着的就带上,用不着的就不带,有必要的时候可以借别人的用一下。
其实,这就是WPF中依赖属性的原理。传统的.NET开发中,一个对象所暂用的内存空间在调用New操作符进行实例化的时候就已经决定了,而WPF允许对象在被创建的时候并不包含用于存储数据的空间(即字段所占用的空间)、只保留在需要用到数据的时候能够获得默认值。借用其它对象的数据或者实时分配空间的能力----这种对象称为依赖对象而他这种实时获取数据的能力则依靠依赖属性来实现。在WPF开发中,必须使用依赖对象作为依赖属性的宿主,使二者结合起来,才能形成完整的Binding目标被数据所驱动。
在WPF系统中,依赖对象的概念被DependencyObject类所实现。依赖属性的概念则由DependencyProperty来实现。DependencyObject具有GetValue和SetValue两个方法:
1: public class DependencyObject : DispatcherObject
2: {
3: public object GetValue(DependencyProperty dp)
4: {
5: //....
6: }
7:
8: public void SetValue(DependencyProperty dp,object value)
9: {
10: //....
11: }
12: }
这两个方法都以DependencyProperty作为参数,GetValue是通过DependencyProperty获取对象;SetValue通过DependencyProperty对象存储值-----正是这两个方法把DependencyObject和DependencyProperty紧密的结合在了一起。
DependencyObject是WPF系统中相当底层的一个基类,如下图所示:
从这棵继承树上可以看出,WPF的所有控件都是依赖对象。WPF的类库在设计的时候充分利用了依赖属性的优势,UI控件的绝大多数属性已经依赖化了。
1.2.2 声明和使用依赖属性
下面我们通过一个简单的实例来说明依赖属性的使用方法。
1: <Window x:Class="WpfApplication1.Window19"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: Title="Window19" Height="300" Width="300">
5: <StackPanel Background="AliceBlue" Name="stackPanel1" Margin="10">
6: <TextBox Height="23" Name="textBox1" Width="200" Margin="10"/>
7: <TextBox Height="23" Name="textBox2" Width="200" Margin="10"/>
8: <Button Content="Test" Height="23" Name="button1" Width="75" />
9: </StackPanel>
10: </Window>
运行效果如下:
前面已经说过,DependencyProperty必须以DependencyObject作为宿主,借助它的SetValue和GetValue进行记录和读取。因此,想使用自定义的DependencyProperty,宿主一定是DependencyObject的派生类。DenpendencyProperty实例声明特点很鲜明----引用变量由public static readonly三个修饰符进行修饰,实例并非使用new操作符得到而是使用DependencyProperty.Register方法实现,代码如下:
1: public class Student:DependencyObject
2: {
3: public static readonly DependencyProperty NameProperty = DependencyProperty.Register("Name", typeof(string), typeof(Student));
4: }
这是使用DependencyProperty的最简代码,我们来分析一下:
首先,如前所诉,DependencyProperty一定存在于DependencyObject里面,所以Student派生自DependencyObject类。
其次,使用DependencyProperty的时候需要同时使用public static readonly修饰。在这里我们遇到了一个命名约定----成员变量的名字需要加入Property的后缀表明它是一个依赖属性。我们打算用这个依赖属性存学生的姓名,所以把它命名为NameProperty。
再次,这个成员变量所引用的实例并非使用new操作符得到,而是使用DependencyProperty.Register方法创建。现在使用的这个方法参数最少、最简单的一种重载,下面分析一下这三个参数:
- 第一个参数为string类型,用这个参数指明哪个CLR属性作为这个依赖属性的包装器,或者说此依赖属性支持的是哪个CLR属性。目前虽然没有准备为这个依赖属性准备包装器,但将来会使用名为Name的CLR属性来包装它,所以这个参数被赋值为Name。
- 第二个参数用来指明此依赖属性用来存储什么类型的值,学员的姓名是string类型的值,所以这个参数被赋值为typeof(string)。
- 第三个参数用来指明此依赖属性的宿主是什么类型,或者说DependencyProperty.Register方法将这个依赖属性关联到哪个类型上。本例的意图是为Student类准备一个可依赖的名称属性,所以需要将NameProperty和Student进行关联。因此该参数被赋值为typeof(Student)。
注意:
(1)依赖属性包装器(Wrapper)是一个CLR属性,因为初学者脑中“属性”就是CLR属性,所以常常把包装器误认为是依赖属性。而实际上依赖属性就是由public static readonly修饰的DenpendencyProperty实例,有没有包装器这个实例都存在。
(2)既然没有包装器依赖属性都存在,那么包装器是用来做什么的呢?包装器的作用是以“实例属性”的形式向外界暴露依赖属性,这样一个依赖属性才能成为数据源的path。
(3)注册依赖属性的时候第二个参数是一个数据类型,这个数据类型也是包装器的数据类型,它的全称应该是“依赖属性的注册类型”,但一般情况下也会称做为“依赖属性的类型”(严格的所,依赖属性的类型永远都是DependencyProperty,只是工作中叫习惯了)。
理解了依赖属性声明变量和创建实例的过程,我们可以尝试使用它了。依赖属性首先是属性,所以我们先尝试用这个属性来存储值并把存储的值顺利的读取出来。
按钮下面的代码如下:
1: private void button1_Click(object sender, RoutedEventArgs e)
2: {
3: Student stu = new Student();
4: stu.SetValue(Student.NameProperty, textBox1.Text);
5:
6: textBox2.SetValue(TextBox.TextProperty, stu.GetValue(Student.NameProperty));
7: }
第一句是创建一个Student实例并创建stu引用;第二句是调用SetValue方法把textBox1的值赋值给Student的依赖属性。第三句是使用GetValue将值从依赖属性里面读取出来。注意,GetValue的返回值是一个Object类型,所以要适当的进行类型转换。如前所述,Student的SetValue和GetValue方法继承自DependencyObject类。
程序运行效果如下图:
当第一次看到这个例子的时候,也许会有点百思不得其解的感觉---依赖属性是一个static对象,哪怕有1000个student实例,依赖属性对象也只有一个,那么调用SetValue的时候值被存到哪里去了?调用GetValue又如何将值读取出来?而且ReadOnly关键字修饰的变量不是只读的吗?那么怎么可能写入值呢?其实这个问题直指依赖属性的核心,我们会在后面讲到,现在我们把思维先集中在依赖属性的使用上。
上面的例子,依赖属性做为“属性”的功能已经体现出来,但是,如何体现出依赖呢?让我们先看下面一个例子。先回顾一下Binding,Binding作为数据流动的桥梁,一端是数据来源,一段是数据目标。一般情况下数据来源是业务逻辑层对象而目标就是UI上的控件。在下面这个例子里面,我们暂且倒过来,让textBox1作为数据源,把Student实例作为目标,让Student实例依赖在TextBox上。注意,这仅仅是为了展示依赖属性的依赖功能,现实中几乎不可能去这样做。
下面是窗口类的后台代码:
1: Student stu;
2: public Window19()
3: {
4: InitializeComponent();
5: stu = new Student();
6: Binding bind = new Binding("Text") { Source=textBox1};
7: BindingOperations.SetBinding(stu, Student.NameProperty, bind);
8: }
9:
10: private void button1_Click(object sender, RoutedEventArgs e)
11: {
12: //Student stu = new Student();
13: //stu.SetValue(Student.NameProperty, textBox1.Text);
14:
15: textBox2.SetValue(TextBox.TextProperty, stu.GetValue(Student0.NameProperty));
16: }
最核心的代码位于构造器中的最后两段代码,先创建一个Binding实例,让TextBox1做为数据源对象并从Text属性中获取数据;最后一句使用BindingOperations的SetBinding方法指定将stu对象借助刚刚绑定的实例依赖在TextBox1上。
当在TextBox1中输入Darren的时候,出现的结果和上一个图片一样。
说实话,这种“学院派”的例子不怎么实用,但通过它我们认清了一个事实,那就是依赖属性接是没有CLR属性外包装器也可以很好的工作。
代码的进化并没有结束。如果想把TextBox1和TextBox2关联起来,代码应该是这样:
1: BindingOperations.SetBinding(stu, Student0.NameProperty, bind);
2: //---
3: Binding bind0 = new Binding("Text") { Source=textBox1};
4: textBox2.SetBinding(TextBox.TextProperty, bind0);
5: //---
这里调用了textBox2的SetBinding方法,这比使用BindingOperations调用SetBinding的方法以第三人称的视角将数据源和数据目标关联起来感觉要自然一些。如果你尝试调用stu的SetBinding方法,你会发现stu没有这个方法。因为DependencyObject类没有这个方法,SetBinding是FramWorkElement类的方法。FramWorkElement是一个相当高层的类,甚至比UIElement的级别还高---这从侧面给我们传递了一个思想----微软希望能够SetBinding的对象元素是UI元素。其实FramWorkElement类的SetBinding方法并不神秘,仅仅对BindingOpertions的SetBinding方法做一个简单的封装,代码如下:
1: public class FrameworkElement : UIElement
2: {
3: //...
4: public BindingExpressionBase SetBinding(DependencyProperty dp, BindingBase bind)
5: {
6: return BindingOperations.SetBinding(this, dp, bind);
7: }
8: //....
9: }
看完这几个例子,相信大家已经理解了依赖属性的使用方法。但是现在我们使用依赖属性依靠SetValue和GetValue两个方法进行外界的暴露,而且在使用GetValue的时候还要做一次数据类型转换,因此,大多数情况下我们会对依赖属性做一个CLR属性外包装:
1: public class Student:DependencyObject
2: {
3: public string Name
4: {
5: get
6: {
7: return (string)GetValue(NameProperty);
8: }
9: set
10: {
11: SetValue(NameProperty, value);
12: }
13: }
14: public static readonly DependencyProperty NameProperty = DependencyProperty.Register("Name", typeof(string), typeof(Student));
15: }
有了这个CLR属性包装我们就可以这样访问依赖属性了:
1: private void button1_Click(object sender, RoutedEventArgs e)
2: {
3: stu.Name = textBox1.Text;
4: textBox2.Text = stu.Name;
5: }
如果不关心底层的实现,下游程序员在使用依赖属性是与使用单纯的CLR属性感觉别无二致。
我们知道Binding对象可以通过Binding依赖在其它对象上,即依赖对象是作为数据目标而存在的。现在我们为依赖对象的依赖属性添加的CLR属性包装,有了这个包装,就相当于为依赖对象准备了用于暴露数据的Path,也就是说,现在依赖对象已经具备了扮演数据源和数据目标的双重角色。值得注意的是,尽管Student类没有实现INotifyPropertyChanged接口,当属性的值发送改变时与之关联的binding对象依然可以得到通知,依赖属性默认的带有这种功能,天生就是合格的数据源。
现在我们向FramWork类借用一下它的SetBinding方法,升级一下Student类:
1: public class Student:DependencyObject
2: {
3: public string Name
4: {
5: get {
6: return (string)GetValue(NameProperty);
7: }
8: set
9: {
10: SetValue(NameProperty, value);
11: }
12: }
13:
14: public static readonly DependencyProperty NameProperty = DependencyProperty.Register("Name", typeof(string), typeof(Student));
15: public BindingExpressionBase SetBinding(DependencyProperty dp, BindingBase bind)
16: {
17: return BindingOperations.SetBinding(this,dp,bind);
18: }
19: }
1: Student0 stu;
2: public Window19()
3: {
4: InitializeComponent();
5: stu = new Student0();
6: Binding bind = new Binding("Text") { Source=textBox1};
7:
8: stu.SetBinding(Student0.NameProperty, bind);
9:
10: textBox2.SetBinding(TextBox.TextProperty, new Binding("Name") { Source=stu});
11:
12: }
运行程序的时候,当TextBox1的时候中输入字符的时候,TextBox2也会同步显示。当然,此时的Student对象的Name属性值也同步发生变化了。
注意:
在一个类中声明依赖属性并不需要手动进行声明、注册并使用CLR属性进行封装,只需要输入propdp,VisualStudio提示列表就会有一项高亮显示,连续按两次Tab键,一个标准的依赖属性(带CLR属性包装)就声明好了,再按动Tab键,可以在提示环境中修改依赖属性的各个参数。这个功能称为snippet(称为代码模板或代码片段),这是VisulaStudio中所有非简化版本自带的功能,多多掌握这个功能可以大大的提高编码速率和降低错误率。
有snippet自动生成的代码中,DependencyProperty.Register使用的是带4个参数的重载,前三个参数和我们之前介绍的一致,第4个参数的类型是PropertyMetaData类。第4个参数是给依赖属性的DefaultMetaData属性赋值。顾名思义,DefaultMetaData属性是为了向依赖属性的调用者提供一些基本信息,这些信息包括:
- CoerceValueCallBack:依赖属性值被强制改变的时候该委托会被调用,此委托可以关联一个响应函数。
- DefaultValue:依赖属性未被显示赋值的时候,若读取之则获得此默认值,不设此值会抛出默认值。
- IsSealed:控制PropertyMetaData的属性值是否可以更改,默认值为true;
- PropertyChangedCallBack:依赖属性的值被更改的时候该委托会被调用,此委托可以关联一个影响函数。
注意:
依赖属性的DefaultMetaData只能通过Register方法的第四个参数进行赋值,而且一旦赋值就不能改变(DefaultMetaData是个只读属性)。如果想用新的PropertyMetadata替换这个Metadata,需要使用DependencyProperty.OverrideMetadata方法。
1.2.3 依赖属性存取值的秘密
回到前面那个问题----调用依赖对象的SetValue方法时,值被存储到哪里了呢?因为依赖对象的依赖属性是一个static对象,所以值不可能保存在这个对象里面,不然几百个实例都进行赋值的时候到底应该保存在哪里,丢掉哪个?显然,WPF有一套机制来存储依赖属性的值。下面让哦我们来剖析一下。
回想前面学习的类容,不难发现依赖属性的使用大致分为两个步骤:第一步,在DependencyObject的派生类中声明public static修饰的DependencyProperty成员变量,并使用DependencyProperty的Register方法(而不是new操作符)获得DependencyProperty实例;第二步,使用DependencyObject的SetValue和GetValue方法,借助DependenctyProperty实例来存取值。因此,我们要重点分析的就是DependencyProperty.Register和DependencyProperty.SetValue和DependencyProperty.GetValue方法。
先来研究DependencyProperty的Register方法。顾名思义,这个方法不仅要创建DependencyProperty实例,还要对它进行注册。这样问题就来了,DependencyProperty被注册到哪里了呢?
阅读源码你会发现DependencyProperty具有一个这样的全局Hashtable存在,这个Hshtable就是用来注册DependencyProperty实例的地方。
1: public static Hashtable PropertyFromName = new Hashtable();
显然,一旦程序运行,就会有这样一个全局的Hashtable存在,这个Hashtable就是用来注册DependencyProperty实例的地方。
在源码中,所有DependencyProperty.Register方法重载归到对DependencyProperty的RegisterCommon调用(可以把RegisterCommon方法理解为Register的完整版),RegisterCommon方法的原型如下:
1: private static DependencyProperty RegisterCommon
2: {
3: string name,
4: Type propertyType,
5: Type ownerType,
6: PropertyMetaData defaultMetaData,
7: ValidateValueCallback validateValueCallback)
8: }
可以看到,RegisterCommon的前4个参数与我们分析的Register方法一致。下面来看一下这个方法如何操作它的参数。
在刚刚进入方法的时候,你会看到这么一句:
- FromNameKey key = new FromNameKey(name,ownerType);
FromNamekey是一个.NETFrameWork内部数据类型,它的构造器如下:
1: public FromNameKey(string name,Type ownerType)
2: {
3: _name = name;
4: _ownerType = ownerType;
5: _hashCode = _name.GetHashCode()^_ownerType.GetHashCode();
6: }
1: public override int GetHashCode()
2: {
3: return _hashCode;
4: }
代码的意图一目了然:FromNameKey对象(也就是变量key)的hashCode实际上是RegisterCommon的第一个参数(CLR属性名称字符串)的hashcode与第三个参数(宿主类型)的hashCode做异或运算得来的。这样操作,每对“CLR属性---宿主类型”所决定的Dependency实例就是唯一的。所以,在RegisterCommon方法里面发现了这样的代码:
1: if(PropertyFromName.Contains(key))
2: {
3: throw new ArgumentException(SR.Get(SRID.PropertyAlreadyRegistered,name,ownerType.Name));
4: }
也就是说,如果你使用同一个CLR属性名称和同一个宿主类型注册,程序会抛出异常。
接下来,RegisterCommon检测程序员是否提供了PropertyMetaData,如果没有准备则提供一个默认的PropertyMetaData实例。当所有的原料都准备妥当,没有问题之后,DependencyProperty就被创建出来了。
1: DependencyProperty dp = new DependencyProperty(name,propertyType,ownerType,defaultMetaData,validateValueCallBack);
并且被注册进HashTable中(hashTable会自动调用key的GetHashCode方法获取hashcode)
- PropertyFromName[key] = dp;
读到这里,我们可以用一句话来概括Dependency对象的创建也注册,那就是:创建一个DependencyProperty实例并用它的CLR属性名和宿主类型生成hashCode,最后把hashcode和DependencyProperty实例作为Key-Value对存入全局中。名为PropertyFromName的HashTable中。这样,WPF属性系统就可以通过CLR属性名和宿主类型名就可以从这个全局的HashTable中检索出对应的DependencyProperty实例。
最后,生成的DependencyProperty实例当作返回值交还。
1: return dp;
注意:
把DependencyProperty实例注册进全局HashTable使用的Key由CLR属性名哈希值和宿主类型哈希值经过运算得到,但这并不是DependencyProperty的哈希值。每个DependencyProperty都有一个名为GloballIndex的int类型属性,GlobalIndex的值是经过一些算法处理得到的,确保每个DependencyProperty实例的GloballIndex是唯一的。
并且,DependencyProperty的GetHashCode亦被重写。
1: public override int GetHashCode()
2: {
3: return GloballIndex;
4: }
所以GloballIndex属性值也就是DependencyProperty实例的哈希值---这一点非常重要,因为通过这个值可以直接检索到某个DependencyProperty实例。
至此,一个DependencyProperty实例已经被注册进了HashTable中,下面就是使用DependencyObject的SetValue和GetValue借助这个DependencyProperty实例保存和读取值了。我们先来看到相对比较简单的getValue方法,它的代码如下:
1: public Object GetValue(DependencyProperty dp)
2: {
3: this.VerifyAccess();
4: if(dp==null)
5: {
6: throw new ArgumentNullException("dp");
7: }
8: //Call Forwarded
9: return GetValueEntry(LookupEntry(dp.GloballIndex),dp,null,RequestFlags.FullyResolved).value;
10: }
1: EntryIndex entryIndex = LookUpEntry(dp.GloballIndex);
2: EffectiveValueEntry valueEntry = GetValueEntry(entryIndex,dp,null,RequestFlags.FullyResolved);
3: return valueEntry.Value;
这几句代码中屡次出来的Entry这个词,Entry是“入口”的意思。WPF依赖属性系统在存放值的时候会把每个值存放进一个小房间,每个小房间都有自己的入口--检索算法只是要找到这个入口,走进入口就能拿到依赖属性的值。这里说的小房间指的就是EffectiveValueEntry类的实例。EffectiveValueEntry所有构造器都包含一个DependencyProperty类型的参数,换句话说,每个EffectiveValueEntry类都关联这个一个DependencyProperty。EffectiveValueEntry类具有一个PropertyIndex的属性,这个值实际上就是与之关联的DependencyProperty的GloballIndex的值。
在DependencyObject类中可以找到这样一个变量:
1: private EffectiveValueEntry[] _effectiveValues;
这个数组依每个成员的PropertyIndex属性值进行排序,对这个数组的操作(插入,删除,排序)由专门的算法来完成。正是这个数组提示了我们依赖属性存储的密码---每个DependencyObject都自带一个EffectiveValueEntry类型数组,当某个依赖属性的值要被读取的时候,算法就会从这个数组中去检索,如果数组中没有包含这个值,算法会返回依赖属性的默认值(这个值有依赖属性的DefaultMetaData自动提供)。
至此,我们明白了一件事情,那就是被Static关键字所修饰的依赖属性对象其作用是用来检索真正的属性值而不是存储值;被用来检索值的实际上是依赖对象的GlobIndex属性(本质是Hashcode,而HashCode又由属性包装器和宿主类型共同决定),为了保证GlobIndex的稳定性,我们又使用了ReadOnly关键字进行了修饰。
实际工作中,依赖属性的值除了可以存放在EffectiveValueEntry宿主或由默认值提供外,还有很多途径可以获得,可能来自于元素的Style或Theme。也可能由上层元素继承而来,还可能是某个动画控制下不断变化而来。我们怎么知道获取的值来自于哪里呢?原来WPF对依赖属性的读取是有优先级控制的,先后顺序如下:
(1)WPF属性系统强制性。
(2)由动画过程中控制的值。
(3)本地变量值(EffectiveValueEntry数组中的值)。
(4)由上级元素Template设置的值。
(5)由隐式样式控制的值。
(6)由样式之触发器控制的值。
(7)由模板之触发器控制的值。
(8)由样式之设置器设置的值。
(9)由默认样式设置的值。默认模式其实就是由主题指定的模式。
(10)由上级元素继承而来的值。
(11)默认值,来源于依赖属性的元数据(metadata)。
理解了GetValue,SetValue也就不在神秘。
- 进入这个方法后,首先验证依赖属性的值是否可以被改变,如果不能则抛出异常,如果可以就进入后面的赋值流程。赋值流程也很简单,主要有这样几个操作:
- 检测值是不是DependencyProperty.UnsetValue,如果是,说明调用者的意图是清空现有的值。此时程序会调用ClearValueCommon方法来清空现有值。
- 检测EffectiveValueEntry数组中是否已经存在相应的依赖属性的位置,如果有,则把旧的值改为新值;如果没有,就创建EffectiveValueEntry实例并存储新值。这样,只有被用用到的值才会放进这个列表,借此,WPF系统用算法(时间)换取了对内存控件的节省。
- 调用UpdateEffectValue对新值做一些对应的处理。
DependencyObject和DependencyProperty是WPF中属性系统的核心。通过上面的介绍,我们了解到了WPF的设计理念,即public static 类型的变量作为标记并以这个标记为索引进行对象的存储,访问,删除等操作。这一种理念在传统的.net系统中(ASP.NET,wiNFORM)是不曾出现的,它是WPF的创新并广泛使用的(后面的路由事件,系统命令都会用到这样的理念),同时我们也理解为什么WPF在性能上还不尽人意,微软也在不断的完善这个机制,使其效率进一步提升。
1.3 附加属性(Attached Properties)
理解了依赖属性以后,再来看看附加属性。顾名思义,附加属性就是说一个属性本来不属于某个对象,但由于某种需求后来又被附加上了。也就是说把对象放入一个特定的环境对象才能拥有该属性,这种属性就是附加属性。实际开发工作中,我们经常会遇到这种情况,比如一个名为Human的类,它有可能被与学校相关的工作流用到(记录它的班级,年级等信息),也有可能被与学校相关的工作流用到(记录他的部门,项目)。那么设计类的时候我们是不是要这样做呢?
1: public class Human
2: {
3: public int Id { get; set; }
4: //For School Overflow
5: public int ClassId { get; set; }
6: public int MajorId { get; set; }
7: public int GradeId { get; set; }
8:
9: //For comapany OverFlow
10: public int DepartmentId { get; set; }
11: public int ProjectId { get; set; }
12: }
显然这样做不太合适,因为流程一旦有所变化,这个类的实现就需要改动,也就是说这个类总是不能被关闭的。而且,如果某些Human类型的实例只用于公司的相关流程,那么其它的School OverFlow中的属性占用的内存就太浪费了。
再回想一下我们布局中遇到的例子。如果Grid中给一个TextBox定位,代码回事这样:
1: <Window x:Class="WpfApplication1.Window20"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: Title="Window20" mc:Ignorable="d" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Height="287" Width="416">
5: <Grid ShowGridLines="True">
6: <Grid.ColumnDefinitions>
7: <ColumnDefinition />
8: <ColumnDefinition />
9: <ColumnDefinition />
10: </Grid.ColumnDefinitions>
11: <Grid.RowDefinitions>
12: <RowDefinition />
13: <RowDefinition />
14: <RowDefinition />
15: </Grid.RowDefinitions>
16: <TextBox Name="textBox1" Grid.Row="1" Grid.Column="1" Background="LightBlue"/>
17: </Grid>
18: </Window>
运行效果如下图所示:
如果TextBox放置在Canvas里面,则代码会是这样:
1: <Window x:Class="WpfApplication1.Window20"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: Title="Window20" mc:Ignorable="d" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Height="287" Width="416">
5: <Canvas Name="canvas1" Margin="10">
6: <TextBox Width="200" Canvas.Top="0" Background="Lime"></TextBox>
7: <TextBox Width="200" Canvas.Top="30" Background="Lime"></TextBox>
8: <TextBox Width="200" Canvas.Top="60" Background="Lime"></TextBox>
9: </Canvas>
10: </Window>
运行效果如下图:
放在DockPanel里面,代码会是这样:
1: <Window x:Class="WpfApplication1.Window20"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: Title="Window20" mc:Ignorable="d" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Height="287" Width="416">
5: <DockPanel LastChildFill="False">
6: <TextBox Background="Orange" DockPanel.Dock="Top"></TextBox>
7: <TextBox Background="Orange" DockPanel.Dock="Bottom"></TextBox>
8: <TextBox Background="Green" DockPanel.Dock="Left" Width="80"></TextBox>
9: <TextBox Background="Green" DockPanel.Dock="Right" Width="80"></TextBox>
10: </DockPanel>
11: </Window>
运行效果如下图:
放在StackPanel里最省事:
1: <Window x:Class="WpfApplication1.Window20"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: Title="Window20" mc:Ignorable="d" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Height="287" Width="416">
5: <StackPanel>
6: <TextBox Background="LightBlue" Margin="0,5"></TextBox>
7: <TextBox Background="LightBlue" Margin="0,5"></TextBox>
8: <TextBox Background="LightBlue" Margin="0,5"></TextBox>
9: </StackPanel>
10: </Window>
运行效果如下图:
作为TextBox的设计者,他不着调控件发布以后程序员会把它放在Grid里面还是Canvas里面(甚至以后还有可能推出新的布局里),所以他也不可能为TextBox准备诸如Column、Row或者Left、Right属性,那么干脆让布局来设置它的位置吧!放在Grid里面让Grid为它附加Column属性。放在Canvas里面就让Canvas为它附加上Top和Left属性。放在DockPanel里面,就让DockPanel为它附加Dock属性。可见,附加属性就是做用就是将属性于宿主解耦,让数据类型设计更加灵活。
理解了附加属性的含义,我们开始研究附加属性的声明,注册和使用。附加属性的本质就是依赖属性。二者仅在包装器和注册上有一点区别。前面已经讲过,VS里面自带的有用于快速创建依赖属性的snippet和propdp,现在使用另外一个snippet用于快速创建附加属性propa。当VS出现高亮显示的时候连续按两次Tab键,一个附加属性框架就准备好了。继续按Tab键可以在几个空缺间轮换并修改,直至按下Enter键。
下面的代码是做好“完型填空”的代码:
1: public class School : DependencyObject
2: {
3:
4: public static int GetGrade(DependencyObject obj)
5: {
6: return (int)obj.GetValue(GradeProperty);
7: }
8:
9: public static void SetGrade(DependencyObject obj, int value)
10: {
11: obj.SetValue(GradeProperty, value);
12: }
13:
14: // Using a DependencyProperty as the backing store for Grade. This enables animation, styling, binding, etc...
15: public static readonly DependencyProperty GradeProperty =
16: DependencyProperty.RegisterAttached("Grade", typeof(int), typeof(School), new UIPropertyMetadata(0));
17:
18: }
可以明显看出,GradeProperty就是一个DependencyProperty类型的成员变量,声明时一样使用了public static readonly修饰符。唯一的不同就是注册附加属性的时候使用的是RegisterAttached方法,但参数却与Register方法无异。附加属性的包装器与依赖属性的包装器不同---依赖属性使用CLR属性对GetValue和SetValue两个方法进行包装。附加属性则使用两个方法分别进行了包装----这样做完全是在使用的时候保持语句行文上的流畅。
如何消费School的GradeProperty呢?首先我们要准备一个派生自DependencyObject名为Human的类:
1: public class Human:DependencyObject
2: {
3:
4: }
1: private void Button_Click(object sender, RoutedEventArgs e)
2: {
3: Human human = new Human();
4: School.SetGrade(human, 15);
5: int grade = School.GetGrade(human);
6: MessageBox.Show(grade.ToString());
7: }
运行效果如下图:
剖析.netframework源码,你会发现这一过程和之前依赖属性保存值的方式别无二致---值仍然被保存在Human实例的EffectiveValueEntry数组里,只是用于在数组里面检索数据的依赖属性(即附加属性)并不是以Human为宿主而是寄宿在School类里,可那又有什么关系呢---反正CLR属性名和宿主类型名只是用来生成hashcode和GloballIndex。
下面我们看看如何把下面这段XAML代码用C#代码来实现。
1: <Grid ShowGridLines="True">
2: <Grid.ColumnDefinitions>
3: <ColumnDefinition />
4: <ColumnDefinition />
5: <ColumnDefinition />
6: </Grid.ColumnDefinitions>
7: <Grid.RowDefinitions>
8: <RowDefinition />
9: <RowDefinition />
10: <RowDefinition />
11: </Grid.RowDefinitions>
12: <TextBox Name="textBox1" Grid.Row="1" Grid.Column="1" Background="LightBlue"/>
13: </Grid>
与之等效的C#代码如下:
1: public Window21()
2: {
3: InitializeComponent();
4: InitializeLayout();
5: }
6:
7: private void InitializeLayout()
8: {
9: Grid gd = new Grid();
10: gd.ShowGridLines = true;
11: gd.RowDefinitions.Add(new RowDefinition());
12: gd.RowDefinitions.Add(new RowDefinition());
13: gd.RowDefinitions.Add(new RowDefinition());
14: gd.ColumnDefinitions.Add(new ColumnDefinition());
15: gd.ColumnDefinitions.Add(new ColumnDefinition());
16: gd.ColumnDefinitions.Add(new ColumnDefinition());
17: Button btn = new Button() { Content="Test"};
18: Grid.SetColumn(btn, 1);
19: Grid.SetRow(btn, 1);
20: gd.Children.Add(btn);
21: this.Content = gd;
22: }
运行效果如下图:
现在我们已经知道如何在XAML和C#代码中直接为附加属性赋值,不过别忘了,附加属性的本质是依赖属性---附加属性也可以使用Binding依赖在其它对象的数据上。请看下面这个例子,窗体使用Canvas布局,两个Slider用来控制矩形在Canvas中的横纵坐标。程序运行效果如下图:
XAML代码如下:
1: <Window x:Class="WpfApplication1.Window22"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: Title="Window22" Height="349" Width="464" Background="AliceBlue">
5: <Canvas Margin="10" Background="Orange">
6: <Slider Canvas.Left="10" Canvas.Top="10" Height="23" Name="slider1" Width="359" Maximum="200" Minimum="50" Value=http://www.mamicode.com/"50"/>
7: <Slider Canvas.Left="10" Canvas.Top="43" Height="23" Name="slider2" Width="359" Maximum="200" Minimum="50" Value=http://www.mamicode.com/"50" />
8: <Rectangle Canvas.Left="{Binding Path=Value, ElementName=slider2}" Canvas.Top="{Binding Path=Value, ElementName=slider1}" Height="80" Name="rectangle1" Stroke="Black" Width="80" Fill="Blue"/>
9: </Canvas>
10: </Window>
1: rt.SetBinding(Canvas.LeftProperty, new Binding("Value") { Source=slider1});
2: rt.SetBinding(Canvas.TopProperty, new Binding("Value") { Source = slider2});
由此可见,在使用Binding的时候除了宿主有所不同之外没有任何区别。