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

本文转自:http://liutiemeng.blog.51cto.com/120361/95273

小序:
怎么直接从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>  
<Window x:Class="BindingSample.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="http://blog.csdn.net/FantasiaX" Height="300" Width="300">
    <Window.Background>
        <LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
            <GradientStop Color="Blue"  Offset="0.3"/>
            <GradientStop Color="LightBlue" Offset="1"/>
        </LinearGradientBrush>
    </Window.Background>
    <Grid>
        <TextBox Height="23" Margin="10,10,9,0" Name="textBox1" VerticalAlignment="Top" />
        <TextBox Height="23" Margin="10,41,9,0" Name="textBox2" VerticalAlignment="Top" />
        <Slider Height="21" Margin="10,73,9,0" Name="slider1" VerticalAlignment="Top" Maximum="100" />
    </Grid>
</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>  
    <Grid>
        <TextBox Height="23" Margin="10,10,9,0" Name="textBox1" VerticalAlignment="Top" />
        <TextBox Height="23" Margin="10,41,9,0" Name="textBox2" VerticalAlignment="Top" />
        <Slider Height="21" Margin="10,73,9,0" Name="slider1" VerticalAlignment="Top" Maximum="100" />
    </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>  
    <Grid>
        <TextBox Height="23" Margin="10,10,9,0" Name="textBox1" VerticalAlignment="Top" Text="{Binding ElementName=slider1, Path=Value}"/>
        <TextBox Height="23" Margin="10,41,9,0" Name="textBox2" VerticalAlignment="Top" />
        <Slider Height="21" Margin="10,73,9,0" Name="slider1" VerticalAlignment="Top" Maximum="100" />
    </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.   
  5.     // 1. 我打算用slider1作为Source   
  6.     // 2. 我打算用textBox1作为Target   
  7.     Binding binding = new Binding();   
  8.     binding.Source = this.slider1;   
  9.     binding.Path = new PropertyPath("Value");   
  10.     this.textBox1.SetBinding(TextBox.TextProperty, binding);               
  11. }  
public Window1()
{
	InitializeComponent();
	// 1. 我打算用slider1作为Source
	// 2. 我打算用textBox1作为Target
	Binding binding = new Binding();
	binding.Source = this.slider1;
	binding.Path = new PropertyPath("Value");
	this.textBox1.SetBinding(TextBox.TextProperty, binding);			
}
有意思的是,Source端的操作,接管子和插管芯分两步,而Target端却是在SetBinding方法中一步完成。注意啦,TextBox.TextProperty就是一个Dependency Property的庐山真面目!关于Dependency Property的文档业已完成,我将择一黄道吉日挂将出来:p
上面的代码稍有简化的余地,那就是把Path的设定转移到Binding的构造中去:
  1. public Window1()   
  2. {   
  3.     InitializeComponent();   
  4.        
  5.     // 1. 我打算用slider1作为Source   
  6.     // 2. 我打算用textBox1作为Target   
  7.     Binding binding = new Binding("Value");   
  8.     binding.Source = this.slider1;   
  9.     this.textBox1.SetBinding(TextBox.TextProperty, binding);               
  10. }  
public Window1()
{
	InitializeComponent();
	
	// 1. 我打算用slider1作为Source
	// 2. 我打算用textBox1作为Target
	Binding binding = new Binding("Value");
	binding.Source = this.slider1;
	this.textBox1.SetBinding(TextBox.TextProperty, binding);			
}
这样做的好处是——随便你给binding指定一个Source,只要这个Source有“Value”这个属性,binding就会自动提取它的值并传输给Target端。
我们还可以为binding设些“关卡”:
  1. public Window1()   
  2. {   
  3.     InitializeComponent();   
  4.   
  5.   
  6.     // 1. 我打算用slider1作为Source   
  7.     // 2. 我打算用textBox1作为Target   
  8.     Binding binding = new Binding("Value");   
  9.     binding.Source = this.slider1;   
  10.     binding.Mode = BindingMode.TwoWay;   
  11.     binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;   
  12.     this.textBox1.SetBinding(TextBox.TextProperty, binding);               
  13. }  
