引言

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. 关于Java中volatile关键字笔记

    volatile通常被认为是一种轻量级的synchronized,字面上它表示易变的,在并发编程中,它保证了共享变量的可见性.所谓可见性指的是,某个线程对变量进行操作后,其他线程能够读取到操作后的最新 ...

  2. 读书笔记 effective c++ Item 27 尽量少使用转型(casting)

    C++设计的规则是用来保证使类型相关的错误不再可能出现.理论上来说,如果你的程序能够很干净的通过编译,它就不会尝试在任何对象上执行任何不安全或无意义的操作.这个保证很有价值,不要轻易放弃它. 不幸的是 ...

  3. 篇2 安卓app自动化测试-初识python调用appium

    篇2              安卓app自动化测试-初识python调用appium --lamecho辣么丑 1.1概要 大家好!我是lamecho(辣么丑),上一篇也是<安卓app自动化测 ...

  4. PHP count() 函数

    count() 函数计算数组中的单元数目或对象中的属性个数. 对于数组,返回其元素的个数,对于其他值,返回 1.如果参数是变量而变量没有定义,则返回 0.如果 mode 被设置为 COUNT_RECU ...

  5. 蓝桥杯- 煤球数目-java

    /* (程序头部注释开始) * 程序的版权和版本声明部分 * Copyright (c) 2016, 广州科技贸易职业学院信息工程系学生 * All rights reserved. * 文件名称: ...

  6. Github--账号重新申请与配置

    2017-04-24 最近洗心革面痛下决心要好好再深入学习一番前端,正好加入了一个外包团队接了份单子,外包项目正在如火如荼地进行着,自己也打算趁这个机会来好好学习总结一番. 但是俗话说得好," ...

  7. UVA 10905 Children's Game (贪心)

    Children's Game Problem Description There are lots of number games for children. These games are pre ...

  8. 让div自适应浏览器窗口居中显示

    今天做 banner 时发现一个问题,就是浏览器窗口水平拉伸时 banner 图未能居中,所以网上找了些资料,自己写了个小 demo html代码: <div class="div1& ...

  9. MongoDB副本集的搭建

    副本集是mongodb提供的一种高可用解决方案.相对于原来的主从复制,副本集能自动感知primary节点的下线,并提升其中一个Secondary作为Primary. 整个过程对业务透明,同时也大大降低 ...

  10. shopping_cart

    #!/usr/bin/env python # -*- coding: utf-8 -*- print('欢迎土豪光临随心所欲旗舰店') user_money = int(input('老板,请输入你 ...