深入淺出WPF(9)——數據的綠色通道,Binding(下)

深入淺出WPF(9)——數據的綠色通道,Binding(下)

 

小序:

看着自己上一篇技術文章,指算來,已經月餘沒有動筆了——實在是不像話。最近一來是忙工作,二來是興趣點放在了設計模式上,而且嘗試着把設計模式也“深入淺出”了一把,當然啦,因爲對於design pattern我也是初學,在沒有經過大家檢驗之前我是不敢拿到blog裏丟人現眼滴~~~現在項目組裏由喵喵同學、美女燕、大馬同學和小馬同學一同push一個“設計模式沙龍”,大家一起學習和討論這些模式和如何應用在我們的項目裏做重構。等活動結束後,我心裏有底了,就把文章放上來:)

 

N久不動筆了……上回寫到哪兒了?呃~~~咱們繼續吧!

 

正文

如果用一句話概括前幾篇關於data binding的文章,那就是:介紹了數據驅動(界面)開發的基本原理,以及如何使用Binding類的實例連接數據源與數據表現元素、形成一對一的binding(爲了讓數據有效、安全,我們還可以添加Converter和ValidationRule等附件)

 

注意啦,我強調了一下——是一對一的binding哦!也就是說,一個binding實例一端是數據源、一端是表現元素。現在問題來了:實際工作中,我們操作的大部分數據都是集合,怎麼進行“羣體binding”呢?呵呵,這就引出了我們今天的第一個topic——對集合進行binding。

 

集合Binding揭祕

我們想這樣一個問題——如果我有一個List<Student>的實例,裏面裝着二十個Student對象,現在我想讓一個ListBox顯示出學生的Name,並且當集合中有Student對象的Name發生改變時,ListBox的Item也立刻顯示出來,應該怎麼做呢?

有人會說:那還不好辦?做一個循環,按照集合元素的數量生成相應多的ListBoxItem,並把每個ListBoxItem的Text屬性(如果有)用Binding一對一連接到List中的Student對象上不就結了?

我沒試過這樣行不行,但我知道,這違反了“數據驅動UI”的原則——請記住,在WPF開發時,不到萬不得已,不要去打UI元素的主意、不要把UI元素摻合進任何運算邏輯。拿上面的例子來說,手動地去生成ListBoxItem就已經超越了“數據驅動UI”的限制,是不恰當的作法。

OK,讓我們看看微軟提供的“正宗集合binding”吧!

首先我們得準備一個用來存放數據的集合,對於這個集合有一個特殊的要求,那就是,這個集合一定要是實現了IEnumerable接口的集合。爲什麼呢?原因很簡單,實現了IEnumerable接口就意味着這個集合裏的元素是可枚舉的,可枚舉就意味着這個集合裏的元素是同一個類型的(至少具有相同的父類),元素是同一個類型的就意味着在每個元素中我都能找到同樣的屬性。舉個例子,如果一個實現了IEnumerable的集合裏裝的是Student元素,這就意味着每個元素都有諸如ID、Name、Age等屬性,對於任何一個元素我都不會找不到ID、Name或者Age——不然就沒辦法“批量binding”了;如果一個實現了IEnumerable接口的集合裏除了有Student對象,還有Teacher對象、Programmer對象,怎麼辦呢?這時候,這個集合肯定只能拿Student、Teacher、Programmer的共同基類來進行枚舉了,假設它們的共同基類是Human,那Human至少會有Name和Age屬性吧——我們可以拿這兩個屬性去做binding的Path,而集合裏的每一個元素都作爲一個獨立的數據源。

下面我給出核心代碼。

 

首先我們準備了一個Student類,包含StuNum、Name、Age三個屬性,

[Csharp] view plain copy
  1. class Student  
  2. {  
  3.     public int StuNum { getset; }  
  4.     public string Name { getset; }  
  5.     public int Age { getset; }  
  6. }  

然後我們在Window的Grid元素裏添加一個ListBox,這個操作是在XAML文件裏做的:

 

 

