Binding的原理简单介绍
前言
开始写这个新系列,这些年用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;
2: 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来构建这个取值赋值操作:
1: //Set Property
2: var prop = entry.SourceType.GetProperty(entry.SourceProp);
3: var paraSource = Expression.Parameter(entry.SourceType, "source");
4:
5: //Get Property
6: var targetProperty = entry.TargetType.GetProperty(entry.TargetProp);
7: var paraTarget = Expression.Parameter(entry.TargetType, "target");
8: var getter = Expression.Property(paraTarget, targetProperty);
9:
10: //Combine
11: var boy = Expression.Call(paraSource, prop.GetSetMethod(), getter);
12: Delegate action = Expression.Lambda(boy, paraSource, paraTarget).Compile();
在WeakSource的Register方法中传入的第三个参数就是这个Delegate,当绑定的B属性值发生变化是,调用Delegate的DynamicInvoke方法即可完成更新值操作。
1: action.DynamicInvoke(source, target)
Converter
顺水推舟,加上对Converter的支持,定义IDataConverter接口,如下:
1: public interface IDataConverter
2: {
3: object Convert(object value, object parameter);
4: }
修改Register接口:
1: public static WeakSource Register(Object source, INotifyPropertyChanged target, Delegate action, string targetProp,
2: IDataConverter converter = null, object parameter = null)
为了重用构建出的Expression Tree,缓存了构建出的Delegate,创建结构体WeakEntry作为索引
1: private struct WeakEntry
2: {
3: public Type SourceType;
4: public Type TargetType;
5: public string SourceProp;
6: public string TargetProp;
7: }
对应修改的Expression Tree如下:
1: //Set Property
2: var prop = entry.SourceType.GetProperty(entry.SourceProp);
3: var paraObj = Expression.Parameter(entry.SourceType);
4:
5: //Get Property
6: var targetProperty = entry.TargetType.GetProperty(entry.TargetProp);
7: var paraTarget = Expression.Parameter(entry.TargetType);
8: var getter = Expression.Property(paraTarget, targetProperty);
9:
10: //Combine
11: var paraConvert = Expression.Variable(typeof(IDataConverter));
12: var paraParameter = Expression.Variable(typeof(object));
13:
14: var boy = Expression.IfThenElse(
15: Expression.NotEqual(paraConvert, Expression.Constant(null)),
16: Expression.Call(paraObj, prop.GetSetMethod(), Expression.Convert(Expression.Call(paraConvert, typeof(IDataConverter).GetMethod("Convert"),
17: Expression.Convert(getter, typeof(object)), Expression.Convert(paraParameter, typeof(object))), prop.PropertyType)),
18: Expression.IfThenElse(
19: Expression.Equal(Expression.Constant(prop.PropertyType, typeof(Type)), Expression.Constant(getter.Type, typeof(Type))),
20: Expression.Call(paraObj, prop.GetSetMethod(), Expression.Convert(Expression.Convert(getter, typeof(object)), prop.PropertyType)),
21: Expression.Throw(Expression.Constant(new InvalidOperationException(
22: "The property type between binding source and target does not match, please use IDataConverter to do custom convert.")))));
23:
24: Delegate action = Expression.Lambda(boy, paraObj, paraTarget, paraConvert, paraParameter).Compile();
使用Binding
创建类BindingEngine,封装Binding的操作,
1: public class BindingEngine
2: {
3: public static void SetPropertyBinding(Object source, INotifyPropertyChanged target, string sourceProp, string targetProp,
4: IDataConverter converter = null, object parameter = null)
5: {}
6:
7: public static void ClearPropertyBinding(Object source, INotifyPropertyChanged target, string sourceProp, string targetProp)
8: {}
9: }
创建测试类View和ViewModel,以及TextConverter
1: public class View
2: {
3: public string Text { get; set; }
4: public int Value { get; set; }
5: }
6:
7: public class ViewModel : INotifyPropertyChanged
8: {
9: private int _Value = 0;
10: public int Value
11: {
12: get
13: {
14: return _Value;
15: }
16: set
17: {
18: _Value = value;
19: NotifyPropertyChanged("Value");
20: }
21: }
22:
23: public event PropertyChangedEventHandler PropertyChanged;
24:
25: public void NotifyPropertyChanged(string prop)
26: {
27: if (PropertyChanged != null)
28: {
29: PropertyChanged(this, new PropertyChangedEventArgs(prop));
30: }
31: }
32: }
33:
34: public class TextConverter : IDataConverter
35: {
36: public object Convert(object value, object parameter)
37: {
38: int v = System.Convert.ToInt32(value);
39: if (parameter != null)
40: {
41: v = System.Convert.ToInt32(parameter) + v;
42: }
43: return string.Format("\"{0}\"", v);
44: }
45: }
使用BindingEngine的用法:
1: View view1 = new View();
2: View view2 = new View();
3: ViewModel model = new ViewModel();
4:
5: TextConverter converter = new TextConverter();
6: BindingEngine.SetPropertyBinding(view1, model, "Text", "Value", converter, null);
7: BindingEngine.SetPropertyBinding(view1, model, "Value", "Value");
8: BindingEngine.SetPropertyBinding(view2, model, "Text", "Value", converter, 2);
9: BindingEngine.SetPropertyBinding(view2, model, "Value", "Value");
10:
11: BindingEngine.ClearPropertyBinding(view1, model, "Text", "Value");
后续
本文只是简略了介绍了一下BindingEngine的实现,对于List控件绑定List并没有进行支持。并且由于使用了Delegate的DynamicInvoke,性能上还有提高余地,可以使用Emit、DynamicMethod来完成取值赋值操作。
BindingEngine的源代码和测试代码请点击此BindingEngineSample下载,如有问题和建议也欢迎给我留言,谢谢。