public Window1()
{
	InitializeComponent();
	// 1. 我打算用slider1作为Source
	// 2. 我打算用textBox1作为Target
	Binding binding = new Binding("Value");
	binding.Source = this.slider1;
	binding.Mode = BindingMode.TwoWay;
	binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
	this.textBox1.SetBinding(TextBox.TextProperty, binding);			
}
 还有一个小小的提示:如果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.   
  3. public class Student    
  4. {   
  5.     private int id;   
  6.     public int Id   
  7.     {   
  8.         get { return id; }   
  9.         set { id = value; }   
  10.     }   
  11.   
  12.     private string name;   
  13.     public string Name   
  14.     {   
  15.         get { return name; }   
  16.         set { name = value; }   
  17.     }   
  18.   
  19.     private int age;   
  20.     public int Age   
  21.     {   
  22.         get { return age; }   
  23.         set { age = value; }   
  24.     }   
  25.   
  26. }</PRE>  
  1. // 第一步:声明一个类,准备必要的属性   
  2.   
  3. public class Student    
  4. {   
  5.     private int id;   
  6.     public int Id   
  7.     {   
  8.         get { return id; }   
  9.         set { id = value; }   
  10.     }   
  11.   
  12.     private string name;   
  13.     public string Name   
  14.     {   
  15.         get { return name; }   
  16.         set { name = value; }   
  17.     }   
  18.   
  19.     private int age;   
  20.     public int Age   
  21.     {   
  22.         get { return age; }   
  23.         set { age = value; }   
  24.     }   
  25.   
  26. }  