[XML] view plain copy
  1. <window title="Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" x:class="CollectionBinding.Window1" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" height="300" width="300">  
  2.     <grid>  
  3.         <listbox name="listBox1" margin="5" background="LightBlue">  
  4.     </listbox></grid>  
  5. </window><pre></pre>  

顯示出來的效果是這樣的:

 

 

接下來,我們使用集合binding,讓ListBox把學生的名字顯示出來。爲了方便起見,我把邏輯代碼寫在了Window的構造函數裏,請大家注意——做項目的時候要儘量保持構造函數裏的“乾淨”,很多很多緊耦合都是不小心在構造函數裏“創造”出來的。

 

  1. //水之真諦出品
  2. // http://blog.csdn.net/FantasiaX
  3. public Window1()
  4. {
  5.     InitializeComponent();
  6.     List<Student> stuList = new List<Student>() 
  7.     {
  8.         new Student{StuNum=1, Name="Tim", Age=28},
  9.         new Student{StuNum=2, Name="Ma Guo", Age=25},
  10.         new Student{StuNum=3, Name="Yan", Age=25},
  11.         new Student{StuNum=4, Name="Xaiochen", Age=28},
  12.         new Student{StuNum=5, Name="Miao miao", Age=24},
  13.         new Student{StuNum=6, Name="Ma Zhen", Age=24}
  14.     };
  15.     this.listBox1.ItemsSource = stuList;
  16.     this.listBox1.DisplayMemberPath = "Name";
  17. }

立竿見影地說,你馬上就能看到效果:

 

 

其實,最有用的就是最後兩句代碼:

this.listBox1.ItemsSource = stuList;一句的意思是告訴ListBox說:stuList這個集合裏的元素就是你的條目啦!也就是說,stuList就等同於listBox1.Items了。集合對集合,意味着兩個集合裏的元素也將一一對應。

顯然,stuList集合裏的元素是ListBox.Items集合裏元素的數據源,兩個集合裏的元素一一對應。

還有一句,this.listBox1.DisplayMemberPath = "Name";,是告訴ListBox說,你的每個條目不是要顯示點東西給用戶看嗎?那你就顯示“Name”屬性的值吧!

你可能會問:它怎麼知道去找Student對象的Name屬性呀?你想呀,前面說過,能用於做數據源的集合一定實現了IEnumerable接口(List<>就實現了這個接口),也就是說,我可以枚舉出一個一個的Student對象,又因爲每個Items裏的元素都與stuList裏的一個Student對象一一對應、每個Student對象肯定有Name屬性,so,很容易就Binding上了。

很好玩兒,是吧!讓我看接着往下看——常見的客戶需求是:在ListBox裏顯示一個什麼東西的名稱,點上去之後,在一個明細表單裏顯示出每一個條目的詳細信息。讓我們改造一下我們的程序!

 

首先,我修改了UI,XAML如下:

 

  1. <Window x:Class="CollectionBinding.Window1"
  2.     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.     Title="水之真諦" Height="300" Width="300">
  5.     <StackPanel>
  6.         <ListBox Name="listBox1" Margin="5" Height="150" Background="LightBlue"/>
  7.         <TextBox Name="stuNumTextBox"  Margin="5" Background="LightGreen"/>
  8.         <TextBox Name="nameTextBox"  Margin="5" Background="LightGreen"/>
  9.         <TextBox Name="ageTextBox"  Margin="5" Background="LightGreen"/>
  10.     </StackPanel>
  11. </Window>

效果如圖:

 

 

