开始写这个新系列,这些年用WPF做了很多项目,杂七杂八的东西写了不少,略略总结下,也希望能给朋友们带来点帮助。
本篇文章主要是自实现了一个BindingEngine,可以在WPF,WinForm,Web等各个情景中使用。
引子
按照惯例,先找一个插入点,从之讲起。
既然是企业应用,先来说说为什么要写这个BindingEngine。
项目背景是一个机械的配置文件编辑器,大概有200多个机械,每个机械200多个参数,要支持增/删/改,版本控制,Undo/Redo等一系列操作。使用WPF开发界面,开发模式采用MVVM,控件选取了DataGrid。为了提高性能,使用了Virtualizing等一系列方案优化。项目原型开发后,面对如此大数量的Cell,DataGrid的表现十分令人伤心,无论是性能还是内存占用量,都有很长的路要走。
一条路走到黑是不行的,把DataGrid更换为SourceGrid,SourceGrid是Codeplex上的一个开源C#表格实现,自测量和绘制内部的Cell,思路和实现很赞。更换控件后,一切顺利,但SourceGrid对绑定支持的不够。MVVM模式,VM(ViewModel)是PM(PresentationModel)的一个演化,由于采用了数据绑定和Behavior,View和PM之间可以更充分解耦,PM中Presentation的含义是其中要保存所有可能被用户修改的View的状态。修改了PM中View的状态后,通过数据绑定,View应该自动刷新,但SourceGrid不是基于WPF的,要实现Visible,Filter,换肤等功能PM需要持有View的引用或者通过MVP模式抽象出View的接口,无论哪种模式都要修改原有的设计,于是就萌生出了自实现一个BindingEngine 的想法。
什么是Binding?
在实现一个BindingEngine之前,先来看一下,什么是Binding?
Binding(绑定),是在.net 2.0之后被提出的,Binding大体分两类,一类是List控件绑定到List上,根据List的改变来增减List控件的Item。另一类是把单一View绑定到Model上,根据Model的属性变化来更改View的状态。
Binding这个概念简单易懂,使用起来也很方便,一提出就受到了热捧,在WPF/Silverlight中更是大行其道,基本所有介绍WPF的资料中都会把Binding拿出来炫耀一番。本文不是介绍如何使用WPF/Silverlight中Binding的,那么Binding的原理是什么呢?
Binding的原理
一个最简单的Binding就是把A的属性绑定到B的属性上,当B的属性变化时,A的属性可以自动更新。这个Binding分两层含义:
- A需要监视B属性的变化,当B属性变化时A得到通知。
- 当收到变化通知时,A要根据B的属性新值来设置自己的属性值。
关于监视B属性变化,这是一个经典的Observer模式,在.net中用event 来表示,如:
1: b.PropertyChanged += a.HandlePropertyChanged;
void HandlePropertyChanged()
3: {
4: a.Prop = b.Prop;
5: }
关于PropertyChange事件,.net在System.ComponentModel里提供了INotifyPropertyChanged接口,里面定义了event PropertyChangedEventHandler PropertyChanged。通常可被用于数据绑定的Model类都要实现INotifyPropertyChanged接口,在属性变化时raise这个PropertyChanged事件。
Binding的亮点
在WPF中,Binding无处不在,关于Binding的漂亮用法有很多,其主要的设计亮点有二:
- Weak Event模式
- Converter
监听B的属性变化,A需要注册B的PropertyChanged事件,.net中事件是强引用,一旦A注册了B的事件,B就持有了一个A的引用。也就是说,如果A不注销B的事件,即使A已经空置,如果B对象存活,垃圾回收器仍不会回收A的内存,在使用中就造成了A的内存泄露。在Binding的使用过程中,可能会出现多级绑定,A->B->C,一个对象也可能绑定多个对象,在对象空置时注销绑定的监听事件是不太现实的,实现起来太过繁琐。这里就期望能有弱事件(Weak Event)模式,即A监听了B的事件后,B不会阻止A的垃圾回收。
直接把A的属性绑定到B的属性上有时也是不太友好的,比如B的属性是string,A的属性是DateTime,在绑定的过程中需要做一定的转换(Convert)。WPF/Silverlight中的Converter是很不错的想法,可以自定义一些转换,在属性间做一些转换工作。
设计
开始设计实现BindingEngine,首先来解决弱事件的问题。
在.net中,可以使用WeakReference(弱引用)来监视对象,WeakReference不会阻止对象的垃圾回收。在实际使用中,A注册B的事件后,B持有了A的引用,B对象会阻止A的垃圾回收。直接把B对象变成弱引用对象是不现实的,但可以引入弱引用对象C,让B持有C的引用,C持有A的引用。这样即使没有注销事件监视,C对象仍持有A的引用,但是C对象是弱引用对象,不会阻止A的垃圾回收。
用一副图表示:
把用来作为中间传递的C类命名为WeakSource,它的设计如下:
WeakSource用来隔离A对象,为了内存考虑,它和A对象间是一一对应关系。这样,在监听B的PropertyChanged事件时,原有的b.ProppertyChanged += a.HandlePropertyChanged就变成了b.PropertyChanged += weakSource.HandlePropertyChanged。WeakSource提供了两个静态方法Register和UnRegister来创建和销毁WeakSource,其中的第一个参数object Source就是WeakSource需要封装的A对象。
Register的第二个参数INotifyPropertyChanged target,就是需要监听的B对象,最后一个参数targetProp是需要监听B对象的属性名。当B的属性值发生变化时,WeakSource会得到通知,为了完成绑定,WeakSource需要把内部封装的A对象对应的属性值设置为B对象绑定属性的新值。
绑定值
当B属性绑定值发生变化时,完成绑定需要设置两步,一,取得B属性的新值。二,把这个新值设置到A属性上去。
最简单的办法可以用反射完成这两步操作,为了编写简单,使用了Expression Tree来构建这个取值赋值操作:
//Set Property
2: var prop = entry.SourceType.GetProperty(entry.SourceProp);
);
4:
//Get Property
6: var targetProperty = entry.TargetType.GetProperty(entry.TargetProp);
);
8: var getter = Expression.Property(paraTarget, targetProperty);
9:
//Combine
11: var boy = Expression.Call(paraSource, prop.GetSetMethod(), getter);
12: Delegate action = Expression.Lambda(boy, paraSource, paraTarget).Compile();