引言

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, );

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

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

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

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

剖析WPF数据绑定机制的更多相关文章

  1. WPF源代码分析系列一:剖析WPF模板机制的内部实现(一)

    众所周知,在WPF框架中,Visual类是可以提供渲染(render)支持的最顶层的类,所有可视化元素(包括UIElement.FrameworkElment.Control等)都直接或间接继承自Vi ...

  2. WPF数据绑定Binding(二)

    WPF数据绑定Binding(二) 1.UI控件直接的数据绑定 UI对象间的绑定,也是最基本的形式,通常是将源对象Source的某个属性值绑定 (拷贝) 到目标对象Destination的某个属性上. ...

  3. WPF入门教程系列(二) 深入剖析WPF Binding的使用方法

    WPF入门教程系列(二) 深入剖析WPF Binding的使用方法 同一个对象(特指System.Windows.DependencyObject的子类)的同一种属性(特指DependencyProp ...

  4. 理解和使用WPF 验证机制

    博客 学院 下载 更多 写博客 发布Chat 登录注册 理解和使用WPF 验证机制 原创 2013年06月20日 11:15:37 7404 首先建立一个demo用以学习和实验WPF Data Val ...

  5. 如何妙用Spring 数据绑定机制?

    前言 在剖析完 「Spring Boot 统一数据格式是怎么实现的? 」文章之后,一直觉得有必要说明一下 Spring's Data Binding Mechanism 「Spring 数据绑定机制」 ...

  6. 微软原文翻译:适用于.Net Core的WPF数据绑定概述

    原文链接,大部分是机器翻译,仅做了小部分修改.英.中文对照,看不懂的看英文. Data binding overview in WPF 2019/09/19 Data binding in Windo ...

  7. WPF 数据绑定Binding

    什么是数据绑定? Windows Presentation Foundation (WPF) 数据绑定为应用程序提供了一种简单而一致的方法来显示数据以及与数据交互. 通过数据绑定,您可以对两个不同对象 ...

  8. WPF——数据绑定(一)什么是数据绑定

    注意:本人初学WPF,文中可能有表达或者技术性问题,欢迎指正!谢谢! 一:什么是数据绑定? “Windows Presentation Foundation (WPF) 数据绑定为应用程序提供了一种简 ...

  9. 再谈angularJS数据绑定机制及背后原理—angularJS常见问题总结

    这篇是对angularJS的一些疑点回顾,是对目前angularJS开发的各种常见问题的整理汇总.如果对文中的题目全部了然于胸,觉得对整个angular框架应该掌握的七七八八了.希望志同道合的通知补充 ...

随机推荐

  1. 简单聊聊Storm的流分组策略

    简单聊聊Storm的流分组策略 首先我要强调的是,Storm的分组策略对结果有着直接的影响,不同的分组的结果一定是不一样的.其次,不同的分组策略对资源的利用也是有着非常大的不同,本文主要讲一讲loca ...

  2. 原生ajax实现http请求

      1⃣️先简单了解一下HTTP协议: http是计算机通过网络进行通信的一种规则,它是一种无状态协议(不建立持久链接,直白点儿说就是请求响应完事儿之后,链接就断开)  2⃣️一个完整的http请求有 ...

  3. bootstrap快速入门笔记(三)响应式,行,列,偏移量,排序

    一,响应式列重置 .clearfix <div class="row"> <div class="col-xs-6 col-sm-3"> ...

  4. linux性能分析及调优

    第一节:cpu 性能瓶颈 计算机中,cpu是最重要的一个子系统,负责所有计算任务: 基于摩尔定律的发展,cpu是发展最快的一个硬件,所以瓶颈很少出现在cpu上: 我们线上环境的cpu都是多核的,并且基 ...

  5. mysql行列转换方法总结

    这是一道行转列并且构造交叉表的问题: http://topic.csdn.net/u/20090530/23/0b782674-4b0b-4cf5-bc1a-e8914aaee5ab.html 数据样 ...

  6. 572. Subtree of Another Tree

    Problem statement: Given two non-empty binary trees s and t, check whether tree t has exactly the sa ...

  7. java线程(四)

    java5线程并发库 线程并发库是JDK 1.5版本级以上才有的针对线程并发编程提供的一些常用工具类,这些类被封装在java.concurrent包下. 该包下又有两个子包,分别是atomic和loc ...

  8. Docker - 手动迁移镜像

    在没有Docker Registry时,可以通过docker save和docker load命令完成镜像迁移的过程,先将镜像保存为压缩包,然后在其他位置再加载压缩包. 将镜像保存为压缩包文件 [ro ...

  9. [刷题]算法竞赛入门经典(第2版) 6-9/UVa127 - "Accordian" Patience

    题意:52张牌排一行,一旦出现任何一张牌与它左边的第一张或第三张"匹配",即花色或点数相同,则须立即将其移动到那张牌上面,将其覆盖.能执行以上移动的只有压在最上面的牌.直到最后没有 ...

  10. Linux 下安装RabbitMQ 3.6.1

    1.安装erlang 依赖 yum install -y gcc gcc-c++ unixODBC-devel openssl-devel ncurses-devel 2.安装erlang ### 设 ...