引言
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——这关键一步通过上面示范的反射机制即可胜任。
这就是属性名从特定属性内部流出直到指导控件更新的过程,可谓环环相扣精巧严密。
局限于篇幅,我不能事无巨细地说明每一个细节,请读者对想深入理解的点查阅更多的资料,定会收获良多。