深入浅出WPF(7)——数据的绿色通道,Binding(上)

水之真谛关注6人评论28117人阅读2008-06-23 02:40:00

 http://liuteimeng.blog.51cto.com/120361/
深入浅出WPF(7)——数据的绿色通道,Binding(上)
小序:
怎么直接从2蹦到7啦?!啊哦,实在是不好意思,最近实在是太忙了,忙的原因也非常简单——自己的技术太差了,还有很多东西要学呀。门里门外,发现专业程序员非常重要的一项技能是读别人写的代码,这项技能甚至比自己写代码更重要。Anstinus同学就是读代码的高手,我写的代码他看两眼就知道怎么回事了,并且能够立刻修改,而他的代码我读了好几天还不知道是怎么回事儿呢。
2到7之间是留给XAML语言基础的,有些文章已经快写好了,但如果我对它不满意,是绝对不会放到网上来的。同时,最近有很多朋友又在催我往下写,情急之下,只好把最重要的几节赶出来、先挂上来。
因此,毫不夸张地说,从本篇文章起接下来的几篇文章几乎可以说是WPF的核心内容,非常重要。这几篇文章分别介绍了Binding、Dependency Property、Routed Event & Command等内容。精彩不断,敬请关注!
正文:
在学习新东西的时候,人们总是习惯拿它与自己已经了解的旧有知识去做比较,这样才掌握得快、记忆深刻。所以,经常有朋友问我:“WPF与Windows Form最大的区别是什么?请用最简短的话告诉我。”OK,这个问题问的非常好——看上去WPF与WinForm最大的区别像是前面讲的那个XAML语言,但XAML只是个表层现象,WPF真正引人入胜、使之与WinForm泾渭分明的特点就是——“数据驱动界面”。围绕着这个核心,WPF准备了很多概念相当前卫的技术,其中包括为界面准备的XAML、为底层数据准备的Dependency Property和为消息传递准备的Routed Event & Command。
“数据驱动界面”,听起来有点抽象。用白话解释(中文白话似乎总也上不了台面、更不能往书里写,而老外的书里却可以白话连篇)就是——数据是底层、是心脏,数据变了作为表层的UI就会跟着变、将数据展现给用户;如果用户修改了UI元素上的值,相当于透过UI元素直接修改了底层的数据;数据处于核心地位,UI处于从属地位。这样一来,数据是程序的发动机(驱动者)、UI成了几乎不包含任何逻辑专供用户观察数据和修改数据的“窗口”(被驱动者)。
 顺便插一句,如果你是一位WinForm程序员,“数据驱动界面”一开始会让你感觉不太习惯。比如,在WinForm编程时,如果想对ListBox里的Item排序,我们会直接去排列这些Item,也就是针对界面进行操作,这在WPF里就行不通了——实际上,在WPF里因为界面完全是由数据决定的(甚至包括界面元素的排序),所以,我们只需要将底层数据排序,作为界面的Items也就在数据的驱动下乖乖地排好序了。
那么,数据是怎样从底层传递到界面的呢?我们今天的主角,Binding同学,就要登场啦!
深入浅出话Binding
Binding同学最近很不开心,是因为它的中文名字“很暴力”——绑定。我猜,最早的译者也没什么标准可遵循,大概是用了谐音吧!这一谐音不要紧,搞的中国程序员就有点摸不清头脑了。“绑”是捆绑的意思,再加上一个“定”字,颇多了几分“绑在一起、牢不可分”的感觉。而实际呢?Binding却是个地地道道的松耦合的关系!
依在下拙见,Binding译为“关联”是再合适不过了。在英语词典里,也的确有这一词条。关联吗,无需多讲,人人都知道是“之间有些关系”的意思。Data Binding也就不应该再叫“数据绑定”了,应该称为“数据关联”,意思是说,在数据和界面(或其他数据)之间具有某些关系和联动。
具体到WPF中,Binding又是怎样一种关系和联动呢?就像我们的大标题一样——Binding就是数据的“绿色通道”。“绿色通道”代表着“直接”和“快速”,Binding就是这样。
让我们分享一个有趣的例子,请看下面的截图:
这里是两个TextBox和一个Slider组成的UI,现在客户的需求是——当Slider的滑块移动时,上面那个TextBox里显示Slider的Value;反过来,当在上面那个TextBox里输入合适的值后,鼠标焦点移开后,Slider的滑块也要滑到相应的位置上去。
站在一个WinForm程序员的角度去考虑,他会做这样几件事情:
  1. 响应slider1的ValueChanged事件,在事件处理函数中让textBox1显示slider1的Value
  2. 响应textBox1的LostFocus事件,把textBox1的Text转换成数值,并赋值给slider1
