WPF数据绑定机制是如何实现
ref: https://blog.csdn.net/liebert/article/details/78035681
接触MVVM模式也有一段时间了,这种将前后台分离开了的设计模式一下子就吸引了我,也是当时一直有一个问题困扰了我很久:WPF是如何实现数据变动通知的。
通过查询各种资料,自己反复推敲实验,终于发现这种机制背后的Support。下面我就从委托、Lambda表达式、LINQ、事件等几个方面给大家分享一下我的发现,不当之处还请多多指教。
1.委托
C#中有一个关键字:delegate,我们可以通过一下方式定义一个委托类型:
public delegate int AddDelegate(int a, int b);
此时,我们定义了一个委托类型AddDelegate
通过一下方式,我们定义一个委托变量:
private AddDelegate add;
我们写了一个方法
private int _add(int x,int y)
{
Return (x+y);
}
实例化一个委托变量
add = new AddDelegate(_add);
此时我们可以这样调用这个委托方法
Console.WriteLine(“使用这个方法之后的返回值是:{0}”, add(1,2));
那么我们思考一下,委托到底是如何实现的呢?
其实,通过上面委托变量的实例化我们就可以发现,委托类型是一个类,我们在使用一个委托变量之前必须实例化,那么这个委托类型究竟是如何的呢?其实这个工作都是有编译器来完成的,当我们使用delegate这个关键字的时候,后台好多工作都是由C#编译器来完成的。我们可以使用的ildasm.exe来查看编译后的文件。
当我们定义public delegateint AddDelegate(int a, int b);委托类型的时候,编译器自动实现了如下这样一个类:
sealed class AddDelegate:System.MulticastDelegate
{
public int Invoke(int a,int b);
public IAsyncResult BeginInvoke(int a,int b, AsyncCallback cb, object state);
pubic int EndInvoke(IAsyncResult result);
}
现在问题又来了,System.MulticastDelegate又是什么呢?其实这个类是System.Delegate继承下来的,是一个抽象类:
public abstract class MulticastDelegate: Delegate
{
//此处展示的是部分成员
//返回所指向的方法列表,其实我们委托进来的方法都是存储在这份列表中的
public sealed override Delegate[] GetInvocationList();
//重载的操作符
public static bool operator == (MulticastDelegate d1, MulticastDelegate d2);
public static bool operator != (MulticastDelegate d1, MulticastDelegate d2);
//用来在内部管理委托所维护的方法列表
private IntPtr _InvocationCount;
private object _InvocationList;
}
我们继续追本溯源,看看Delegate都继承了那些类和接口。不过我们可以猜一猜。从MulticastDelegate的类成员中我们可以猜到,Delegate肯定继承了ISerializable接口,因为public sealed override Delegate[] GetInvocationList();那现在让我们看看Delegate到底是怎样的:
public abstract sealed class Delegate:IClone,ISerializable
{
//列出了部分成员
//与函数列表交互的方法
public static Delegate Combine (params Delegate[] delegates);
public static Delegate Combine (Delegate a, Delegate b);
public static Delegate Remove (Delegate source, Delegate value);
public static Delegate RemoveAll (Delegate source, Delegate value);
//重载操作符
public static bool operator == (Delegate d1, Delegate d2);
public static bool operator != (Delegate d1, Delegate d2);
//扩展委托目标的属性
public MethodInfo Method{get;}
public object Target{get;}
}
此时,我们应该能够明白的委托的幕后实现机制了吧。
如果有对泛型委托不了的同志还需要的了解一下Action<>和Func<>泛型委托,我们在讨论WPF绑定机制Command的时候将会使用大量的泛型委托。
下面咱们来看看事件,C#提供的event关键字也只不过是一个语法糖而已。编译器遇到event关键字的时候,能够自动的提供注册和注销方法以及任何必要的委托类型和成员变量。
比如我们在People类定义了如下一个委托类型
public delegate void Welcome(string str);
回想之前介绍的委托幕后机制,此时Welcome类型是怎样的?
下面我们定义两个事件
public event Welcome InSchool;
public event Welcome InParty;
此时,当我们在学校的时候,如果遇见学生,应该说欢迎您来到学校
InSchool(“欢迎您来到学校”);
当我们在一个派对上遇到客人,应该说
InParty(“欢迎您来参见Party”);
可能举得这两个例子不是很好,但主要说明这是两个在不同条件下要出发的事件。
当我们定义了这两个事件之后,就之间可以对其进行+=、-=的操作(注册、注销)了。那么我们会有疑问,这个事件变量Welcome还没有实例化为对象,我们怎么能直接对它进行注册和注销事件呢?
其实这个实例化过程就是有编译器来完成的。我们可以使用其CIL代码,就会发现确实是编译器完成了这些步骤,为我们省掉了不少代码。
.method public hidebysig specialname instance void
add_ InSchool(class People/ Welcome ‘value’) cil managed
{
...
call class [mscorlib]System.Delegate
[mscorlib] System.Delegate::Combine(
class [mscorlib] System.Delegate, class [mscorlib] System.Delegate)
...
}
.method public hidebysig specialname instance void
remove_ InParty (class People/ Welcome ‘value’) cil managed
{
...
call class [mscorlib]System.Delegate
[mscorlib] System.Delegate::Remove(
class [mscorlib] System.Delegate, class [mscorlib] System.Delegate)
...
}
2.数据变动通知
WPF是如何实现数据变动通知的。在了解这个机制之前,我们首先要对WPF路由事件、依赖属性、控件有一定的基础。
一般的我们要实现这种通知机制,首先要实现INotifyPropertyChanged接口的一个类:
public class NotificationObject: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
if(this.PropertyChanged != null)
{
this.PropertyChanged.Invoke(this,new ProperttyChangedEventArgs(proprtyName));
}
}
}
我们再来看看委托类型PropertyChangedEventHandler的定义
namespace System.ComponentModel
{
public delegate void PropertyChangedEventHandler(object sender, ProperttyChangedEventArgs e);
}
namespace System.ComponentModel
{
event PropertyChangedEventHandler PropertyChanged;
}
我们在自定义类NotificationObject定义了这样一个方法:
public void RaisePropertyChanged(string propertyName)
{
if(this.PropertyChanged != null)
{
this.PropertyChanged.Invoke(this,new ProperttyChangedEventArgs(proprtyName));
}
}
这个方法有一个输入参数,为发生更新的属性名称,方法首先判断委托变量PropertyChanged是否实例化,如果不为空,那么就调用这个委托事件,并传入两个参数:this和new ProperttyChangedEventArgs(proprtyName)。
this表示当前类的实例化对象,第二个主要传入发生更新的属性的名称。
下面我们创建这样一个类,继承于NotificationObject:
public class math: NotificationObject
{
public double input1;
public double Input1
{
get{return input1;}
set
{
input1 = value;
this. RaisePropertyChanged(“Input1”);
}
}
public double input2;
public double Input2
{
get{return input2;}
set
{
input2 = value;
this. RaisePropertyChanged(“Input2”);
}
}
public double result;
public double Result
{
get{return result;}
set
{
result = value;
this. RaisePropertyChanged(“Result”);
}
}
}
UI的主窗口MainWindow.xaml代码中我们对这三变量进行绑定:
<Grid>
<StackPanel>
<TextBox x:Name=”txt1” Height=”35” Text=”{Binding Input1}”></TextBox>
<TextBox x:Name=”txt2” Height=”35” Text=”{Binding Input2}”></TextBox>
<TextBox x:Name=”txt3” Height=”35” Text=”{Binding Result}”></TextBox>
<Button x:Name =“Btn1” Height=”35” Width=”80” Click=”Btn1_Click1”></ Button >
</StackPanel>
</Grid>
为了实现绑定,我们还有在MainWindow.xaml.cs中写如下代码:
public partial class MainWindow:Window
{
public MainWindow()
{
InitializeComponent();
math m = new math();
this.DataContext = m;
}
private void Btn1_Click1(object sender, RoutedEventArgs e)
{
m.Result = m. Input1 + m. Input2;
}
}
运行程序,在txt1中输入“2”,在txt2中输入“3”,点击按钮,我们会看到txt3中显示“5”。
这时,我们修改一下属性Result的访问器代码:
public double Result
{
get{return result;}
set
{
result = value;
//this. RaisePropertyChanged(“Result”);
}
}
我们再次运行代码,在txt1中输入“2”,在txt2中输入“3”,点击按钮发现txt3中显示为“0”。
这是为什么呢?当注释掉this. RaisePropertyChanged(“Result”)时也就是不去触发事件。也就是不调用this.PropertyChanged.Invoke(m,new ProperttyChangedEventArgs(“Result”));方法。
那么委托对象PropertyChanged是由谁来实例化的呢?我们通过委托变量的Invoke()方法(前面接受委托机制时说明了Invoke方法是由编译器来实现的,主要是同步调用委托对象维护的方法)。
此时,我们应该清楚:之所以能够实现通知UI数据更新的事件一定是委托对象PropertyChanged调用了其维护的方法,这个方法就实现了通知UI控件刷新显示。那么这个方法是由谁来实现的呢?又是如何注册到委托对象中的呢?
我们看看MainWindow.xaml.cs中的一部分代码:
math m = new math();
this.DataContext = m;
如果注释掉这段代码,也是无法实现数据绑定的。那么我们可以肯定与DataContext也是紧密相关的。
我们回顾一下WPF依赖属性和路由事件,这也是WPF的核心。WPF所有的UI控件都是依赖对象。
首先,从依赖属性的使用说起,如何去实现一个依赖属性。我们自定义一个依赖对象:
public class MyClass:DependencyObject
{
//定义依赖属性
public static DependencyProperty Input1Property;
public static DependencyProperty Input2Property;
public static DependencyProperty ResultProperty;
//注册这个依赖属性
MyStringProperty = DependencyProperty.Register(“Input1”,typeof(int),typeof(MyClass));
MyStringProperty = DependencyProperty.Register(“Input2”,typeof(int),typeof(MyClass));
MyStringProperty = DependencyProperty.Register(“Result”,typeof(int),typeof(MyClass));
//CLR属性包装器,实现对依赖属相的操作
public int Input1
{
get{return (int)GetValue(Input1Property);}
set{SetValue(Input1Property,value);}
}
public int Input2
{
get{return (int)GetValue(Input2Property);}
set{SetValue(Input2Property,value);}
}
public int Result
{
get{return (int)GetValue(ResultProperty);}
set{SetValue(ResultProperty,value);}
}
}
第一个参数:“Input1”,属性名。
第二个参数:typeof(string),属性的类型。
第三个参数:typeof(MyClass),包含这个属性的类的类型。
在UI的主窗口MainWindow.xaml代码中我们对这三变量进行绑定:
<Grid>
<StackPanel>
<TextBox x:Name=”txt1” Height=”35” Text=”{Binding Input1}”></TextBox>
<TextBox x:Name=”txt2” Height=”35” Text=”{Binding Input2}”></TextBox>
<TextBox x:Name=”txt3” Height=”35” Text=”{Binding Result}”></TextBox>
<Button x:Name =“Btn1” Height=”35” Width=”80” Click=”Btn1_Click1”></ Button >
</StackPanel>
</Grid>
运行程序,在txt1中输入“2”,在txt2中输入“3”,点击按钮,我们会看到txt3中显示“5”。
哎??我们没有实现INotifyPropertyChanged接口,当属性值发生更改时与之关联的Binding对象仍旧可以得到通知,这是如何实现的呢?只有一个原因,那就是依赖属性本身就实现了通知这项功能,可以作为数据源和数据目标。
在WinForm程序开发的时候,我们知道每一个控件维护着自己的窗体和消息队列。但在WPF程序中,只有的一个窗口,也就是只有一个消息队列,由这一个消息队列去处理所有的消息。
我猜测,在WPF程序创建的时候的,有这样一个集合。
public Dictionary<int, object> PropertyDictionary = new Dictionary<string, object >();
WPF将属性名和宿主类型名生成hashcode,最后把hash code和DependencyProperty实例作为Key-Value对去存入全局的哈希表中。
DependencyProperty类中有这样一个成员:
private static Hashtable PropertyFromName = new Hashtable();
当程序运行时,会产生这样一个全局的Hashtable对象,这个Hashtable就是用于注册DependencyProperty实例的地方。
FromNameKey key = new FromNameKey(name,ownerType);
FromNameKey是一个.NETFramework内部数据类型。它的构造器代码如下:
public FromNameKey(string name,Type ownerType)
{
_name = name;
_ownerType = ownerType;
_hashcode = _name.GetHashCode()^_ownerType. GetHashCode();
}
GetHashCode()重写如下:
public override int GetHashCode()
{
return _hashCode;
}
在RegisterCommon方法中有这样一段代码:
if(PropertyFromName.Contain(key))
{
throw new ArgumentException(“这个依赖属性已经注册过了”);
}
DependencyProperty dp = new DependencyProperty( name, propertyType, ownerType);
注册一个依赖属性dp
PropertyFromName[key] = dp;
最后返回依赖属性实例:
return dp;
Binding绑定机制其自身就维护者一个绑定注册表,这个注册表中将源与目标一一对应了起来。Target<---->Source。每当UI的属性值发生改变时,WPF系统将会自动调用一个全局的委托事件处理函数,可能就是public event PropertyChangedEventHandler PropertyChanged。在这个事件中,会使用刚才提到的绑定注册表,从而维护绑定目标和绑定数据源之间的数据同步机制
Binding接收到事件后,事件消息会告诉他是哪个属性发生了改变,于是就会通知Binding目标端的UI元素属性显示新的值。
依赖属性和路由事件都是通过注册的方式来实现的,而不是通过直接new方法来实例化得到的。也就是说在类中维护着一个依赖属性注册表和路由事件注册表。而这也是WPF依赖属性和路由事件实现附加功能的幕后机制的基础。
依赖属性的更改通知:
当我们使用绑定机制实现UI元素与依赖属性绑定之后,当UI元素的Property发生改变或者是依赖属性的值发生了改变,都会激发一个PropertyChanged的事件,WPF会响应这个事件实现UI元素与依赖属性的同步。而我们知道,UI元素也是依赖属性。
————————————————
版权声明:本文为CSDN博主「李伯特」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/liebert/article/details/78035681