如果客戶的要求比較簡單,就是選中ListBox中的一項後,只查看它的某一個屬性(比如選中一個學生的名字,只看他的學號),那這時候我們有個簡單的辦法——每個成功男人的背後都有一個女人;每個顯示出來的Text背後都隱藏着一個Value!

 

  1. public Window1()
  2. {
  3.     InitializeComponent();
  4.     List<Student> stuList = new List<Student>() 
  5.     {
  6.         new Student{StuNum=1, Name="Tim", Age=28},
  7.         new Student{StuNum=2, Name="Ma Guo", Age=25},
  8.         new Student{StuNum=3, Name="Yan", Age=25},
  9.         new Student{StuNum=4, Name="Xaiochen", Age=28},
  10.         new Student{StuNum=5, Name="Miao miao", Age=24},
  11.         new Student{StuNum=6, Name="Ma Zhen", Age=24}
  12.     };
  13.     this.listBox1.ItemsSource = stuList;
  14.     this.listBox1.DisplayMemberPath = "Name";
  15.     this.listBox1.SelectedValuePath = "StuNum";
  16.     this.stuNumTextBox.SetBinding(TextBox.TextProperty, new Binding("SelectedValue") { Source = this.listBox1 });
  17. }

this.listBox1.SelectedValuePath = "StuNum";這句代碼的意思是說:如果ListBox裏的某一條Item被選中了,那麼ListBox就去找到與這條Item所對應的數據源集合裏的那個元素,並把這個元素的StuNum屬性的值拿出來,當作當前選中Item的值。最後一句是把TextBox的Text依賴屬性關聯到listBox1的SelectedValue上。運行起來的效果就是:

 

 

如果客戶要求顯示所有信息,那這種“簡裝版”的binding就不靈了,因爲它只能拿到一個值。這時候,我們需要這樣做:

 

  1. public Window1()
  2. {
  3.     InitializeComponent();
  4.     List<Student> stuList = new List<Student>() 
  5.     {
  6.         new Student{StuNum=1, Name="Tim", Age=28},
  7.         new Student{StuNum=2, Name="Ma Guo", Age=25},
  8.         new Student{StuNum=3, Name="Yan", Age=25},
  9.         new Student{StuNum=4, Name="Xaiochen", Age=28},
  10.         new Student{StuNum=5, Name="Miao miao", Age=24},
  11.         new Student{StuNum=6, Name="Ma Zhen", Age=24}
  12.     };
  13.     this.listBox1.ItemsSource = stuList;
  14.     this.listBox1.DisplayMemberPath = "Name";
  15.     //this.listBox1.SelectedValuePath = "StuNum";
  16.     this.stuNumTextBox.SetBinding(TextBox.TextProperty, new Binding("SelectedItem.StuNum") { Source = this.listBox1 });
  17.     this.nameTextBox.SetBinding(TextBox.TextProperty, new Binding("SelectedItem.Name") { Source = this.listBox1 });
  18.     this.ageTextBox.SetBinding(TextBox.TextProperty, new Binding("SelectedItem.Age") { Source = this.listBox1 });
  19. }

這回,我們使用的是ListBox的SelectedItem屬性——每當我們選中ListBox(包括其它ItemsControl)中的一個Item時,ListBox都會“默默地”自動從數據源集合裏選出與當前選中Item相對應的那個條目,作爲自己的SelectedItem屬性值。而且,上面這個例子裏我們使用到了“多級路徑”——"SelectedItem.Age",實際項目中,你可以一路“點”下去,直到取出你想要的值。

初學者一般會在這兩個地方遇到問題:

1. Q:爲什麼this.nameTextBox.SetBinding(TextBox.TextProperty, new Binding("SelectedItem.Name") { Source = this.listBox1 });可以,而改成this.nameTextBox.SetBinding(TextBox.TextProperty, new Binding("Name") { Source = this.listBox1.SelectedItem });卻不行了呢?它們指向的值是一樣的呀!

A:第一句,Binding的Source是listBox1,這個對象在整個程序中都不變,任何時候我們都能找到它的SelectedItem並且根據要求取出Name屬性;第二句,Binding的Source是listBox1.SelectedItem,每次listBox1的選中項改變後,listBox1.SelectedItem都會是一個新的對象!而上面這段代碼是寫在構造函數裏的,只在窗體構造的時候執行一次,所以就不靈了。如果想讓第二句與第一句達到同樣的效果,你需要把第二句寫到listBox1.SelectionChanged事件的處理函數裏去——這就失去Binding的本意了。