注意了!这就是典型的“非数据驱动界面”的思想。为什么呢?
当我们响应slider1的ValueChanged事件时,我们要的是slider1的Value这个值,这时候,slider1处于核心地位、是数据的“源”(Source);当我们响应textBox1的LostFocus事件时,我们需要的是它的Text属性值,这时候,textBox1又成了数据的source。也就是说,在这种处理方法中,数据没有固定的“源”,两个UI元素是对等的、不存在谁从属于谁的问题。换句话说:它们之间是“就事论事”,并没有什么“关联”。
接下来,让我们体验一下“绿色通道”的快捷!
上述例子的XAML源代码如下:
  1. <Window x:Class="BindingSample.Window1"
  2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4. Title="http://blog.csdn.net/FantasiaX" Height="300" Width="300">
  5. <Window.Background>
  6. <LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
  7. <GradientStop Color="Blue"  Offset="0.3"/>
  8. <GradientStop Color="LightBlue" Offset="1"/>
  9. </LinearGradientBrush>
  10. </Window.Background>
  11. <Grid>
  12. <TextBox Height="23" Margin="10,10,9,0" Name="textBox1" VerticalAlignment="Top" />
  13. <TextBox Height="23" Margin="10,41,9,0" Name="textBox2" VerticalAlignment="Top" />
  14. <Slider Height="21" Margin="10,73,9,0" Name="slider1" VerticalAlignment="Top" Maximum="100" />
  15. </Grid>
  16. </Window>
 
剔除那些花里呼哨的装饰品后,最重要的只有下面3行(而实际上第2行那个textBox2只是为了让鼠标的焦点有个去处):
  1. <Grid>
  2. <TextBox Height="23" Margin="10,10,9,0" Name="textBox1" VerticalAlignment="Top" />
  3. <TextBox Height="23" Margin="10,41,9,0" Name="textBox2" VerticalAlignment="Top" />
  4. <Slider Height="21" Margin="10,73,9,0" Name="slider1" VerticalAlignment="Top" Maximum="100" />
  5. </Grid>
 
 
然后,我只需在第1行代码上做一个小小的修改,就能完成WinForm中需要用两个事件响应、十多行代码才能完成的事情:
  1. <Grid>
  2. <TextBox Height="23" Margin="10,10,9,0" Name="textBox1" VerticalAlignment="Top" Text="{Binding ElementName=slider1, Path=Value}"/>
  3. <TextBox Height="23" Margin="10,41,9,0" Name="textBox2" VerticalAlignment="Top" />
  4. <Slider Height="21" Margin="10,73,9,0" Name="slider1" VerticalAlignment="Top" Maximum="100" />
  5. </Grid>
 
细心的你,一定一眼就看到只多了这样一句话:Text="{Binding ElementName=slider1, Path=Value}"
这句话的意思是说:Hi,textBox1,从此以后,你的Text属性值就与slider1这个UI元素的Value属性值关联上了,Value变的时候你的Text也要跟着变。
这时候的效果是——你拖动Slider的滑块,textBox1就会显示值(双精度,0到100之间);你在textBox1里输入一个0到100之间的数字,当把鼠标移动到textBox2里时,slider1的滑块会跳到相应的值上去,如图:
 
