剖析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——這關鍵一步通過上面示范的反射機制即可勝任。

這就是屬性名從特定屬性內部流出直到指導控件更新的過程,可謂環環相扣精巧嚴密。

 

 

局限於篇幅,我不能事無巨細地說明每一個細節,請讀者對想深入理解的點查閱更多的資料,定會收獲良多。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM