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


    前言

      开始写这个新系列,这些年用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下载,如有问题和建议也欢迎给我留言,谢谢。

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

  • 相关阅读:
    SpringMVC从Request域中获取数据
    SpringMVC重定向
    SpringMVC的请求转发的三种方法
    SpringMVC文件上传
    SpringMVC处理请求释放静态资源的三种方式
    jackson实现json转换
    SpringMVC之请求部分
    SpringMVC的执行流程
    Java [Leetcode 39]Combination Sum
    深入解析Java对象的hashCode和hashCode在HashMap的底层数据结构的应用
  • 原文地址:https://www.cnblogs.com/Zhouyongh/p/1977768.html
Copyright © 2020-2023  润新知