代码改变世界

WPF企业应用--自实现Binding,可用于WinForm,Web等

2011-03-09 01:29 周永恒 阅读(...) 评论(...) 编辑 收藏

前言

  开始写这个新系列,这些年用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分两层含义:

  1. A需要监视B属性的变化,当B属性变化时A得到通知。
  2. 当收到变化通知时,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的漂亮用法有很多,其主要的设计亮点有二:

  1. Weak Event模式
  2. 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的垃圾回收。

  用一副图表示:

image

  把用来作为中间传递的C类命名为WeakSource,它的设计如下:

image

  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下载,如有问题和建议也欢迎给我留言,谢谢。

 

 

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。