深入浅出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高手多着呢:)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值