2. Q:爲什麼我在試圖把listBox1.SelectedItem轉換成ListBoxItem時,程序會拋出異常呢?A:因爲SelectedItem指的是數據源集合裏與界面中選中Item對應的那個對象,所以,它的類型是數據源集合的元素類型——在“數據驅動UI”的WPF中,請不要把“數據”和“界面”攪在一起。

 

================================

我在想:有人能讀到這兒嗎?

================================

 

數據的“制高點”——DataContext

所去8年,那時候哥們兒還混跡於某農業院校……在反恐流行起來之前,我們幾個兄弟最喜歡玩兒的是《三角洲部隊》。有一種遊戲模式叫“搶山頭”,也就是攻佔制高點啦!制高點意味着什麼?它意味着站在下面的人都可以看見站在上面的人,而且一旦另一個人上來了,就會把前一個擠下去

今天我們要討論滴不是遊戲,掙錢要緊,學習WPF先。WPF也爲我們準備了一個用來放置數據的“制高點”——DataContext。

怎麼理解這個數據制高點呢?讓我們接着看上面的程序。現在客戶的需求又變了:要求在窗體裏顯示兩個ListBox,一個裏面顯示學生列表,一個裏面顯示老師列表,選中任何一個ListBox裏的項,下面的TextBox都顯示相應的詳細信息。

這時候我們遇到困難了!因爲一個UI元素不可能binding到兩個數據源上啊!怎麼辦呢?這時候DataContext就派上用場了。

 

首先我們把界面改成這樣:

 

  1. <Window x:Class="CollectionBinding.Window1"
  2.     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.     Title="水之真諦" Height="300" Width="300">
  5.     <StackPanel>
  6.         <ListBox Name="stuListBox" Margin="5" Height="70" Background="LightBlue"/>
  7.         <ListBox Name="tchrListBox" Margin="5" Height="70" Background="LightPink"/>
  8.         <TextBox Name="idTextBox"  Margin="5" Background="LightGreen"/>
  9.         <TextBox Name="nameTextBox"  Margin="5" Background="LightGreen"/>
  10.         <TextBox Name="ageTextBox"  Margin="5" Background="LightGreen"/>
  11.     </StackPanel>
  12. </Window>

效果圖:

 

 

相應地,我們重構了一下Student類和Teacher類,讓它們趨於一致:

 

  1. interface ISchoolMember
  2. {
  3.      int ID { getset; }
  4.      string Name { getset; }
  5.      int Age { getset; }
  6. }
  7. class Student : ISchoolMember
  8. {
  9.     public int ID { getset; }
  10.     public string Name { getset; }
  11.     public int Age { getset; }
  12. }
  13. class Teacher : ISchoolMember
  14. {
  15.     public int ID { getset; }
  16.     public string Name { getset; }
  17.     public int Age { getset; }
  18. }

現在讓我們看看DataContext是怎麼玩兒的:

 

  1. public Window1()
  2. {
  3.     InitializeComponent();
  4.     List<Student> stuList = new List<Student>() 
  5.     {
  6.         new Student{ID=1, Name="Tim", Age=28},
  7.         new Student{ID=2, Name="Ma Guo", Age=25},
  8.         new Student{ID=3, Name="Yan", Age=25},
  9.     };
  10.     List<Teacher> tchrList = new List<Teacher>()
  11.     {
  12.         new Teacher{ID=1, Name="Ma Zhen", Age=24},
  13.         new Teacher{ID=2, Name="Miao miao", Age=24},
  14.         new Teacher{ID=3, Name="Allen", Age=26}
  15.     };
  16.     stuListBox.ItemsSource = stuList;
  17.     tchrListBox.ItemsSource = tchrList;
  18.     stuListBox.DisplayMemberPath = "Name";
  19.     tchrListBox.DisplayMemberPath = "Name";
  20.     stuListBox.SelectionChanged += (sender, e) => { this.DataContext = this.stuListBox.SelectedItem; };
  21.     tchrListBox.SelectionChanged += (sender, e) => { this.DataContext = this.tchrListBox.SelectedItem; };
  22.     this.idTextBox.SetBinding(TextBox.TextProperty, new Binding("ID"));
  23.     this.nameTextBox.SetBinding(TextBox.TextProperty, new Binding("Name"));
  24.     this.ageTextBox.SetBinding(TextBox.TextProperty, new Binding("Age"));
  25. }

 

 

