深入浅出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三个属性,
  1. class Student   
  2. {   
  3.     public int StuNum { getset; }   
  4.     public string Name { getset; }   
  5.     public int Age { getset; }   
  6. }  
然后我们在Window的Grid元素里添加一个ListBox,这个操作是在XAML文件里做的:
 
 
  1. <WINDOW title=Window1 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Width="300" Height="300" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="CollectionBinding.Window1">  
  2.     <GRID>  
  3.         <LISTBOX Background="LightBlue" Margin="5" Name="listBox1" />  
  4.     </GRID>  
  5. </WINDOW><PRE></PRE>  
显示出来的效果是这样的:
 
 
接下来,我们使用集合binding,让ListBox把学生的名字显示出来。为了方便起见,我把逻辑代码写在了Window的构造函数里,请大家注意——做项目的时候要尽量保持构造函数里的“干净”,很多很多紧耦合都是不小心在构造函数里“创造”出来的。
 
  1. //水之真谛出品
  2. // [url]http://blog.csdn.net/FantasiaX[/url]
  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高手多着呢:)









本文转自 水之真谛 51CTO博客,原文链接:http://blog.51cto.com/liutiemeng/95283,如需转载请自行联系原作者
目录
相关文章
|
2月前
|
传感器 C# 监控
硬件交互新体验:WPF与传感器的完美结合——从初始化串行端口到读取温度数据,一步步教你打造实时监控的智能应用
【8月更文挑战第31天】本文通过详细教程,指导Windows Presentation Foundation (WPF) 开发者如何读取并处理温度传感器数据,增强应用程序的功能性和用户体验。首先,通过`.NET Framework`的`Serial Port`类实现与传感器的串行通信;接着,创建WPF界面显示实时数据;最后,提供示例代码说明如何初始化串行端口及读取数据。无论哪种传感器,只要支持串行通信,均可采用类似方法集成到WPF应用中。适合希望掌握硬件交互技术的WPF开发者参考。
40 0
|
2月前
|
数据处理 开发者 C#
WPF数据绑定实战:从零开始,带你玩转数据与界面同步,让你的应用程序更上一层楼!
【8月更文挑战第31天】在WPF应用开发中,数据绑定是核心技能之一,它能实现界面元素与数据源的同步更新。本文详细介绍了WPF数据绑定的概念与实现方法,包括属性绑定、元素绑定及路径绑定等技术,并通过示例代码展示了如何创建数据绑定。通过数据绑定,开发者不仅能简化代码、提高可维护性,还能提升用户体验。无论初学者还是有经验的开发者,都能从中受益,更好地掌握WPF数据绑定技巧。
32 0
|
2月前
|
开发框架 前端开发 JavaScript
循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(10) -- 在DataGrid上直接编辑保存数据
循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(10) -- 在DataGrid上直接编辑保存数据
|
2月前
|
开发框架 前端开发 搜索推荐
循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(4) -- 实现DataGrid数据的导入和导出操作
循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(4) -- 实现DataGrid数据的导入和导出操作
|
2月前
|
开发框架 .NET C#
WPF/C#:显示分组数据的两种方式
WPF/C#:显示分组数据的两种方式
43 0
|
2月前
|
XML C# 数据格式
WPF/C#:如何将数据分组显示
WPF/C#:如何将数据分组显示
33 0
|
2月前
|
C# Windows
WPF/C#:如何显示具有层级关系的数据
WPF/C#:如何显示具有层级关系的数据
38 0
|
存储 自然语言处理 C#
WPF技术之Binding
WPF(Windows Presentation Foundation)是微软推出的一种用于创建应用程序用户界面的框架。Binding(绑定)是WPF中的一个重要概念,它用于在界面元素和数据源之间建立关联。通过Binding,可以将界面元素(如文本框、标签、列表等)与数据源(如对象、集合、属性等)进行绑定,从而实现数据的双向传递和同步更新。
252 2
WPF技术之Binding
|
11月前
|
算法 C# UED
浅谈WPF之控件模板和数据模板
WPF不仅支持传统的Windows Forms编程的用户界面和用户体验设计,同时还推出了以模板为核心的新一代设计理念。在WPF中,通过引入模板,将数据和算法的“内容”和“形式”进行解耦。模板主要分为两大类:数据模板【Data Template】和控件模板【Control Template】。
175 8
WPF-Binding问题-模板样式使用Binding TemplatedParent与TemplateBinding区别
WPF-Binding问题-模板样式使用Binding TemplatedParent与TemplateBinding区别
184 0