深入淺出WPF(7)——數據的綠色通道,Binding(上)
深入淺出WPF(7)——數據的綠色通道,Binding(上)
小序:
怎么直接從2蹦到7啦?!啊哦,實在是不好意思,最近實在是太忙了,忙的原因也非常簡單——自己的技術太差了,還有很多東西要學呀。門里門外,發現專業程序員非常重要的一項技能是讀別人寫的代碼,這項技能甚至比自己寫代碼更重要。Anstinus同學就是讀代碼的高手,我寫的代碼他看兩眼就知道怎么回事了,並且能夠立刻修改,而他的代碼我讀了好幾天還不知道是怎么回事兒呢。
2到7之間是留給XAML語言基礎的,有些文章已經快寫好了,但如果我對它不滿意,是絕對不會放到網上來的。同時,最近有很多朋友又在催我往下寫,情急之下,只好把最重要的幾節趕出來、先掛上來。
因此,毫不誇張地說,從本篇文章起接下來的幾篇文章幾乎可以說是WPF的核心內容,非常重要。這幾篇文章分別介紹了Binding、Dependency Property、Routed Event & Command等內容。精彩不斷,敬請關注!
正文:
在學習新東西的時候,人們總是習慣拿它與自己已經了解的舊有知識去做比較,這樣才掌握得快、記憶深刻。所以,經常有朋友問我:“
WPF與Windows Form最大的區別是什么?請用最簡短的話告訴我。”OK,這個問題問的非常好——看上去WPF與WinForm最大的區別像是前面講的那個XAML語言,但XAML只是個表層現象,WPF真正引人入勝、使之與WinForm涇渭分明的特點就是——“
數據驅動界面”。圍繞着這個核心,WPF准備了很多概念相當前衛的技術,其中包括為界面准備的XAML、為底層數據准備的Dependency Property和為消息傳遞准備的Routed Event & Command。
“數據驅動界面”,聽起來有點抽象。用白話解釋(
中文白話似乎總也上不了台面、更不能往書里寫,而老外的書里卻可以白話連篇)就是——
數據是底層、是心臟,數據變了作為表層的UI就會跟着變、將數據展現給用戶;如果用戶修改了UI元素上的值,相當於透過UI元素直接修改了底層的數據;數據處於核心地位,UI處於從屬地位。這樣一來,數據是程序的發動機(驅動者)、UI成了幾乎不包含任何邏輯專供用戶觀察數據和修改數據的“窗口”(被驅動者)。
順便插一句,如果你是一位WinForm程序員,“數據驅動界面”一開始會讓你感覺不太習慣。比如,在WinForm編程時,如果想對ListBox里的Item排序,我們會直接去排列這些Item,也就是針對界面進行操作,這在WPF里就行不通了——實際上,在WPF里因為界面完全是由數據決定的(甚至包括界面元素的排序),所以,我們只需要將底層數據排序,作為界面的Items也就在數據的驅動下乖乖地排好序了。
那么,數據是怎樣從底層傳遞到界面的呢?我們今天的主角,Binding同學,就要登場啦!
深入淺出話Binding
Binding同學最近很不開心,是因為它的中文名字“很暴力”——綁定。我猜,最早的譯者也沒什么標准可遵循,大概是用了諧音吧!這一諧音不要緊,搞的中國程序員就有點摸不清頭腦了。“綁”是捆綁的意思,再加上一個“定”字,頗多了幾分“綁在一起、牢不可分”的感覺。而實際呢?Binding卻是個地地道道的松耦合的關系!
依在下拙見,Binding譯為“關聯”是再合適不過了。在英語詞典里,也的確有這一詞條。關聯嗎,無需多講,人人都知道是“之間有些關系”的意思。Data Binding也就不應該再叫“數據綁定”了,應該稱為“數據關聯”,意思是說,在數據和界面(或其他數據)之間具有某些關系和聯動。
具體到WPF中,Binding又是怎樣一種關系和聯動呢?就像我們的大標題一樣——Binding就是數據的“綠色通道”。“綠色通道”代表着“直接”和“快速”,Binding就是這樣。
讓我們分享一個有趣的例子,請看下面的截圖:

這里是兩個TextBox和一個Slider組成的UI,現在客戶的需求是——當Slider的滑塊移動時,上面那個TextBox里顯示Slider的Value;反過來,當在上面那個TextBox里輸入合適的值后,鼠標焦點移開后,Slider的滑塊也要滑到相應的位置上去。
站在一個WinForm程序員的角度去考慮,他會做這樣幾件事情:
- 響應slider1的ValueChanged事件,在事件處理函數中讓textBox1顯示slider1的Value
- 響應textBox1的LostFocus事件,把textBox1的Text轉換成數值,並賦值給slider1
注意了!這就是典型的“非數據驅動界面”的思想。為什么呢?
當我們響應slider1的ValueChanged事件時,我們要的是slider1的Value這個值,這時候,slider1處於核心地位、是數據的“源”(Source);當我們響應textBox1的LostFocus事件時,我們需要的是它的Text屬性值,這時候,textBox1又成了數據的source。也就是說,在這種處理方法中,數據沒有固定的“源”,兩個UI元素是對等的、不存在誰從屬於誰的問題。換句話說:它們之間是“就事論事”,並沒有什么“關聯”。
接下來,讓我們體驗一下“綠色通道”的快捷!
上述例子的XAML源代碼如下:
- <Window x:Class="BindingSample.Window1"
- xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
- xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- Title="http://blog.csdn.net/FantasiaX" Height="300" Width="300">
- <Window.Background>
- <LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
- <GradientStop Color="Blue" Offset="0.3"/>
- <GradientStop Color="LightBlue" Offset="1"/>
- </LinearGradientBrush>
- </Window.Background>
- <Grid>
- <TextBox Height="23" Margin="10,10,9,0" Name="textBox1" VerticalAlignment="Top" />
- <TextBox Height="23" Margin="10,41,9,0" Name="textBox2" VerticalAlignment="Top" />
- <Slider Height="21" Margin="10,73,9,0" Name="slider1" VerticalAlignment="Top" Maximum="100" />
- </Grid>
- </Window>
剔除那些花里呼哨的裝飾品后,最重要的只有下面3行(而實際上第2行那個textBox2只是為了讓鼠標的焦點有個去處):
- <Grid>
- <TextBox Height="23" Margin="10,10,9,0" Name="textBox1" VerticalAlignment="Top" />
- <TextBox Height="23" Margin="10,41,9,0" Name="textBox2" VerticalAlignment="Top" />
- <Slider Height="21" Margin="10,73,9,0" Name="slider1" VerticalAlignment="Top" Maximum="100" />
- </Grid>
然后,我只需在第1行代碼上做一個小小的修改,就能完成WinForm中需要用兩個事件響應、十多行代碼才能完成的事情:
- <Grid>
- <TextBox Height="23" Margin="10,10,9,0" Name="textBox1" VerticalAlignment="Top" Text="{Binding ElementName=slider1, Path=Value}"/>
- <TextBox Height="23" Margin="10,41,9,0" Name="textBox2" VerticalAlignment="Top" />
- <Slider Height="21" Margin="10,73,9,0" Name="slider1" VerticalAlignment="Top" Maximum="100" />
- </Grid>
細心的你,一定一眼就看到只多了這樣一句話:Text="{Binding ElementName=slider1, Path=Value}"
這句話的意思是說:
Hi,textBox1,從此以后,你的Text屬性值就與slider1這個UI元素的Value屬性值關聯上了,Value變的時候你的Text也要跟着變。
這時候的效果是——你拖動Slider的滑塊,textBox1就會顯示值(雙精度,0到100之間);你在textBox1里輸入一個0到100之間的數字,當把鼠標移動到textBox2里時,slider1的滑塊會跳到相應的值上去,如圖:


非常簡單是不是?請注意,這里面可蘊含了“數據驅動界面”的模型哦!在這里,我們始終把slider1的Value當成是數據源(Data Source),而textBox1則是用來顯示和修改數據的窗口(Data Presenter)——slider1是核心,它的Value屬性值將驅動textBox1的Text進行改變;人為改變textBox1的Text屬性值,也會被送回到slider1的Value屬性值上去。
是時候讓我們了解Data Binding的幾個關鍵概念了——
- 數據源(Data Source,簡稱Source):顧名思義,它是保有數據的實體、是數據的來源、源頭。把誰當作數據源完全由程序員來決定——只要你想把它當做數據核心來使用。它可以是一個UI元素、某個類的實例,也可以是一個集合(關於對集合的綁定,非常重要,專門用一篇文章來討論之)。
- 路徑(Path):數據源作為一個實體可能保有着很多數據,你具體關注它的哪個數值呢?這個數值就是Path。就上面的例子而言,slider1是Source,它擁有很多數據——除了Value之外,還有Width、Height等,但都不是我們所關心的——所以,我們把Path設為Value。
- 目標(Target):數據將傳送到哪里去?這就是數據的目標了。上面這個例子中,textBox1是數據的Target。有一點需要格外注意:Target一定是數據的接收者、被驅動者,但它不一定是數據的顯示者——也許它只是數據聯動中的一環——后面我們給出了例子。
- 關聯(Binding):數據源與目標之間的通道。正是這個通道,使Source與Target之間關聯了起來、使數據能夠(直接或間接地)驅動界面!
- 設定關聯(Set Binding):為Target指定Binding,並將Binding指向Target的一個屬性,完成數據的“端對端”傳輸。
綠色通道上的“關卡”:
話說眼看就要到奧運會了,北京的各大交通要道上也都加強了安檢力度。微軟同學也給Binding這條“綠色通道”准備了幾道很實用的“關卡”。這些“關卡”的啟閉與設置是通過Binding的屬性來完成的。其中常用的有:
- 如果你想把“綠色通道”限制為“單行道”,那就設置Binding實例的Mode屬性,它是一個BindingMode類型的枚舉值,其中包含了TwoWay、OneWay和OneWayToSource幾個值。上面這個例子中,默認地是TwoWay,所以才會有雙向的數據傳遞。
- 如果用戶提出只要textBox1的文本改變slider1的滑塊立刻響應,那就設置Binding的UpdateSourceTrigger屬性。它是一個UpdateSourceTrigger類型枚舉值,默認值是UpdateSourceTrigger.LostFocus,所以才會在移走鼠標焦點的時候更新數據。如果把它設置為UpdateSourceTrigger.PropertyChanged,那么Target被關聯的屬性只要一改變,就立刻傳回給Source——我們要做的僅僅是改了一個單詞,而WinForm程序員這時候正頭疼呢,因為他需要去把代碼搬到另一個事件響應函數中去。
- 如果Binding兩端的數據類型不一致怎么辦?沒關系,你可以設置Binding的Converter屬性,具體內容在下篇文章中討論。
- 如果數據中有“易燃易爆”的不安全因素怎么辦?OK,可以設置Binding的ValidationRules,為它加上一組“安檢設施”(同樣也在下篇文中討論)。
在C#代碼中設置Binding
XAML代碼是如此簡單,簡單就那么一句話。這可不是吾等C#程序員、刨根問底之徒可以善罷甘休的!
形象地講,Binding就像一個盒子,盒子里裝了一些機關用於過濾和控制數據,盒子兩端各接着一根管子,管子是由管殼和管芯構成的,看上去就像下面的圖:

當腦子里有了這樣一個形象之后,遵循下面的步驟就OK了:
- Source:確定哪個對象作為數據源
- Target:確定哪個對象作為目標
- Binding:聲明一個Binding實例
- 把一根管子接到Source上並把管芯插在Source的Path上
- 把另一根管子接到Target上並把管芯插在Target的聯動屬性上
如果有必要,可以在3與4之間設置Binding的“關卡”們。其實,第3步之后的順序不是固定的,只是這個步驟比較好記——一概向右連接。所得結果看上去是這樣:

