• 【转载】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高手多着呢:)

  • 相关阅读:
    ubuntun16.04不支持intel的最新网卡,升级到17.10后解决
    python网络爬虫之使用scrapy下载文件
    Django之QuerySet 创建对象
    一起来学linux:磁盘与文件系统:
    python自动化运维九:Playbook
    一起来学linux:FTP服务器搭建
    Learning Scrapy 中文版翻译 第二章
    python自动化运维八:Ansible
    linux c编程:make编译一
    【原创】大叔经验分享(27)linux服务器升级glibc故障恢复
  • 原文地址:https://www.cnblogs.com/fx2008/p/2423499.html
Copyright © 2020-2023  润新知