// 第一步:声明一个类,准备必要的属性
public class Student 
{
	private int id;
	public int Id
	{
		get { return id; }
		set { id = value; }
	}
	private string name;
	public string Name
	{
		get { return name; }
		set { name = value; }
	}
	private int age;
	public int Age
	{
		get { return age; }
		set { age = value; }
	}
}
 接下来就是使用INotifyPropertyChanged接口“武装”这个类了,注意,这个接口在System.ComponentModel名称空间中:
  1. // 第二步:实现INotifyPropertyChanged接口   
  2.   
  3. public class Student : INotifyPropertyChanged   
  4. {   
  5.     public event PropertyChangedEventHandler PropertyChanged; // 这个接口仅包含一个事件而已   
  6.   
  7.     private int id;   
  8.     public int Id   
  9.     {   
  10.         get { return id; }   
  11.         set  
  12.         {   
  13.             id = value;   
  14.             if (this.PropertyChanged != null)   
  15.             {   
  16.   
  17.                 this.PropertyChanged.Invoke(thisnew PropertyChangedEventArgs("Id")); // 通知Binding是“Id”这个属性的值改变了   
  18.             }   
  19.         }   
  20.     }   
  21.   
  22.   
  23.     private string name;   
  24.     public string Name   
  25.     {   
  26.         get { return name; }   
  27.         set  
  28.         {   
  29.             name = value;   
  30.             if (this.PropertyChanged != null)   
  31.             {   
  32.                 this.PropertyChanged.Invoke(thisnew PropertyChangedEventArgs("Name")); // 通知Binding是“Name”这个属性的值改变了   
  33.             }   
  34.         }   
  35.     }   
  36.   
  37.     private int age;   
  38.     public int Age   
  39.     {   
  40.         get { return age; }   
  41.         set { age = value; } // Age的值改变时不进行通知   
  42.     }  
// 第二步:实现INotifyPropertyChanged接口
public class Student : INotifyPropertyChanged
{
	public event PropertyChangedEventHandler PropertyChanged; // 这个接口仅包含一个事件而已
	private int id;
	public int Id
	{
		get { return id; }
		set
		{
			id = value;
			if (this.PropertyChanged != null)
			{
				this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Id")); // 通知Binding是“Id”这个属性的值改变了
			}
		}
	}
	private string name;
	public string Name
	{
		get { return name; }
		set
		{
			name = value;
			if (this.PropertyChanged != null)
			{
				this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Name")); // 通知Binding是“Name”这个属性的值改变了
			}
		}
	}
	private int age;
	public int Age
	{
		get { return age; }
		set { age = value; } // Age的值改变时不进行通知
	}
  1. OK,此时,你可以尝试使用Student类的实例作为数据源了!  
OK,此时,你可以尝试使用Student类的实例作为数据源了!
自定义数据目标:
往而不来,非礼也;来而不往,亦非礼也——《礼记·曲礼》
知道了如何定义数据源,一定想一鼓作气再定义一个数据目标吧?让我们回想一下:Binding接在Target一端的管子,它的管芯是插在一个Dependency Property上的!所以,在我们熟悉Dependency Property之前,恐怕只能使用现成的.NET对象来充当Target了!
所以,敬请关注《深入浅出WPF》系统的后续文章!
 

 http://liutiemeng.blog.51cto.com/120361/95275

小序:
今天中午吃完饭回工位的路上,和俺们组资深的Level 2技术支持肖老师聊了几句。我跟肖老师说,最近我在学习Binding,肖老师说——那可不是个好东西!因为如果在程序中使用了Binding,当出现错误的时候,比较难于调试。道理很简单——以前使用事件(C++里是回调)的时候,能明确地在事件处理函数里去跟踪调试,现在使用Binding,数据源和UI之间是一支封闭的“管道”,在代码中很难看到他们是在哪里关联上的、出了问题也不知道在哪里拦截、设断点。
这的确是个问题!我又去请教了别的同事。Allen同学告诉我,在Binding和数据目标(的Dependency Property)有一些事件,会在数据有传输的时候被激发,或者在Binding的Converter和Validator上下下功夫。看来Binding值得研究的地方还真多呀!
文章的篇幅毕竟有限,我只能捡工作当中用的最多的来介绍。那么,我还有哪些东西需要介绍呢?
  • 让数据“为我所用”的Converter 
  • 让数据“干干净净”的Validation 
  • 集合控件与集合数据的Binding 
  • 偷懒专用的数据中转站——DataContext
希望我的工作能给大家搭起一个良好的学习框架——全面细致的内容尽在MSDN里,请大家阅读的时候会轻松一些。
正文 
不拘一格用数据的Converter
上篇文已经说明,Binding就是数据源与目标之间的“关联”。大多数情况下,数据从Source到Target以及从Target返回Source都是“直来直去”的,但有些场景却需要我们对数据做些转换才能为我所用。举两个典型的例子:
  • 如果数据源里的值是Y和N,如果是Y,那么UI上的CheckBox就被勾选,否则就不勾选,这就需要我们把string(也许是char)类型的数据转换成bool?类型再使用。如果Binding是TwoWay的,CheckBox的勾选操作还会把值传回数据源。 
  • 如果“评论内容”TextBox里没有内容,则“提交”Button不可以点击。这是个典型的OneWay数据Binding,因为只有TextBox去影响Button的份儿。具体如何实现,大家可以先猜猜;)
想要实现这类的转换,就需要为Binding这个“绿色通道”设置“关卡”,这里我们用到的关卡就是“数据转换器”(Data Converter)。Converter实际上就是一个类,它这个类有个要求——它需要实现IValueConverter这个接口。这个接口的内容非常简单——只有两个方法,它们分别是:
  • Convert方法:按照你的要求,把从数据源传来的数据转成你想要的数据——至于是加减乘除还是煎炒炸炖,那就要看你怎么实现函数体了 
  • ConvertBack方法:如果Binding是TwoWay的,那么数据目标会回传经用户改动后的数据,这时候你就不得不把数据转换回数据源里的格式——大多数情况下,它是Convert方法的逆运算,具体情况还要具体分析。(不过,熟饭估计怎么着也变不成生米了,呵呵~~)
 下面是第一个例子的核心代码,我来一步一步实现。
第一步:先声明一个类。我的习惯是用Converter开头,后缀是“源类型2目标类型”,这里的“2”是“to”的意思。
 
  1. class ConverterYN2TF   
  2. {   
  3.   
  4. }  
class ConverterYN2TF
{
}
第二步:让这个类实现IValueConverter接口。这里有个使用VS2008的小窍门——在类名后写上“: IValueConverter”后,按下键盘上的“Shift+Alt+F10”会弹出VS2008的智能菜单,选择其中的第一项“实现IValueConverter的方法”,VS2008会自动为我们生成需要实现的方法体:
  1. class ConverterYN2TF : IValueConverter   
  2. {  
  3.     #region IValueConverter Members   
  4.   
  5.     public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)   
  6.     {   
  7.         throw new NotImplementedException();   
  8.     }   
  9.   
  10.     public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)   
  11.     {   
  12.         throw new NotImplementedException();   
  13.     }  
  14.     #endregion   
  15. }  
