zoukankan      html  css  js  c++  java
  • 剖析WPF数据绑定机制

    引言

    WPF框架采取的是MVVM模式,也就是数据驱动UI,UI控件(Controls)被严格地限制在表示层内,不会参与业务逻辑的处理,只是通过数据绑定(Data Binding)简单忠实地表达与之绑定的数据。

    本文计划从数据端、控件端各自的实现要求,绑定的过程和中介等角度全面地剖析数据绑定的运行机理,帮助读者打开数据绑定的盒子,看到运作的本质,使读者知其然更知其所以然。

    一个简单的例子

    最开始提供一个简单的数据绑定例子,各环节的功能算是完备,在阅读随时可以回来参考例子理理思路。TextBox绑定一个包装过的字符串,单击按钮改变字符串,TextBox应当相应改变,代码如下。

    XAML文件:

        <StackPanel>
           <Button x:Name="b" Content="Change Value" Margin="30" Width="100" Click="b_Click"/>
           <TextBox x:Name="tb" Width="100"/>
        </StackPanel>

    C#文件:

        public partial class MainWindow : Window
        {
           private Source s = new Source();
           public MainWindow()
           {
               InitializeComponent();
               Binding binding = new Binding("S");
               binding.Source = s;
               tb.SetBinding(TextBox.TextProperty, binding);
           }
           private void b_Click(object sender, RoutedEventArgs e)
           {
               s.S = "New value";
           }
        }
        class Source:INotifyPropertyChanged
        {
           public event PropertyChangedEventHandler PropertyChanged;
           private string _s = "Old value";
           public string S
           {
               get
               {
                  return _s;
               }
               set
               {
                  _s = value;
                  PropertyChanged.Invoke(this,new PropertyChangedEventArgs("S"));
               }
           }
        }

    数据端:INotifyPropertyChanged接口

    控件要处于一个被动的地位,根据数据的变化来自动做动作,这种多对一的监听很显然属于设计模式中的“订阅/发布模式”(Subscribe/Publish),而.NET C#天然地以事件event支持了这一模式,可以说极大地方便了基于此的数据绑定机制。做一个简单说明:

       

        delegate void Handler();
        class Publisher
        {
           public event Handler Event;
           public void Invoke()
           {
               Event.Invoke();
           }
        }
        class Subscriber
        {
           public void Subscribe(Publisher p)
           {
               p.Event += _callback;
           }
           private void _callback()
           {
               throw new NotImplementedException();
           }
        }
        class Program
        {
           static void Main(string[] args)
           {
               Publisher p = new Publisher();
               Subscriber s = new Subscriber();
               s.Subscribe(p);
               try
               {
                  p.Invoke();   
               }catch(NotImplementedException)
               {
                  Console.WriteLine("Process normally.");
               }
               Console.ReadKey();
           }
        }

    例子中,声明了事件Event,它看做一个委托方法(Delegate method)的集合,订阅者向其中添加自己的回调方法这即是订阅了该事件。

    现在考虑WPF数据绑定,数据是事件的发生者即发布者,控件是订阅者,所以数据应该有一个可以触发(Invoke)的事件,在.NET中采用接口(Interface)INotifyPropertyChanged。

    这个接口在System.ComponentModel里面,内容很简单:

        public interface INotifyPropertyChanged
        {
           event PropertyChangedEventHandler PropertyChanged;
        }

    实现这么个事件即可,委托如下:

    public delegate void PropertyChangedEventHandler(object sender, PropertyChangedEventArgs e);

    第二个参数也很简单:

        public class PropertyChangedEventArgs : EventArgs
        {
           public PropertyChangedEventArgs(string propertyName);
           public virtual string PropertyName { get; }
        }

    只需要提供一个字符串作为属性(Property)名即可。这里可以考虑,实现了这一接口的发布者在数据改变时主动地加一句话去Invoke此事件,注册(按照这里讨论的,就是绑定)了此数据的控件的回调方法会被调用做动作,这就是数据绑定——nice!

    请留意,这个接口并非必须实现不可,之后的部分我将提到一种不用实现它的做法。

    控件端与属性

    C#里对C++这种原始的OOP——方法+字段进行了拓展,把字段的简洁用法和方法的逻辑能力结合,这就叫属性。对于以往的字段,推荐使用属性编写。

    请打开Visual Studio,找一个控件类一路上溯它的继承体系,会看到Control类再向上有一个叫做DependencyObject的基类,这是本节研究的重点。

    依赖(Dependency)是控件的特点,毕竟数据驱动UI开发,UI是要依赖一些东西的(这里讲的就是数据,依赖来自数据绑定)。

    需要介绍和DependencyObject协作的另一个类DependencyProperty,以DependencyObject为主体,通过一系列的方法操作DependencyProperty,比如以下两个:

        public class DependencyObject : DispatcherObject
        {
           //....
           public object GetValue(DependencyProperty dp);
           //....
           public void SetValue(DependencyProperty dp, object value);
           //....
        }

    具体的机制我不准备详细介绍,刘铁猛老师的书《深入浅出WPF》中有非常好的讲解。简单来说,DependencyObject应该为DependencyProperty提供一个C#属性作为包装。每个DependencyObject拥有n(n=依赖属性数量)个静态的DependencyProperty实例(此实例由DependencyProperty的静态方法Register得到)而非每个实例拥有一个。每个DependencyProperty实例包含一个广泛的表,作用是通过与C#属性名、属性类型有关的经过哈希运算得到的键来获取需要的,特定实例,特定属性的值,关系可由下图说明:

    深入绑定

    现在看看控件端特性与数据端特性是如何相互作用的。

    专门提供方法的静态类(Static class)BindingOperations有静态方法SetBinding,基类FrameworkElement有对其的同名封装,控件就是通过这个函数和数据实现绑定的,下面研究一下这个没有封装的原始形式。

            public static BindingExpressionBase SetBinding(DependencyObject target, DependencyProperty dp, BindingBase binding);

    先看一下第三个参数,再回头看看前两个参数和控件端相关的。

    1.  BindingBase是一个抽象类(Abstract class),内部有抽象方法CreateBindingExpressionOverride由它的子类实现,明确了数据来源的子类完成创建BindingExpressionBase的工作。

    2.  由上图可以清晰地看出,DependencyObject和DependencyProperty并非包含关系而是相依的,你需要同时提供两个才能明确哪个控件的哪个依赖属性需要绑定。

    Binding对象是面向数据侧的,这很好理解,支持了多个控件绑定同一数据。

    那么一次SetBinding究竟做了什么?它的返回值是BindingExpressionBase,它有三个子类分别是BindingExpression,MultiBindingExpression,PriorityBindingExpression,在此只研究简单的目标绑定单源,即用BindingExpression子类。一个绑定数据的Binding可以多次与控件绑定,每次返回一个新的BindingExpression,那么很好理解,它就是一组绑定的实例,它与Binding是多对一的关系。可以把Binding看做一个通电的插排,不断有充当插头的DependencyObject来对接(绑定),而返回的BindingExpression就是真正可用的配合。它继承并重写了BindingExpressionBase的UpdateTarget和UpdateSource方法——至此,Binding的地位和作用开始明确了:

    UpdateSource只在TwoWay和OneWayToSource模式下有效,这里以UpdateTarget这个通用的方法说明这对“更新方法”。每一组绑定有一个BindingExpression实例,SetBinding的作用正是将更新方法写进数据源INotifyPropertyChanged接口的事件委托之中,当事件触发,即数据发生改变时调用注册的回调来更新Target控件——毕竟更新方法是public方法,随时可以手工调用只是什么都不会发生罢了(当数据源没有实现INotifyPropertyChanged等通知接口时可以这样强制更新,但这是舍弃了自动的连贯行为,转为手工实现)。

    注意,BindingExpression还实现了接口IWeakEventListener,这是关于.NET的弱事件模式(Weak event pattern)。通常,监听者注册事件会在事件源内存放一个自己的引用,而如果不显式地删除这个引用,即使监听者生命周期早已结束,引用仍然存在,GC不会进行——这就造成了一种形式的内存泄漏。数据绑定符合这个场景。.NET给出的解决方法是弱事件模式。在这个模式中,事件源端实现一个WeakEventManager,监听端实现接口IWeakEventListener,这样注册到源的事件处理方法进传递一个弱引用,这不会无限延长监听者的生命周期。

    属性与反射的应用

    C#的反射技术给动态访问类的属性提供了可能。通过类似这样的代码:

        MyClass mc = new MyClass();
        mc.GetType().GetProperty("MyProperty").SetValue(mc, 1);

    我们得以通过传递字符串的方式标记指定类的指定属性。本节的目的是串联之前各部分,看看方法的参数用意何为,看看反射是怎么贯穿数据绑定机制的环节之间的。

    约定数据源包装实际数据,通过属性暴露出来,在属性改变时激发事件PropertyChanged。如之前讲的,这个事件激发的参数是表明此属性的字符串。现在,属性名已经分发到了每个有关此数据的BindingExpression上。要注意,数据源只有独一个PropertyChanged事件,所有属性更改都会激发它(为什么只一个?这是INotifyPropertyChanged接口规定的啊),所以绑定此数据源的所有Binding都会接到通知(Notify),它们需要鉴别。通过public属性ResolvedSource和ResolvedSourcePropertyName可知,它确实有了识别属性的足够信息,于是它们分别对照Invoke时PropertyChangedEventArgs附加的属性名看是不是自己关联的,最终只有一个Binding确认自己绑定了这个属性,然后它UpdateTarget——这关键一步通过上面示范的反射机制即可胜任。

    这就是属性名从特定属性内部流出直到指导控件更新的过程,可谓环环相扣精巧严密。

     

     

    局限于篇幅,我不能事无巨细地说明每一个细节,请读者对想深入理解的点查阅更多的资料,定会收获良多。

  • 相关阅读:
    favorite 单词学习 主要是发音 fa vor it 注意 ri不连读 是自然带出来的r的尾音
    echart 带表格
    正则 小于等于1 小数最多两位
    period 发音 per + iod 没有ri音 (per=round od=hod=way)
    SelectZenEmpty 下拉框 支持 最大长度 超出... vue 组件
    expensive pens 就是 spend 花费 pend 就是 to pay
    reciterdoc 资料库 支持中文搜索了。 vuepresspluginfulltextsearch(用一半)
    react 中 动态添加 class,防止图片 重复加载, 主要是 backgroundimage的二次加载会有新请求,和图片的闪烁
    vscode 格式化 vue 和 js代码 vetur prettier beautify
    sign 单词学习 本质:去分开
  • 原文地址:https://www.cnblogs.com/jily/p/6970120.html
Copyright © 2011-2022 走看看