讓我們來仔細品嚐這段代碼:

 

stuListBox.SelectionChanged += (sender, e) => { this.DataContext = this.stuListBox.SelectedItem; };
tchrListBox.SelectionChanged += (sender, e) => { this.DataContext = this.tchrListBox.SelectedItem; };

這兩句是兩個Lambda表達式,實際上就是兩個事件處理函數的縮寫——讓下游程序員不用跳轉就知道兩個ListBox在各自的SelectionChanged事件發生時都做什麼事情。我們這裏做的事情就是:哪個ListBox的選中項改變了,那就把選中的數據放到窗體的DataContext屬性裏,隱含地,就把前一個數據給擠走了。

 

有意思的是最後三句:在爲三個TextBox設置Binding的時候,我沒有提供數據源——但程序一樣work,爲什麼呢?前面我說了,DataContext是“制高點”,當一個元素髮現自己有Binding但這個Binding沒有Source時,它就會“向上看”——它自然會看到制高點上的數據,這時候它會拿這個數據來試一試,有沒有Binding所指示的Path——有,就拿來用;沒有,就再往上層去找,也就是找更高的制高點——山外有山、天外有天、控件外面套控件:p

 

實際項目中,我會根據數據的影響範圍來選擇在哪一級上設置DataContext,以及把什麼對象設置爲DataContext。比如:一個ListBox裏的SelectedItem需要被包含它的Grid裏的其它元素共享,我就可以把ListBox.SelectedItem設置爲Grid的DataContext,而沒必要把ListBox設置爲最頂層Window的DataContext——原則就是“範圍正好,影響最小”。

 

=====================================

快累吐血了~~~~

=====================================

 

結語:

 

Binding的基本知識終於講完了~~~~深呼了一口氣~~~~希望對大家有點用吧!WPF目前在國內不算火,不過我想,等火起來的時候,這篇文章能派上大用場。

 

提醒大家一點,本文中很多C#代碼(特別是與Binding相關的地方)是可以挪到XAML裏去的,只是爲了講解方便,我用C#實現的,實際項目中,請大家靈活掌握。

 

我能寫出這幾篇文章來,非常感謝我的同事Anstinus,若不是他對我學習WPF的大力支持和指導,我不可能學這麼快。同時還要感謝我的前搭檔——美女Yan(這傢伙調到另外一個組去了)、Yan她mentor(Allen)和我的夥伴們~~~我要說的是,感謝你們!文章記載的不光是技術,還有我們的友情——幾十年之後翻開它,WPF可能早已經過時,但我們的友情將歷久彌新……

 

另外,Binding作爲WPF的核心技術,遠不止這點內容,其他重要的內容還包括:

  • Binding與Routed Event結合(常見的是在有數據流動時,Binding拋出一些Routed Event,由外界捕捉處理)
  • Binding與Command結合
  • Binding與ItemsControl的ItemTemplate/CellTemplate等DataTemplate的結合——這個非常重要,甚至是每天工作的主要內容,我會用專門的文章去介紹
  • 如果你想自己創建一個集合類,讓它可以與Binding配合使用,別忘了它的元素一定要實現INotifyPropertyChanged接口,而這個集合自身要是(或者派生自)ObservableCollection<T>……實際上太多東西需要在實際工作中去摸索和掌握了,一兩篇文章只是杯水車薪——我也不想像瓊瑤姐姐那樣上、中、下、繼、再繼、再再繼……

 

So far,如果你在工作中遇到問題,可以隨時聯繫我,我非常歡迎與大家討論技術問題,就算我不會,我們team高手多着呢:)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章