6.4.2 Binding的数据转换
前面的例子大多都是使用Slider控件与TextBox控件之间建立关联 — — Slider控件作为Source(Path是Value属性),TextBox控件作为Target(目标属性为Text)。Slider的Value属性是double类型值,而TextBox的Text属性是string类型值,在C#中这种强类型(strong-typed)语言中却可以往来自如。原因如下:
Binding还有另外一种机制称为类型转换(Data Convert),当Source端Path所关联的数据与Target端目标属性数据类型不一致时,我们可以添加数据转换器(Data Converter)。上面所提到的double类型与string类型之间的转换,因为比较简单,所以WPF类库自动替我们完成这项工作。但有些类型之间的转换就不是WPF能替我们做的了。如下面几种情况:
- Source里的数据是Y、N和X三个值(可能是char类型、string类型或自定义枚举类型),UI上对应的是CheckBox控件,需要把这三个值映射为它的IsChecked属性(bool?类型)。
- 当TextBox里已经输入了文字时,用于登录的Button才会出现,这是string类型与Visibility枚举类型或bool类型之间的转换(Binding的Mode将是OneWay)。
- Source里的数据可能是Male或Female(string或枚举),UI上对应的是用于显示头像的Image控件,这时候需要把Source里的值转换成对应的头像图片URI(亦是OneWay)。
当遇到上述这些情况时,我们只能自己动手写Convert,方法是创建一个类并让这个类实现IValueConverter接口。接口的定义如下:
public interface IValueConverter
{
object Convert(object value, Type targetType, object parameter, CultureInfo culture);
object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture);
}
当数据从Binding的Source流向Target时,Convert方法将被调用;反之,ConvertBack方法将被调用。
这两个方法的参数列表一模一样:第一个参数object,最大限度地保证了Convert的重用性(可以在方法体内对实际类型进行判断);第二个参数用于确定方法的返回类型(个人认为形参名字叫outputType比targetType要好,可以避免与Binding的Target混淆);第三个参数用于把额外的信息传入方法,若需要传递多个信息则可把信息放入一个集合对象来传入方法。
Binding对象的Mode属性灰影响到这两个方法的调用。如果Mode为TwoWay或Default行为与TwoWay一致则两个方法都有可能被调用;如果Mode为OneWay或Default行为与OneWay一致则只有Convert方法会被调用;其他情况同理。
6.5 MultiBinding(多路Binding)
有的时候UI需要显示的信息由不止一个数据来源决定,这时候就需要使用MultiBinding,即多路Binding。MultiBinding与Binding一样均以BindingBase为基类,也就是说,凡是能使用Binding对象的场合都能使用MultiBinding。MultiBinding具有一个名为Bindings的属性,其类型是Collection<BindingBase>,通过这个属性MultiBinding具有一个名为Bindings的属性,其类型是Collection<BindingBase>,通过这个属性MultiBinding把一组Binding对象聚合起来,处在这个集合中的Binding对象可以拥有自己的数据校验和转换机制,它们汇集起来的数据将共同决定传往MultiBinding目标的数据。
注意:
- MultiBinding对于添加子级Binding的顺序是敏感的,因为这个顺序决定了汇集到Converter里数据的顺序。
- MultiBinding的Converter实现的是IMultiValueConverter接口。
6.6 小结
WPF的核心理念是变传统的UI驱动程序为数据驱动UI,支撑这个理念的基础就是Data Binding和与之相关的数据校验和转换。在使用Binding时,最重要的事情就是准确地设置它的源和路径。
7 深入浅出话属性
本章将目光移向Binding的目标端,研究一下什么样的对象才能作为Binding的Target以及Binding将把数据送往何处?
7.1 属性(Property)的来龙去脉
程序=数据+算法,或者说是用算法来处理数据以期得到输出结果。
在程序中,数据表现为各种各样的变量,算法则表现为各种各样的函数(操作符是函数的简记法)即使到了面向对象时代有了类等数据结构的出现,这一本质仍然没有改变 — — 类的作用只是把散落在程序中的变量和函数进行归档封装并控制对它们的访问而已。被封装在类里的变量称为字段(Field),它表示的是类或实例的状态;被封装在类里的函数称为方法(Method),它表示类或者实例的功能(即能做什么)。字段和方法构成了最原始的面向对象封装,这时候的面向对象概念中还不包含事件、属性等概念。
使用private、public等修饰符来控制字段或方法的可访问性;是否使用static关键字来修饰字段或方法决定了字段或方法是对类有意义还是对类的实例有意义。所谓对”类有意义”或”对实例有意义”都是语义范畴的概念。比如对Human这个类来说,Weight(重量这个字段对于人类的个体是有意义的,而对于”人类”这个概念并没有什么意义);Amount(总量)这个字段就不一样了,它对于人类的个体没有意义,但对于人类是有意义的。方法也有类似的情况,比如speak这个方法,只有人类的个体才能speak,而populate(繁衍)这个方法似乎对于人类比对人类的个体更有意义。为了让程序满足语义要求,C#语言规定:对类有意义的字段和方法使用static关键字修饰,称为静态成员。通过类名加访问操作符(即”.”操作符)可以访问它们;对类的实例有意义的字段和方法不加static关键字,称为非静态成员或实例成员。
从语义方面来看,静态成员与非静态成员有着很好的对称性,但从程序在内存中的结构来看,这种对称就被打破了。静态字段在内存中只有一个拷贝,非静态字段则是每个实例拥有一个拷贝,无论方法是否为静态的,在内存中只会有一份拷贝,区别只是你能通过类名来访问存放指令的内存还是通过实例名来访问存放指令的内存。
字段被封装在实例里,要么能被外界访问(非private修饰)、要么不能(使用private修饰)。这种直接把数据暴露给外界的作法很不安全,很容易就会把错误的值写入字段。如果在每次写入数据的时候都先判断一下值的有效性又会增加冗余的代码并且违反面向对象要求”高内聚”的原则,我们希望对象自己有能力判断将被写入的值是否准确。于是,程序员仍然把字段标记为private但使用一对非private的方法来包装它。在这对方法中,一个以Set为前缀且负责判断数据的有效性并写入数据,另一个以Get为前缀且负责把字段里的数据读取出来。
很多传统的类库使用的就是这种数据封装和访问方法,例如MFC就是这样。我们称这对Get/Set方法为private字段的安全包装。
使用两个方法包装一个字段的方法已然不错,但还是有些麻烦,书写的时候代码比较分散,使用的时候又要在自动提示里上下翻动。于是,当.NET Framework推出时,微软更进一步把Get/Set这对方法合并成了属性(Property)。使用属性时,格式上很像使用非private字段,保证了语义上的顺畅,同时又不失Get/Set方法的安全性,代码变得更加紧凑,自动提示菜单也短了许多。
这种.NET Framework中的属性又称为CLR属性(CLR,Common Language Runtime)。我们既可以说CLR属性是private字段的安全访问包装,也可以说一个private字段在后台支持(back)一个CLR属性。
那实例的每个private字段都会占用一定的内存,现在字段被属性包装了起来,实例看上去都带有相同的属性,那么是不是每个实例的CLR属性也会多占一点内存呢?根据IL反编译器打开的编译结果可以看到在C#代码中的属性的编译结果是两个方法。已经知道,再多的实例方法也只有一个拷贝(静态和非静态方法均只存在一份拷贝!),所以CLR属性并不会增加内存的负担。同时也说明,属性仅仅是个语法糖衣(Syntax Sugar)。
7.2 依赖属性(Dependency Property)
在WPF中,推出了”依赖属性”这个新概念。简而言之,依赖属性就是一种可以自己没有值,并能通过使用Binding从数据源获得值(依赖在别人身上)的属性。拥有依赖属性的对象被称为”依赖对象”。与传统的CLR属性和面向对象思想相比依赖属性有很多新颖之处,其中包括:
- 节省实例对内存的开销。
- 属性值可以通过Binding依赖在其他对象上。
7.2.1 依赖属性对内存的使用方式
依赖属性较之CLR属性在内存使用方面迥然不同。实例的每个CLR属性都包装着一个非静态的字段(或者说由一个非静态的字段在后台支持),如:TextBox有138个属性,假设每个CLR属性都包装着一个4字节的字段,如果程序运行的时候创建了10列1000行的一个TextBox列表,那么这些字段将占用4*138*10*1000≈5.26M内存!在这一百多个属性中,最常用的也就是Text属性,这就意味着大多数内存都会被浪费掉。
在传统的.NET开发中,一个对象所占用的内存空间在调用new操作符进行实例化的时候就已经决定了,而WPF允许对象在被创建的时候并不包含用于存储数据的空间(即字段所占用的空间)、只保留在需要用到数据时能够获得默认值、借用其他对象数据或实时分配空间的能力 — — 这种对象就成为依赖对象(Dependency Object)而它这种实时获取数据的能力则依靠依赖属性(Dependency Property)来实现。WPF开发中,必须使用依赖对象作为依赖属性的宿主,使二者结合起来,才能形成完整的Binding目标被数据所驱动。
在WPF系统中,依赖对象的概念被DependencyObject类所实现,依赖属性的概念则由DependencyProperty类所实现。DependencyObject具有GetValue和SetValue两个方法:
public class DependencyObject : DispatcherObject
{
public object GetValue(DependencyProperty dp)
{
//...
}
public void SetValue(DependencyProperty dp, object value)
{
//...
}
}
这两个方法都以DependencyProperty对象为参数,GetValue方法通过DependencyProperty对象获取数据;SetValue通过DependencyProperty对象存储值 — — 正式这两个方法把DependencyObject和DependencyProperty紧密结合在一起。
DependencyObject是WPF系统中相当底层的一个基类。
WPF的所有UI空间都是依赖对象。WPF的类库在设计时充分利用了依赖属性的优势,UI空间的绝大多数属性都已经依赖化了。
7.2.2 声明和使用依赖属性
DependencyProperty实例的声明特点很鲜明 — — 引用变量由public static readonly三个修饰符修饰,实例并非使用new操作符得到而是使用DependencyProperty.Register方法生成。代码如下:
pulbic class Student : DependencyObject
{
public static readonly DependencyProperty NameProperty = DependencyProperty.Register("Name", typeof(string), typeof(Student));
}
这是使用DependencyProperty的最简代码,分析一下:
首先,如前所述,DependencyProperty一定使用在DependencyObject里,所以Student派生自DependencyObject类。
其次,使用DependencyProperty声明的成员变量同时被public static readonly三个修饰符修饰。在这里我们遇到了一个命名约定 — — 成员变量的名字需要加上Property后缀以表明它是一个依赖属性。我们打算用这个依赖属性存学生的姓名,所以命名为NameProperty。
再次,这个成员变量所引用的实例并非使用new操作符得到,而是使用DependencyProperty.Register方法创建。现在使用的是这个方法参数最少、最简单的一个重载:
- 第1一个参数为string类型,用这个参数来指明以哪个CLR属性作为这个依赖属性的包装器,或者说此依赖属性支持(back)的是哪个CLR属性。目前虽然没有为这个依赖属性准备包装器,但将来会使用名为Name的CLR属性来包装它,所以这个参数被赋值为Name。
- 第2个参数用来指明此依赖属性用来存储什么类型的值,学生的姓名是string类型,所以是这个参数被赋值为typeof(string)。
- 第3个参数用来指明此依赖属性的宿主是什么类型,或者说DependencyProperty.Register方法把这个依赖属性注册关联到哪个类型上。本例中的意图是为Student类准备一个可依赖的名称属性,所以需要把NameProperty注册成为Student关联,因此这个参数被赋值为typeof(student)。