WPF学习之深入浅出话属性
通过前面的学习,我们已经知道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为实例,如果我们把类设计成这样:
最后还有一个小问题,实例的每个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两个方法:
从这棵继承树上可以看出,WPF的所有控件都是依赖对象。WPF的类库在设计的时候充分利用了依赖属性的优势,UI控件的绝大多数属性已经依赖化了。
1.2.2 声明和使用依赖属性
下面我们通过一个简单的实例来说明依赖属性的使用方法。
前面已经说过,DependencyProperty必须以DependencyObject作为宿主,借助它的SetValue和GetValue进行记录和读取。因此,想使用自定义的DependencyProperty,宿主一定是DependencyObject的派生类。DenpendencyProperty实例声明特点很鲜明----引用变量由public static readonly三个修饰符进行修饰,实例并非使用new操作符得到而是使用DependencyProperty.Register方法实现,代码如下:
当第一次看到这个例子的时候,也许会有点百思不得其解的感觉---依赖属性是一个static对象,哪怕有1000个student实例,依赖属性对象也只有一个,那么调用SetValue的时候值被存到哪里去了?调用GetValue又如何将值读取出来?而且ReadOnly关键字修饰的变量不是只读的吗?那么怎么可能写入值呢?其实这个问题直指依赖属性的核心,我们会在后面讲到,现在我们把思维先集中在依赖属性的使用上。
上面的例子,依赖属性做为“属性”的功能已经体现出来,但是,如何体现出依赖呢?让我们先看下面一个例子。先回顾一下Binding,Binding作为数据流动的桥梁,一端是数据来源,一段是数据目标。一般情况下数据来源是业务逻辑层对象而目标就是UI上的控件。在下面这个例子里面,我们暂且倒过来,让textBox1作为数据源,把Student实例作为目标,让Student实例依赖在TextBox上。注意,这仅仅是为了展示依赖属性的依赖功能,现实中几乎不可能去这样做。
下面是窗口类的后台代码:
如果TextBox放置在Canvas里面,则代码会是这样:
放在DockPanel里面,代码会是这样:
放在StackPanel里最省事:
作为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键。
下面的代码是做好“完型填空”的代码:
剖析.netframework源码,你会发现这一过程和之前依赖属性保存值的方式别无二致---值仍然被保存在Human实例的EffectiveValueEntry数组里,只是用于在数组里面检索数据的依赖属性(即附加属性)并不是以Human为宿主而是寄宿在School类里,可那又有什么关系呢---反正CLR属性名和宿主类型名只是用来生成hashcode和GloballIndex。
下面我们看看如何把下面这段XAML代码用C#代码来实现。
现在我们已经知道如何在XAML和C#代码中直接为附加属性赋值,不过别忘了,附加属性的本质是依赖属性---附加属性也可以使用Binding依赖在其它对象的数据上。请看下面这个例子,窗体使用Canvas布局,两个Slider用来控制矩形在Canvas中的横纵坐标。程序运行效果如下图:
XAML代码如下:
rt.SetBinding(Canvas.LeftProperty, new Binding("Value") { Source=slider1}); rt.SetBinding(Canvas.TopProperty, new Binding("Value") { Source = slider2});
由此可见,在使用Binding的时候除了宿主有所不同之外没有任何区别。
转载请注明出处:
http://blog.csdn.net/fwj380891124