我猜你可能會問:“那個D.P.是什么呀?”
D.P.的全稱是“Dependency Property”,直譯過來就是“依賴式屬性”,意思是說它自己本身是沒有值的,它的值是“依賴”在其它對象的屬性值上、通過Binding的傳遞和轉換而得來的。表現在例子里,它就是Target上的被數據所驅動的聯動屬性了!
這里是等價的C#代碼,我把它寫在了Window1的構造函數里:
- public Window1()
- {
- InitializeComponent();
- // 1. 我打算用slider1作為Source
- // 2. 我打算用textBox1作為Target
- Binding binding = new Binding();
- binding.Source = this.slider1;
- binding.Path = new PropertyPath("Value");
- this.textBox1.SetBinding(TextBox.TextProperty, binding);
- }
有意思的是,Source端的操作,接管子和插管芯分兩步,而Target端卻是在SetBinding方法中一步完成。注意啦,TextBox.TextProperty就是一個Dependency Property的廬山真面目!關於Dependency Property的文檔業已完成,我將擇一黃道吉日掛將出來:p
上面的代碼稍有簡化的余地,那就是把Path的設定轉移到Binding的構造中去:
- public Window1()
- {
- InitializeComponent();
- // 1. 我打算用slider1作為Source
- // 2. 我打算用textBox1作為Target
- Binding binding = new Binding("Value");
- binding.Source = this.slider1;
- this.textBox1.SetBinding(TextBox.TextProperty, binding);
- }
這樣做的好處是——隨便你給binding指定一個Source,只要這個Source有“Value”這個屬性,binding就會自動提取它的值並傳輸給Target端。
我們還可以為binding設些“關卡”:
- public Window1()
- {
- InitializeComponent();
- // 1. 我打算用slider1作為Source
- // 2. 我打算用textBox1作為Target
- Binding binding = new Binding("Value");
- binding.Source = this.slider1;
- binding.Mode = BindingMode.TwoWay;
- binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
- this.textBox1.SetBinding(TextBox.TextProperty, binding);
- }
還有一個小小的提示:如果Source碰巧是一個UI元素,那么也可將binding.Source = this.slider1;改寫成binding.ElementName = "slider1";或者binding.ElementName = this.slider1.Name;
自定義數據源:
在我們項目組日常的工作中,經常需要自己寫一個類,並且拿它的實例當作數據源。怎樣才能讓一個類成為“合格的”數據源呢?
要訣就是:
- 為這個類定義一些Property,相當於為Binding提供Path
- 讓這個類實現INotifyPropertyChanged接口。實現這個接口的目的是當Source的屬性值改變后通知Binding(不然人家怎么知道源頭的數據變了並進行聯動協同呢?),好讓Binding把數據傳輸給Target——本質上還是使用事件機制來做,只是掩蓋在底層、不用程序員去寫event handler了。
讓我們寫一個這樣的類:
- <PRE class=csharp name="code">// 第一步:聲明一個類,准備必要的屬性
- public class Student
- {
- private int id;
- public int Id
- {
- get { return id; }
- set { id = value; }
- }
- private string name;
- public string Name
- {
- get { return name; }
- set { name = value; }
- }
- private int age;
- public int Age
- {
- get { return age; }
- set { age = value; }
- }
- }</PRE>
接下來就是使用INotifyPropertyChanged接口“武裝”這個類了,注意,這個接口在System.ComponentModel名稱空間中:
- // 第二步:實現INotifyPropertyChanged接口
- public class Student : INotifyPropertyChanged
- {
- public event PropertyChangedEventHandler PropertyChanged; // 這個接口僅包含一個事件而已
- private int id;
- public int Id
- {
- get { return id; }
- set
- {
- id = value;
- if (this.PropertyChanged != null)
- {
- this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Id")); // 通知Binding是“Id”這個屬性的值改變了
- }
- }
- }
- private string name;
- public string Name
- {
- get { return name; }
- set
- {
- name = value;
- if (this.PropertyChanged != null)
- {
- this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Name")); // 通知Binding是“Name”這個屬性的值改變了
- }
- }
- }
- private int age;
- public int Age
- {
- get { return age; }
- set { age = value; } // Age的值改變時不進行通知
- }
- OK,此時,你可以嘗試使用Student類的實例作為數據源了!
自定義數據目標:
往而不來,非禮也;來而不往,亦非禮也——《禮記·曲禮》
知道了如何定義數據源,一定想一鼓作氣再定義一個數據目標吧?讓我們回想一下:Binding接在Target一端的管子,它的管芯是插在一個Dependency Property上的!所以,在我們熟悉Dependency Property之前,恐怕只能使用現成的.NET對象來充當Target了!
所以,敬請關注《深入淺出WPF》系統的后續文章!