class ConverterYN2TF : IValueConverter
{
	#region IValueConverter Members
	public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
	{
		throw new NotImplementedException();
	}
	public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
	{
		throw new NotImplementedException();
	}
	#endregion
}
第三步:添加Attribute。这步不是必需的,但加上有是有好处的——告诉Converter数据的源类型与目标类型各是什么。值得注意的是,CheckBox的IsChecked属性是bool?类型的(可空bool类型),意思是说可以是True/False/Null三种值,表现在UI上就是勾选/不勾选/中间态。如果想让CheckBox能显示中间态,需要把它的IsThreeState属性设为True。
  1. [ValueConversion(typeof(string), typeof(bool?))] //数据的源类型是string,目标类型是bool?   
  2. class ConverterYN2TF : IValueConverter   
  3. {  
  4.     #region IValueConverter Members   
  5.   
  6.     public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)   
  7.     {   
  8.         throw new NotImplementedException();   
  9.     }   
  10.   
  11.     public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)   
  12.     {   
  13.         throw new NotImplementedException();   
  14.     }  
  15.     #endregion   
  16. }  
[ValueConversion(typeof(string), typeof(bool?))] //数据的源类型是string,目标类型是bool?
class ConverterYN2TF : IValueConverter
{
	#region IValueConverter Members
	public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
	{
		throw new NotImplementedException();
	}
	public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
	{
		throw new NotImplementedException();
	}
	#endregion
}
第四步:实现这两个方法。在开始动手前,我们先分析一下这两个方法的参数和返回值。
首先,这两个方法的参数和返回值都是object类型的,之所以这样做,是因为接口的设计者并不知道你要传入和传出的数据是什么类型的,只好用它们“绝对正确”的基类——object了。
其次,对于Convert方法来说, value是从数据源传来的数据,返回值是转换好后发送给数据目标的数据。对于ConvertBack方法正好反过来,value是从数据目标(比如UI)传回来的数据,返回值是要与数据源匹配的数据。
再次,偶尔我们会用到parameter那个参数。比如在转换某些数据的时候,我们需要依赖一些其它的外部数据来辅助我们的数据转换,这时候就可以在parameter上打主意了。如果想传多个参数的话,可以把这些参数打包成数组或者class/struct等数据结构再传进来。在我们工作的代码中用到过一次parameter,我为我的Converter类准备了一个带参数的构造函数,把外部的辅助数据传给Converter
最后,如果你的Binding是OneWay的,那么恭喜你——你的ConvertBack函数体随便怎么实现都可以——因为它不可能被调用。
完成的类是这样的:
  1. [ValueConversion(typeof(string), typeof(bool?))] //数据的源类型是string,目标类型是bool?   
  2. class ConverterYN2TF : IValueConverter   
  3. {   
  4.     public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)   
  5.     {   
  6.         string str = System.Convert.ToString(value);   
  7.         switch (str)   
  8.         {   
  9.             case "Y":   
  10.                 return true;   
  11.             case "N":   
  12.                 return false;   
  13.             default:   
  14.                 return null;   
  15.         }   
  16.     }   
  17.   
  18.     public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)   
  19.     {   
  20.         bool? b = System.Convert.ToBoolean(value);   
  21.         switch (b)   
  22.         {   
  23.             case true:   
  24.                 return "Y";   
  25.             case false:   
  26.                 return "N";   
  27.             default:   
  28.                 return "Null";   
  29.         }   
  30.     }   
  31. }  
[ValueConversion(typeof(string), typeof(bool?))] //数据的源类型是string,目标类型是bool?
class ConverterYN2TF : IValueConverter
{
	public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
	{
		string str = System.Convert.ToString(value);
		switch (str)
		{
			case "Y":
				return true;
			case "N":
				return false;
			default:
				return null;
		}
	}
	public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
	{
		bool? b = System.Convert.ToBoolean(value);
		switch (b)
		{
			case true:
				return "Y";
			case false:
				return "N";
			default:
				return "Null";
		}
	}
}
使用这个类的方法是将Binding实例的Converter属性设置为这个类的一个实例:
  1. checkBox1.IsThreeState = true;   
  2. Binding binding = new Binding("Text");   
  3. binding.Source = textBox1;   
  4. binding.Converter = new ConverterYN2TF(); // 设定Converter   
  5. this.checkBox1.SetBinding(CheckBox.IsCheckedProperty, binding);  
checkBox1.IsThreeState = true;
Binding binding = new Binding("Text");
binding.Source = textBox1;
binding.Converter = new ConverterYN2TF(); // 设定Converter
this.checkBox1.SetBinding(CheckBox.IsCheckedProperty, binding);
 
至于上面的第二个例子,留给大家自己动手去实现吧。想一想:怎样才能让Button的IsEnable属性与TextBox中文本的有无关联上呢? 
 让数据“干干净净”的Validation
