小序:
看着自己上一篇技术文章,屈指算来,已经月余没有动笔了——实在是不像话。最近一来是忙工作,二来是兴趣点放在了设计模式上,而且尝试着把设计模式也“深入浅出”了一把,当然啦,因为对于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三个属性,
- class Student
- {
- public int StuNum { get; set; }
- public string Name { get; set; }
- public int Age { get; set; }
- }
然后我们在Window的Grid元素里添加一个ListBox,这个操作是在XAML文件里做的:
- <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">
- <GRID>
- <LISTBOX Name="listBox1" Margin="5" Background="LightBlue" />
- </GRID>
- </WINDOW><PRE>
显示出来的效果是这样的:
接下来,我们使用集合binding,让ListBox把学生的名字显示出来。为了方便起见,我把逻辑代码写在了Window的构造函数里,请大家注意——做项目的时候要尽量保持构造函数里的“干净”,很多很多紧耦合都是不小心在构造函数里“创造”出来的。
- //水之真谛出品
- // http://blog.csdn.net/FantasiaX
- public Window1()
- {
- InitializeComponent();
- List<Student> stuList = new List<Student>()
- {
- new Student{StuNum=1, Name="Tim", Age=28},
- new Student{StuNum=2, Name="Ma Guo", Age=25},
- new Student{StuNum=3, Name="Yan", Age=25},
- new Student{StuNum=4, Name="Xaiochen", Age=28},
- new Student{StuNum=5, Name="Miao miao", Age=24},
- new Student{StuNum=6, Name="Ma Zhen", Age=24}
- };
- this.listBox1.ItemsSource = stuList;
- this.listBox1.DisplayMemberPath = "Name";
- }
立竿见影地说,你马上就能看到效果:
其实,最有用的就是最后两句代码:
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如下:
- <Window x:Class="CollectionBinding.Window1"
- xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
- xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- Title="水之真谛" Height="300" Width="300">
- <StackPanel>
- <ListBox Name="listBox1" Margin="5" Height="150" Background="LightBlue"/>
- <TextBox Name="stuNumTextBox" Margin="5" Background="LightGreen"/>
- <TextBox Name="nameTextBox" Margin="5" Background="LightGreen"/>
- <TextBox Name="ageTextBox" Margin="5" Background="LightGreen"/>
- </StackPanel>
- </Window>
效果如图:
如果客户的要求比较简单,就是选中ListBox中的一项后,只查看它的某一个属性(比如选中一个学生的名字,只看他的学号),那这时候我们有个简单的办法——每个成功男人的背后都有一个女人;每个显示出来的Text背后都隐藏着一个Value!
- public Window1()
- {
- InitializeComponent();
- List<Student> stuList = new List<Student>()
- {
- new Student{StuNum=1, Name="Tim", Age=28},
- new Student{StuNum=2, Name="Ma Guo", Age=25},
- new Student{StuNum=3, Name="Yan", Age=25},
- new Student{StuNum=4, Name="Xaiochen", Age=28},
- new Student{StuNum=5, Name="Miao miao", Age=24},
- new Student{StuNum=6, Name="Ma Zhen", Age=24}
- };
- this.listBox1.ItemsSource = stuList;
- this.listBox1.DisplayMemberPath = "Name";
- this.listBox1.SelectedValuePath = "StuNum";
- this.stuNumTextBox.SetBinding(TextBox.TextProperty, new Binding("SelectedValue") { Source = this.listBox1 });
- }
this.listBox1.SelectedValuePath = "StuNum";这句代码的意思是说:如果ListBox里的某一条Item被选中了,那么ListBox就去找到与这条Item所对应的数据源集合里的那个元素,并把这个元素的StuNum属性的值拿出来,当作当前选中Item的值。最后一句是把TextBox的Text依赖属性关联到listBox1的SelectedValue上。运行起来的效果就是:
如果客户要求显示所有信息,那这种“简装版”的binding就不灵了,因为它只能拿到一个值。这时候,我们需要这样做:
- public Window1()
- {
- InitializeComponent();
- List<Student> stuList = new List<Student>()
- {
- new Student{StuNum=1, Name="Tim", Age=28},
- new Student{StuNum=2, Name="Ma Guo", Age=25},
- new Student{StuNum=3, Name="Yan", Age=25},
- new Student{StuNum=4, Name="Xaiochen", Age=28},
- new Student{StuNum=5, Name="Miao miao", Age=24},
- new Student{StuNum=6, Name="Ma Zhen", Age=24}
- };
- this.listBox1.ItemsSource = stuList;
- this.listBox1.DisplayMemberPath = "Name";
- //this.listBox1.SelectedValuePath = "StuNum";
- this.stuNumTextBox.SetBinding(TextBox.TextProperty, new Binding("SelectedItem.StuNum") { Source = this.listBox1 });
- this.nameTextBox.SetBinding(TextBox.TextProperty, new Binding("SelectedItem.Name") { Source = this.listBox1 });
- this.ageTextBox.SetBinding(TextBox.TextProperty, new Binding("SelectedItem.Age") { Source = this.listBox1 });
- }
这回,我们使用的是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就派上用场了。
首先我们把界面改成这样:
- <Window x:Class="CollectionBinding.Window1"
- xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
- xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- Title="水之真谛" Height="300" Width="300">
- <StackPanel>
- <ListBox Name="stuListBox" Margin="5" Height="70" Background="LightBlue"/>
- <ListBox Name="tchrListBox" Margin="5" Height="70" Background="LightPink"/>
- <TextBox Name="idTextBox" Margin="5" Background="LightGreen"/>
- <TextBox Name="nameTextBox" Margin="5" Background="LightGreen"/>
- <TextBox Name="ageTextBox" Margin="5" Background="LightGreen"/>
- </StackPanel>
- </Window>
效果图:
相应地,我们重构了一下Student类和Teacher类,让它们趋于一致:
- interface ISchoolMember
- {
- int ID { get; set; }
- string Name { get; set; }
- int Age { get; set; }
- }
- class Student : ISchoolMember
- {
- public int ID { get; set; }
- public string Name { get; set; }
- public int Age { get; set; }
- }
- class Teacher : ISchoolMember
- {
- public int ID { get; set; }
- public string Name { get; set; }
- public int Age { get; set; }
- }
现在让我们看看DataContext是怎么玩儿的:
- public Window1()
- {
- InitializeComponent();
- List<Student> stuList = new List<Student>()
- {
- new Student{ID=1, Name="Tim", Age=28},
- new Student{ID=2, Name="Ma Guo", Age=25},
- new Student{ID=3, Name="Yan", Age=25},
- };
- List<Teacher> tchrList = new List<Teacher>()
- {
- new Teacher{ID=1, Name="Ma Zhen", Age=24},
- new Teacher{ID=2, Name="Miao miao", Age=24},
- new Teacher{ID=3, Name="Allen", Age=26}
- };
- stuListBox.ItemsSource = stuList;
- tchrListBox.ItemsSource = tchrList;
- stuListBox.DisplayMemberPath = "Name";
- tchrListBox.DisplayMemberPath = "Name";
- stuListBox.SelectionChanged += (sender, e) => { this.DataContext = this.stuListBox.SelectedItem; };
- tchrListBox.SelectionChanged += (sender, e) => { this.DataContext = this.tchrListBox.SelectedItem; };
- this.idTextBox.SetBinding(TextBox.TextProperty, new Binding("ID"));
- this.nameTextBox.SetBinding(TextBox.TextProperty, new Binding("Name"));
- this.ageTextBox.SetBinding(TextBox.TextProperty, new Binding("Age"));
- }
让我们来仔细品尝这段代码:
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高手多着呢:)