非常简单是不是?请注意,这里面可蕴含了“数据驱动界面”的模型哦!在这里,我们始终把slider1的Value当成是数据源(Data Source),而textBox1则是用来显示和修改数据的窗口(Data Presenter)——slider1是核心,它的Value属性值将驱动textBox1的Text进行改变;人为改变textBox1的Text属性值,也会被送回到slider1的Value属性值上去。
是时候让我们了解Data Binding的几个关键概念了——
  1. 数据源(Data Source,简称Source):顾名思义,它是保有数据的实体、是数据的来源、源头。把谁当作数据源完全由程序员来决定——只要你想把它当做数据核心来使用。它可以是一个UI元素、某个类的实例,也可以是一个集合(关于对集合的绑定,非常重要,专门用一篇文章来讨论之)。
  2. 路径(Path):数据源作为一个实体可能保有着很多数据,你具体关注它的哪个数值呢?这个数值就是Path。就上面的例子而言,slider1是Source,它拥有很多数据——除了Value之外,还有Width、Height等,但都不是我们所关心的——所以,我们把Path设为Value。
  3. 目标(Target):数据将传送到哪里去?这就是数据的目标了。上面这个例子中,textBox1是数据的Target。有一点需要格外注意:Target一定是数据的接收者、被驱动者,但它不一定是数据的显示者——也许它只是数据联动中的一环——后面我们给出了例子。
  4. 关联(Binding):数据源与目标之间的通道。正是这个通道,使Source与Target之间关联了起来、使数据能够(直接或间接地)驱动界面!
  5. 设定关联(Set Binding):为Target指定Binding,并将Binding指向Target的一个属性,完成数据的“端对端”传输。
 绿色通道上的“关卡”:
 话说眼看就要到奥运会了,北京的各大交通要道上也都加强了安检力度。微软同学也给Binding这条“绿色通道”准备了几道很实用的“关卡”。这些“关卡”的启闭与设置是通过Binding的属性来完成的。其中常用的有:
  • 如果你想把“绿色通道”限制为“单行道”,那就设置Binding实例的Mode属性,它是一个BindingMode类型的枚举值,其中包含了TwoWay、OneWay和OneWayToSource几个值。上面这个例子中,默认地是TwoWay,所以才会有双向的数据传递。
  • 如果用户提出只要textBox1的文本改变slider1的滑块立刻响应,那就设置Binding的UpdateSourceTrigger属性。它是一个UpdateSourceTrigger类型枚举值,默认值是UpdateSourceTrigger.LostFocus,所以才会在移走鼠标焦点的时候更新数据。如果把它设置为UpdateSourceTrigger.PropertyChanged,那么Target被关联的属性只要一改变,就立刻传回给Source——我们要做的仅仅是改了一个单词,而WinForm程序员这时候正头疼呢,因为他需要去把代码搬到另一个事件响应函数中去。
  • 如果Binding两端的数据类型不一致怎么办?没关系,你可以设置Binding的Converter属性,具体内容在下篇文章中讨论。
  • 如果数据中有“易燃易爆”的不安全因素怎么办?OK,可以设置Binding的ValidationRules,为它加上一组“安检设施”(同样也在下篇文中讨论)。
在C#代码中设置Binding
XAML代码是如此简单,简单就那么一句话。这可不是吾等C#程序员、刨根问底之徒可以善罢甘休的!
形象地讲,Binding就像一个盒子,盒子里装了一些机关用于过滤和控制数据,盒子两端各接着一根管子,管子是由管壳和管芯构成的,看上去就像下面的图:
 
当脑子里有了这样一个形象之后,遵循下面的步骤就OK了:
  1. Source:确定哪个对象作为数据源
  2. Target:确定哪个对象作为目标
  3. Binding:声明一个Binding实例
  4. 把一根管子接到Source上并把管芯插在Source的Path上
  5. 把另一根管子接到Target上并把管芯插在Target的联动属性上
如果有必要,可以在3与4之间设置Binding的“关卡”们。其实,第3步之后的顺序不是固定的,只是这个步骤比较好记——一概向右连接。所得结果看上去是这样:
 
我猜你可能会问:“那个D.P.是什么呀?”
D.P.的全称是“Dependency Property”,直译过来就是“依赖式属性”,意思是说它自己本身是没有值的,它的值是“依赖”在其它对象的属性值上、通过Binding的传递和转换而得来的。表现在例子里,它就是Target上的被数据所驱动的联动属性了!
这里是等价的C#代码,我把它写在了Window1的构造函数里:
  1. public Window1()
  2. {
  3. InitializeComponent();
  4. // 1. 我打算用slider1作为Source
  5. // 2. 我打算用textBox1作为Target
  6. Binding binding = new Binding();
  7. binding.Source = this.slider1;
  8. binding.Path = new PropertyPath("Value");
  9. this.textBox1.SetBinding(TextBox.TextProperty, binding);
  10. }
 
有意思的是,Source端的操作,接管子和插管芯分两步,而Target端却是在SetBinding方法中一步完成。注意啦,TextBox.TextProperty就是一个Dependency Property的庐山真面目!关于Dependency Property的文档业已完成,我将择一黄道吉日挂将出来:p
上面的代码稍有简化的余地,那就是把Path的设定转移到Binding的构造中去:
  1. public Window1()
  2. {
  3. InitializeComponent();
  4. // 1. 我打算用slider1作为Source
  5. // 2. 我打算用textBox1作为Target
  6. Binding binding = new Binding("Value");
  7. binding.Source = this.slider1;
  8. this.textBox1.SetBinding(TextBox.TextProperty, binding);
  9. }
 
这样做的好处是——随便你给binding指定一个Source,只要这个Source有“Value”这个属性,binding就会自动提取它的值并传输给Target端。
我们还可以为binding设些“关卡”:
  1. public Window1()
  2. {
  3. InitializeComponent();
  4. // 1. 我打算用slider1作为Source
  5. // 2. 我打算用textBox1作为Target
  6. Binding binding = new Binding("Value");
  7. binding.Source = this.slider1;
  8. binding.Mode = BindingMode.TwoWay;
  9. binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
  10. this.textBox1.SetBinding(TextBox.TextProperty, binding);
  11. }
 
 还有一个小小的提示:如果Source碰巧是一个UI元素,那么也可将binding.Source = this.slider1;改写成binding.ElementName = "slider1";或者binding.ElementName = this.slider1.Name;