再让我们来看看如何对数据进行“安检”。 
首先,这里有一个“霸王条款”——Binding认为从数据源出去的数据都是“干净”的,所以不进行校验;只有从数据目标回传的数据才有可能是“脏”的,需要校验。
其次,对于一个Binding而言,Converter只能有一个,而校验条件可以是好几个——它们存储在Binding的ValidationRules这个集合里。其实,数据校验与转换做的事儿差不多。
下面给出一个例子:我们以一个Slider为数据源,它的滑块可以从Value=0滑到Value=100;同时,我们以一个TextBox为数据目标,并通过Validation限制它只能将20到35之间的数据传回数据源。现实当中恐怕很少有这么干的,我们这个例子只是为了说明校验的使用方法:)
若要创建一个自定义的校验条件,需要声明一个类,并让这个类派生自ValidationRule类。ValidationRule只有一个名为Validate的方法需要我们实现,这个方法的返回值是一个ValidationResult类型的实例——这个实例携带着两个信息:
  • bool类型的IsValid属性告诉Binding回传的数据是否合法
  • object类型(一般是存储一个string)的ErrorContent属性告诉Binding一些信息,比如当前是进行什么操作而出现的校验错误等等,一般我会把这些信息写进Log文件里
实现好的类是这样的:
  1. public class MyValidationRule : ValidationRule   
  2. {   
  3.     public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)   
  4.     {   
  5.         double d = 0.0;   
  6.         if (double.TryParse((string)value, out d) && d >= 20 && d <= 35)   
  7.         {   
  8.             return new ValidationResult(true"OK");   
  9.         }   
  10.         else  
  11.         {   
  12.             return new ValidationResult(false"Error");   
  13.         }   
  14.     }   
  15. }  
public class MyValidationRule : ValidationRule
{
	public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
	{
		double d = 0.0;
		if (double.TryParse((string)value, out d) && d >= 20 && d <= 35)
		{
			return new ValidationResult(true, "OK");
		}
		else
		{
			return new ValidationResult(false, "Error");
		}
	}
}
在代码里这样使用它:
  1. Binding binding = new Binding("Value");   
  2. binding.Source = slider1;   
  3. binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;   
  4. binding.ValidationRules.Add(new MyValidationRule()); // 加载校验条件   
  5. textBox1.SetBinding(TextBox.TextProperty, binding);  
Binding binding = new Binding("Value");
binding.Source = slider1;
binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
binding.ValidationRules.Add(new MyValidationRule()); // 加载校验条件
textBox1.SetBinding(TextBox.TextProperty, binding);
程序执行起来之后的效果是这样的:
我可以使用Slider滑出从0到100的值来,也可以使用TextBox输入20到35之间的值;但当我输入小于20或者大于35的数字以及非数字时,值就不会被传回到Slider(数据源),同时,TextBox还会被一个红色的边框圈起来以示警告。至于警告的风格,我们可以自定义,具体怎么定义我会放在Style一节去叨叨。
 
TO BE CONTINUE...
下篇文中我们将介绍非常实用的集合Binding与懒人的最爱——DataContext
 
小知识:什么是L2
CA有些同事(而且是C++比较牛的同事)是做Level 2的,也就是“二级技术支持”,Level 1是接客户电话、对客户进行支持的——他们最重要的工作就是当用户发现bug时用开心的口吻告诉他们:“Hi,that a feature:)”,呵呵,开个玩笑,实际上他们经常面对怒气冲冲、快要发疯的客户。如果真的是个bug,那L1的同学们也搞不定,这时候就要把bug提交给L2的同学们了,L2的同学们会连夜改代码、解bug、出SP包……算是我们的“救火队员”了,公司的良好信誉离不开他们的努力。
 
小序:
看着自己上一篇技术文章,指算来,已经月余没有动笔了——实在是不像话。最近一来是忙工作,二来是兴趣点放在了设计模式上,而且尝试着把设计模式也“深入浅出”了一把,当然啦,因为对于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 { get; set; }   
  4.     public string Name { get; set; }   
  5.     public int Age { get; set; }   
  6. }  
class Student
{
    public int StuNum { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}
然后我们在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 { 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>……实际上太多东西需要在实际工作中去摸索和掌握了,一两篇文章只是杯水车薪——我也不想像琼瑶姐姐那样上、中、下、继、再继、再再继……
原文地址:https://www.cnblogs.com/freeliver54/p/4397483.html