zoukankan      html  css  js  c++  java
  • 【转载】wpf学习笔记5

    深入浅出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.     public int StuNum { get; set; } 
    3.     public string Name { get; set; } 
    4.     public int Age { get; set; } 

    然后我们在Window的Grid元素里添加一个ListBox,这个操作是在XAML文件里做的:

    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.     </GRID> 
    5. </WINDOW><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 { get; set; }
    4.      string Name { get; set; }
    5.      int Age { get; set; }
    6. }
    7. class Student : ISchoolMember
    8. {
    9.     public int ID { get; set; }
    10.     public string Name { get; set; }
    11.     public int Age { get; set; }
    12. }
    13. class Teacher : ISchoolMember
    14. {
    15.     public int ID { get; set; }
    16.     public string Name { get; set; }
    17.     public int Age { get; set; }
    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高手多着呢:)

  • 相关阅读:
    vue使用elementui合并table
    使用layui框架导出table表为excel
    vue使用elementui框架,导出table表格为excel格式
    前台传数据给后台的几种方式
    uni.app图片同比例缩放
    我的博客
    【C语言】取16进制的每一位
    SharePoint Solution 是如何部署的呢 ???
    无效的数据被用来用作更新列表项 Invalid data has been used to update the list item. The field you are trying to update may be read only.
    SharePoint 判断用户在文件夹上是否有权限的方法
  • 原文地址:https://www.cnblogs.com/fx2008/p/2423499.html
Copyright © 2011-2022 走看看