自定义数据源:
在我们项目组日常的工作中,经常需要自己写一个类,并且拿它的实例当作数据源。怎样才能让一个类成为“合格的”数据源呢?
要诀就是:
  1. 为这个类定义一些Property,相当于为Binding提供Path
  2. 让这个类实现INotifyPropertyChanged接口。实现这个接口的目的是当Source的属性值改变后通知Binding(不然人家怎么知道源头的数据变了并进行联动协同呢?),好让Binding把数据传输给Target——本质上还是使用事件机制来做,只是掩盖在底层、不用程序员去写event handler了。
 让我们写一个这样的类:
  1. <PRE class=csharp name="code">// 第一步:声明一个类,准备必要的属性
  2. public class Student
  3. {
  4. private int id;
  5. public int Id
  6. {
  7. get { return id; }
  8. set { id = value; }
  9. }
  10. private string name;
  11. public string Name
  12. {
  13. get { return name; }
  14. set { name = value; }
  15. }
  16. private int age;
  17. public int Age
  18. {
  19. get { return age; }
  20. set { age = value; }
  21. }
  22. }</PRE>
 
 接下来就是使用INotifyPropertyChanged接口“武装”这个类了,注意,这个接口在System.ComponentModel名称空间中:
  1. // 第二步:实现INotifyPropertyChanged接口
  2. public class Student : INotifyPropertyChanged
  3. {
  4. public event PropertyChangedEventHandler PropertyChanged; // 这个接口仅包含一个事件而已
  5. private int id;
  6. public int Id
  7. {
  8. get { return id; }
  9. set
  10. {
  11. id = value;
  12. if (this.PropertyChanged != null)
  13. {
  14. this.PropertyChanged.Invoke(thisnew PropertyChangedEventArgs("Id")); // 通知Binding是“Id”这个属性的值改变了
  15. }
  16. }
  17. }
  18. private string name;
  19. public string Name
  20. {
  21. get { return name; }
  22. set
  23. {
  24. name = value;
  25. if (this.PropertyChanged != null)
  26. {
  27. this.PropertyChanged.Invoke(thisnew PropertyChangedEventArgs("Name")); // 通知Binding是“Name”这个属性的值改变了
  28. }
  29. }
  30. }
  31. private int age;
  32. public int Age
  33. {
  34. get { return age; }
  35. set { age = value; } // Age的值改变时不进行通知
  36. }
 
  1. OK,此时,你可以尝试使用Student类的实例作为数据源了!
 
自定义数据目标:
往而不来,非礼也;来而不往,亦非礼也——《礼记·曲礼》
知道了如何定义数据源,一定想一鼓作气再定义一个数据目标吧?让我们回想一下:Binding接在Target一端的管子,它的管芯是插在一个Dependency Property上的!所以,在我们熟悉Dependency Property之前,恐怕只能使用现成的.NET对象来充当Target了!
所以,敬请关注《深入浅出WPF》系统的后续文章!

最新文章

  1. iOS 键盘的隐藏
  2. c++ 读写注册表
  3. 加密工具类 - CryptoUtils.java
  4. JS去除数组中重复值的四种方法
  5. Sigma.js
  6. 用VB操作Excel的方法
  7. 使用SQLyog远程访问mysql数据库设置
  8. man手册使用
  9. 简单学C——第六天
  10. Huffman树及其应用
  11. ExtractNewFolderPath
  12. thinkphp ,进行关联模型的时候出现的问题,版本是3.2
  13. P177 test 6-4 UVa439
  14. (MariaDB/MySQL)MyISAM存储引擎读、写操作的优先级
  15. 2。创建第一个angular应用,已经开发前的一些配置
  16. JS学习笔记9_JSON
  17. Semaphore的使用
  18. FTP下载工具
  19. 【转】jmeter 如何将上一个请求的结果作为下一个请求的参数——使用正则提取器
  20. ubuntu 更改python3为默认版本

热门文章

  1. 简单实用的matlab柱状图显示比例及计数
  2. Dart语言学习(十五) Dart函数方法
  3. 1146. Snapshot Array
  4. HTML5中改变了哪些东西?
  5. shell coding about mac ox
  6. 攻防世界Web进阶-Upload1
  7. PTA的Python练习题(五)
  8. nginx 加工上游服务器返回的内容,并返回给客户端
  9. 吴裕雄--天生自然PythonDjangoWeb企业开发:Django文档阅读简介
  10. 关于c++的头文件和变量声明