标签:des winform style blog http color
1,Data Binding在WPF中的地位
程序的本质是数据+算法。数据会在存储、逻辑和界面三层之间流通,所以站在数据的角度上来看,这三层都非常重要。但算法在3层中的分布是不均匀的,对于一个3层结构的程序来说,算法一般分布在这几处:
A。数据库内部。
B。读取和写回数据。
C。业务逻辑。
D。数据展示。
E。界面与逻辑的交互。
A,B两部分的算法一般都非常稳定,不会轻易去修改,复用性也非常高;C处与客户需求最紧密,最复杂,变化最大,大多少算法都集中在这里。D,E负责UI和逻辑的交互,也占有一定量的算法。
显然,C部分是程序的核心,是开发的重中之重,所以我们应该把精力集中在C部分。然而,D,E两部分却常常成为麻烦的来源。首先这两部分都与逻辑紧密相关,一不小心就有可能把本来该放在逻辑层里面的算法写进这两部分(所以才有了MVC、MVP等模式来避免这样的情况出现)。其次,这两部分以消息或者事件的方式与逻辑层沟通,一旦出现同一个数据须要在多出展示/改动时,用于同步的代码错综复杂;最后,D和E本来是互逆的一对儿。但却须要分开来写-----显示数据写一个算法,改动数据再写一个算法。总之导致的结果就是D和E两部分会占去一部分算法,搞不好还会牵扯不少精力。
问题的根源在于逻辑层和展示层的地位不固定------当实现客户需求的时候,逻辑层的确处于核心地位。但到了实现UI的时候,展示层又处于核心的地位。WPF作为一种专业的展示层技术,华丽的外观和动画仅仅是它的表层现象,最重要的是他在深层次上把程序猿的思维固定在了逻辑层,让展示层永远处于逻辑层的从属地位。WPF具有这样的能力的关键在于它引入了Data Binding概念及与之配套的Dependency Property系统和DataTemplate。
从传统的Winform转移到WPF上,对于一个三层程序而言,数据存储层由数据库和文件系统组成,传输数据和处理仍然使用.NetFramework的ADO.NET等基本类(与Winform开发一样)。展示层则使用WPF类库来实现,而展示层和逻辑层的沟通就使用Data Binding来实现。可见,Data Binding在WPF中所起的作用就是快速公路的作用。有了这条快速公路,加工好的数据自己主动送达用户界面并加以显示,被用户改动过的数据也会自己主动传回业务逻辑层,一旦数据被加工好又会被送往界面。。。。程序的逻辑层就像是一个强有力的引擎一直在运作,用加工好的数据驱动用户界面也文字、图形、动画等形式把数据显示出来------这就是数据驱动UI。
引入Data Binding之后,D,E两部分被简化了非常多。首先,数据在逻辑层和用户界面直来之去、不涉及逻辑问题,这种用户界面部分基本上不包括算法:Data Binding本身就是双向通信,所以相当于把D和E合二为一;对于多个UI元素关注同一个数据的情况,仅仅须要用Data Binding将这些UI元素和数据一一关联上(以数据为中心的星形结构),当数据变化后,这些UI元素会同步显示这一变化。前面提到的问题也都迎刃而解了。更重要的是经过这种优化,全部与业务逻辑相关的算法都处在业务逻辑层,逻辑层成了一个能够独立运转,完整的体系,而用户界面则不须要不论什么逻辑代码。全然依赖和从属于业务逻辑层。这样做有两个显而易见的优点,第一:假设把UI看做是应用程序的皮,把存储层和逻辑层看作是程序的瓤,我们能够非常轻易的把皮撕下来换一个新的。第二:由于数据层能够独立运作,自成体系,所以我们能够进行更完好的单元測试而无需借助UI自己主动化測试工具----你全然能够把单元測试看作是一个“看不见的UI”,单元測试仅仅是使用这个UI绕过真实的UI直接測试业务逻辑罢了。
2 , Binding 基础
假设把Binding比作数据的桥梁,那么它的两端各自是源(Source)和目标(Target)。数据丛哪里来哪里就是源,到哪里去哪里就是目标。普通情况下,Binding的源是业务逻辑层的对象,Binding的目标是UI层的控件对象。这样数据就会源源不断的通过Binding送达UI界面,被UI层展现,这就完毕了数据驱动UI的过程。有了这座桥梁,我们不仅能够控制车辆在源与目标之间是双向通行还是单向通行。还能够控制数据的放行时机,甚至能够在桥上搭建一些关卡用来转换数据类型或者检验数据的正确性。
通过对Binding有了一个基本概念之后,让我们看一个最主要的样例。这个样例是创建一个简单的数据源并通过Binding把它连接到UI元素上。
首先,我们创建一个名为"Student"的类,这个类的实例将作为数据源来使用。
public class Student { private string name; public string Name { get { return name; } set { name = value; } }这个类非常easy,简单到仅仅有一个string类型的Name属性。前面说过数据源是一个对象,一个对象本身可能会有非常多数据,这些数据又通过属性暴露给外界。那么当中哪个元素是你想通过Binding送达UI元素的呢,换句话说,UI元素关心的是哪个属性值的变化呢?这个属性值称之为Binding的路径(Path)。但光有属性还不行-------Binding是一种自己主动机制,当值变化后属性要有能力通知Binding,让Binding把变化传递给UI元素。如何才干让一个属性具备这样的通知Binding值已经改变的能力呢?方法是在属性的Set语句中激发一个PropertyChanged事件。这个事件不须要我们自己声明,我们要做的事是让作为数据源的类实现System.ComponentModel名称空间中的INotifyPropertyChanged接口。当为Binding设置了数据源之后,Binding就会自己主动侦听来自这个接口PropertyChanged事件。
实现INotifyPropertyChanged接口的类看起来是这样:
public class Student : INotifyPropertyChanged { private string name; public string Name { get { return name; } set { name = value; if (PropertyChanged != null) { this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Name")); } } } public event PropertyChangedEventHandler PropertyChanged; }
然后我们在窗口上准备一个TextBox和Button,代码例如以下:
<Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> <TextBox Height="23" HorizontalAlignment="Left" Margin="185,43,0,0" Name="textBox1" VerticalAlignment="Top" Width="120" /> <Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="209,96,0,0" Name="button1" VerticalAlignment="Top" Width="75" Click="button1_Click" /> </Grid> </Window>
/// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { Student stu = null; public MainWindow() { InitializeComponent(); stu = new Student(); Binding bind = new Binding(); bind.Source = stu; bind.Path = new PropertyPath("Name"); this.textBox1.SetBinding(TextBox.TextProperty, bind); } private void button1_Click(object sender, RoutedEventArgs e) { stu.Name += "f"; new Window1().Show(); } }
在MainWindow的构造器中“InitializeComponent();”是自己主动生成的代码,用途是初始化UI元素。“stu=new Student();”这句是创建一个Student实例并用stu成员变量引用它,这个对象就是我们的数据源。
在准备Binding的部分,先使用“Binding bind = new Binding();”声明Binding类型变量并创建实例,然后使用“bind.Source=stu;”为Binding实例指定数据源,最后使用“bind.Path= new PropertyPath(‘Name‘)”语句为Binding指定訪问路径。
把数据源和目标连接在一起的任务是使用“BindingOperations.SetBinding(...)”方法完毕的,这种方法的3个參数是我们记忆的重点:
第一个參数是指定Binding的目标,本例中的this.textBoxName。
与数据源的Path原理类似,第二个參数用于为Binding指明为Binding指明把这个数据送达目标的哪个数据。
第三个參数非常明显,就是指定使用哪个Binding实例将数据源和目标关联起来。
执行程序,单击button我们将会看到例如以下的效果图:
通过上面的样例,我们已经在头脑中建立起来如图所看到的的模型
先用这个做基础,后面我们将研究Binding的每一个特点。
1.3 Binding的源与路径
Binding 的源也就是数据的源头。Binding对源的要求并不苛刻------仅仅要它是一个对象,而且通过属性(Property)公开自己的数据,它就能作为Binding 的源。
前面一个样例已经向大家证明,假设想让作为Binding源的对象具有自己主动通知Binding自己属性值已经已经变化的能力。那么就须要让类实现INotifyChanged接口并在属性的Set语句中激发PropertyChanged事件。在日常生活中,除了使用这样的对象作为数据源之外,我们还有很多其它的选择,比方控件把自己的容器或子集元素当源、用一个控件做为还有一个控件的数据源,把集合作为ItemControl的数据源、使用XML作为TreeView或Menu的数据源。把多个控件关联到一个“数据制高点”上,甚至干脆不给Binding指定数据源、让他自己去找。以下我们就分述这些情况。
1.3.1 把控件作为Binding源与Binding标记扩展。
前面讲过,大多数情况下Binding的源是逻辑层对象,但有的时候为了让UI产生联动效果也会使用Binding在控件间建立关联。以下的代码是吧一个TextBox的Text属性关联到Slider的Value的属性上。
<Window x:Class="WpfApplication1.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="321" Width="401"> <Grid> <TextBox Height="23" HorizontalAlignment="Left" Margin="141,46,0,0" Name="textBox1" VerticalAlignment="Top" Width="120" Text="{Binding Path=Value,ElementName=slider1}"/> <Slider Height="23" HorizontalAlignment="Left" Margin="84,106,0,0" Name="slider1" VerticalAlignment="Top" Width="212" /> <Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="166,197,0,0" Name="button1" VerticalAlignment="Top" Width="75" Click="button1_Click" /> </Grid> </Window>执行效果例如以下图:
正如大家所见,除了能够在C#中建立Binding外在XAML代码里也能够方便的设置Binding,这就给设计师非常大的自由度来决定UI元素之间的关联情况。值得注意的是,在C#代码中,能够訪问在XAML中声明的变量可是XAML中不能訪问C#中声明的变量,因此,要想在XAML中建立UI元素和逻辑对象的Binding还要颇费些周折,把逻辑代码声明为XAML中的资源(Resource),我们放资源一章去讲。
回头来看XAML代码,它使用了Binding标记扩展语法:
<TextBox Height="23" HorizontalAlignment="Left" Margin="141,46,0,0" Name="textBox1" VerticalAlignment="Top" Width="120" Text="{Binding Path=Value,ElementName=slider1}"/>与之等价的C#代码是:
this.textBox1.SetBinding(TextBox.TextProperty, new Binding("Value") { ElementName="Slider1"});由于Binding类的构造器本身具有能够接收Path的參数,所以也常写作:
<TextBox Height="23" HorizontalAlignment="Left" Margin="141,46,0,0" Name="textBox1" VerticalAlignment="Top" Width="120" Text="{Binding Value,ElementName=slider1}"/>注意:
由于我们在C#代码中能够直接訪问控件对象,所以一般不会使用Binding的ElementName属性,而是直接赋值给Binding的Sourece属性。
Binding的标记扩展语法,初看有些平淡甚至有些别扭,但细品就会体验到其的静止之处。说它别扭,是由于我们已经习惯了Text=“Hello World”这样的键--值式的赋值方式,并且觉得值与属性的值类型一定要一致-------大脑非常快会质询Text="{Binding Value,ElementName=Slider1}"的字面意思----Text的类型是String,为什么要赋一个Binding类型的值呢?事实上我们并非为Text属性赋值,为了消除这样的误会,我们能够把代码读作:为Text属性设置Binding为...。再想深一步,我们不是常常把函数视为一个值吗?仅仅是这个值在函数运行之后才干得到。同理,我们也能够把Binding视为一种间接的、不固定的赋值方式-----Binding扩展非常恰当的表达了这个赋值方式。
1.3.2 控制Binding的方向及数据更新
Binding在源与目标之间架起了沟通的桥梁,默认情况下数据即能够通过Binding送达目标,也能够通过目标回到源(收集用户对数据的改动)。有时候数据仅仅须要展示给用户,不须要用户改动,这时候能够把Binding模式设置为从目标向源的单向沟通以及仅仅在Binding关系确立时读取一次数据,这须要我们依据实际情况选择。
控制Binding数据流向的属性是Model,它的类型是BindingModel的枚举。BindingModel能够取值为TwoWay、OneWay、OneTime、OneWayToSource和Default。这里的Default指的是Binding的模式会依据目标是实际情况来确定,不如是能够编辑的(TextBox的Text属性),Default就採用双向模式。假设是TextBlock,不可编辑,就使用单向模式。
接上一节的小样例,拖动Slider手柄时,TextBox就会显示Slider的当前值(实际上这一块涉及到一个Double到String类型的转换,暂且忽略不计);假设我们在TextBox里面输入一个恰当的值按Tab键、让焦点离开TextBox,则Slider手柄就会跳转至对应的值那里。例如以下图所看到的:
为什么一定要在TextBox失去焦点以后才改变值呢?这就引出了Binding的另外一个属性-----UpdateSourceTrigger,它的类型是UpdateSourceTrigger枚举,可取值为PropertyChanged、LostFous、Explicit和Default。显然,对于Text的Default行为与LostFocus一致,我们仅仅须要把这个值改成PropertyChanged,则Slider就会随着输入值的变化而变化了。
注意:
顺便提一句,Binding还具有NotifyOnSourceUpdated属性和NotifyOnTargetUpdated两个bool类型是属性。假设设置为True,则在源或目标被更新以后就会触发对应的SourceUpdated事件和TargetUpdated事件。实际工作中我们能够监听这两个事件来找出来哪些数据或控件被更新了。
1.3.3 Binding的路径(Path)
做为Binding的源可能会有非常多属性,通过这些属性Binding源能够把数据暴露给外界。那么,Binding究竟须要关注哪个属性值呢?就须要用Binding的Path属性来指定了。比如前面这个样例,我们把Slider控件对象作为数据源,把它的Value属性作为路径。
虽然在XAML代码中或者Binding类的构造器參数列表中我们使用字符串来表示Path,但Path的实际类型是PropertyPath。以下让我们来看看怎样创建Path来应付实际情况(我将使用C#和XAML两种代码进行描写叙述)。
最简单的方法就是直接把Binding关联到Binding源的属性上,前面的样例就是这样,语法例如以下:
<TextBox Height="23" HorizontalAlignment="Left" Margin="141,46,0,0" Name="textBox1" VerticalAlignment="Top" Width="120" Text="{Binding Path=Value,ElementName=slider1}"/>等效的C#代码就是:
this.textBox1.SetBinding(TextBox.TextProperty, new Binding("Value") {Source=slider1});Binding还支持多级路径(通俗的讲就是一路“点”下去),比方,我们想让一个TextBox显示另外一个TextBox内容的长度,我们能够这样写:
<TextBox Height="23" HorizontalAlignment="Left" Margin="152,50,0,0" Name="textBox1" VerticalAlignment="Top" Width="158" /> <TextBox Height="23" HorizontalAlignment="Left" Margin="152,105,0,0" Name="textBox2" Text="{Binding Path=Text.Length,ElementName=textBox1,Mode=OneWay}" VerticalAlignment="Top" Width="158"/>
this.textBox2.SetBinding(TextBox.TextProperty, new Binding("Text.Length") {Source = textBox1, Mode= BindingMode.OneWay });
我们知道,集合类型是索引器(Indexer)又称为带參属性。既然是属性,索引器也能作为Path来使用。比方我们想让一个TextBox显示另外一个TextBox的第4个字符,我们能够这样写:
<TextBox Height="23" HorizontalAlignment="Left" Margin="152,50,0,0" Name="textBox1" VerticalAlignment="Top" Width="158" Text="ABCDE" /> <TextBox Height="23" HorizontalAlignment="Left" Margin="152,105,0,0" Name="textBox2" Text="{Binding Path=Text[3],ElementName=textBox1,Mode=OneWay}" VerticalAlignment="Top" Width="158"/>
this.textBox2.SetBinding(TextBox.TextProperty, new Binding("Text.[3]") { Source=textBox1,Mode= BindingMode.OneWay});
当使用一个集合或者DataView做为数据源时,假设我们想把它默认的元素做为数据源使用,则须要使用以下的语法:
List<string> infos = new List<string>() { "Jim","Darren","Jacky"}; textBox1.SetBinding(TextBox.TextProperty, new Binding("/") { Source=infos}); textBox2.SetBinding(TextBox.TextProperty, new Binding("/[2]") { Source = infos, Mode= BindingMode.OneWay }); textBox3.SetBinding(TextBox.TextProperty, new Binding("/Length") { Source = infos, Mode= BindingMode.OneWay });
假设集合中仍然是集合,我们想把子集集合中的元素做Path,我们能够使用多级斜线的语法(即“一路”斜线下去),比如:
/// <summary> /// Window4.xaml 的交互逻辑 /// </summary> public partial class Window4 : Window { public Window4() { InitializeComponent(); List<Contry> infos = new List<Contry>() { new Contry() { Name = "中国", Provinces= new List<Province>(){ new Province(){ Name="四川",Citys=new List<City>(){new City(){Name="绵阳市" }}}}}}; this.textBox1.SetBinding(TextBox.TextProperty, new Binding("/Name") { Source=infos}); this.textBox2.SetBinding(TextBox.TextProperty, new Binding("/Provinces/Name") { Source = infos }); this.textBox3.SetBinding(TextBox.TextProperty, new Binding("/Provinces/Citys/Name") { Source = infos }); } } class City { public string Name { set; get; } } class Province { public string Name { set; get; } public List<City> Citys { set; get; } } class Contry { public string Name { set; get; } public List<Province> Provinces { get; set; } }
1.3.4 "没有Path"的Binding
有的时候我们会在代码中我们看大Path是一个“.”或者干脆没有Path的Binding,着实让人摸不着头脑。原来这是一种比較特殊的情况---Binding源本身就是一种数据且不须要Path来指明。典型的string,int等基本类型都是这样,他们是实例本身就是数据,我们无法指定通过那个属性来訪问这个数据,这是我们仅仅须要将这个数据设置为.就能够了。在XAML中这个.能够忽略不写,可是在C#中编程必须要带上。以下请看以下这段代码:
<Window x:Class="WpfApplication1.Window5" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:String="clr-namespace:System;assembly=mscorlib" Title="Window5" Height="331" Width="538"> <StackPanel Height="184" Name="stackPanel1" Width="288"> <StackPanel.Resources> <String:String x:Key="myString"> 菩提本无树,何处染尘埃。 </String:String> </StackPanel.Resources> <TextBlock Height="23" Name="textBlock1" Text="{Binding Path=.,Source={StaticResource ResourceKey=myString}}" /> </StackPanel> </Window>
<TextBlock Height="23" Name="textBlock1" Text="{Binding .,Source={StaticResource ResourceKey=myString}}" />或者直接写成:
<TextBlock Height="23" Name="textBlock1" Text="{Binding Source={StaticResource ResourceKey=myString}}" />
注意:
最后这样的简写非常easy被误解为没有指定Path,事实上仅仅是省略掉了。与仅仅等效的C#代码例如以下:
string myString = "菩提本无树,明镜亦无台。本来无一物,何处染尘埃。"; this.textBlock1.SetBinding(TextBlock.TextProperty, new Binding(".") { Source=myString});
最后顺便带一句,PropertyPath除了用于Binding的Path属性之外,在动画编程的时候也会派上用场(Storyboard.TargetProperty)。在用于动画编程的时候,PropertyPath还有第二种语法,到时候我们细说。
1.3.5 把Binding指定为源(Source)的几种方法
上一节我们学习了怎样通过Binding的path属性怎样在一个对象上寻找数据。这一节我们将学习怎样为Binding指定源(Source)。
Binding的源是数据的来源,所以,仅仅要一个对象包括数据而且可以通过属性将数据暴露出来,它就能当作Binding的源来使用。包括数据的对象比比皆是,但必须为Binding的Source指定合适的对象Binding才干正常工作,经常使用的办法有:
以下我们使用实例来分别描写叙述每种情况:
1.3.6 没有Source的Binding----使用DataContext作为数据源
前面的样例都是把单个的CLR对象作为Binding 的源,方法有两种:把对象赋值给Binding.Source属性或者把对象的Name赋值给Binding.ElementName。DataContext被定义在FrameWorkElement类中,这个类是WPF控件的基类,这意味着全部的WPF控件包括布局控件都包括这个属性。如前所述,WPF的UI布局是树形结构,这个树的每一个节点都是控件,由此我们推出一个结论----在UI树的每一个节点都有DataContext属性。这一点很重要,由于当一个Binding仅仅知道自己的Path而不知道自己的源的时候,它会沿着树的一路向树的根部找过去,每经过一个节点都要查看这个节点的DataContext属性是否具有Path所指定的属性。假设有,就把这个对象作为自己的Source;假设没有,就继续找下去;假设到树的根部还没有找到,那这个Binding就没有Source,因而也不会得到数据,让我们看看以下的样例:
先创建一个名为Student的类,它具有ID,Name,Age3个属性:
public class Student { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } }
<Window x:Class="WpfApplication1.Window6" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Stu="clr-namespace:WpfApplication1" Title="Window6" Height="345" Width="464"> <StackPanel Background="AliceBlue"> <StackPanel.DataContext> <Stu:Student Id="1" Name="Darren" Age="10"></Stu:Student> </StackPanel.DataContext> <Grid> <StackPanel Height="283" HorizontalAlignment="Left" Margin="12,12,0,0" Name="stackPanel1" VerticalAlignment="Top" Width="418"> <TextBox Height="23" Name="textBox1" Width="120" Margin="15" Text="{Binding Path=Id}"/> <TextBox Height="23" Name="textBox2" Width="120" Margin="15" Text="{Binding Path=Name}"/> <TextBox Height="23" Name="textBox3" Width="120" Margin="15" Text="{Binding Path=Age}"/> </StackPanel> </Grid> </StackPanel> </Window>这个UI能够用例如以下的柱状图来表示:
使用xmlns:Stu="clr-namespace:WpfApplication1",我们就能够在XAML中使用在C#中定义的类。使用了这几行代码:
<StackPanel.DataContext> <Stu:Student Id="1" Name="Darren" Age="10"></Stu:Student> </StackPanel.DataContext>
<TextBox Height="23" Name="textBox1" Width="120" Margin="15" Text="{Binding Id}"/> <TextBox Height="23" Name="textBox2" Width="120" Margin="15" Text="{Binding Name}"/> <TextBox Height="23" Name="textBox3" Width="120" Margin="15" Text="{Binding Age}"/>
前面在学习Binding路径的时候,当Binding的Source本身就是数据、不须要使用属性来暴露数据时,Binding的Path能够设置为".",亦可省略不写。如今Source也能够省略不写了,这样,当某个DataContext为简单类型对象的时候,我们全然可能看到一个既没有Path,又没有Source的Binding:
<Window x:Class="WpfApplication1.Window7" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Str="clr-namespace:System;assembly=mscorlib" Title="Window7" Height="300" Width="300"> <Grid> <Grid.DataContext> <Str:String>Hello DataContext</Str:String> </Grid.DataContext> <StackPanel> <TextBlock Height="23" HorizontalAlignment="Left" Margin="15" Name="textBlock1" Text="{Binding}" VerticalAlignment="Top" /> <TextBlock Height="23" HorizontalAlignment="Left" Margin="15" Name="textBlock2" Text="{Binding}" VerticalAlignment="Top" /> <TextBlock Height="23" HorizontalAlignment="Left" Margin="15" Name="textBlock3" Text="{Binding}" VerticalAlignment="Top" /> </StackPanel> </Grid> </Window>
你可能回忆,Binding怎么会自己主动向UI元素上一层查找DataContext并把它作为自己的Source呢?事实上,“Binding沿着UI元素树向上找”仅仅是WPF给我们的一个错觉,Binding并没有那么智能。之所以会这样是由于DataContext是一个“依赖属性”,后面的章节我们会具体描写叙述,依赖属性有一个非常明显的特点就是你没有为某个控件的依赖属性赋值的时候,控件会把自己容器的属性值接过来当作自己的属性值。实际上属性值是沿着UI元素树向下传递的。
在实际工作中,DataContext属性值的运用很的灵活。比方:
当UI上的多个控件都使用Binding关注同一个对象变化的时候,最好还是使用DataContext。
当作为Source的对象不能被直接訪问的时候----比方B窗口内的控件想把A窗口里的控件当作自己的Binding源时,可是A窗口内的控件可訪问级别是private类型,这是就能够把这个控件或者控件值作为窗口A的DataContext(这个属性是Public级别的)这样就能够暴露数据了。
形象的说,这时候外层的数据就相当于一个数据的“至高点”,仅仅要把元素放上去,别人就行看见。另外DataContext本身就是一个依赖属性,我们可以使用Binding把它关联到一个数据源上。
1.3.7 使用集合对象作为列表控件的ItemsSource
有了DataContext作为基础,我们再来看看把集合类型对象作为Binding源的情况。
WPF中的列表式控件都派生自ItemControl类,自然也继承了ItemSource这个属性。ItemSource能够接收一个IEnumerable接口派生类的实例作为自己的值(全部可被迭代遍历的集合都实现了这个接口,包含数组、List<T>等)。每一个ItemControl都具有自己的条目容器Item Container,比如,ListBox的条目容器是ListBoxItem、Combox的条目容器是ComboxItem。ItemSource里面保存的是一条一条的数据,想要把数据显示出来就要为数据穿上外衣,条目容器就起到了数据外衣的作用。这样将数据外衣和它所相应的条目容器关联起来呢?当然时依靠Binding!仅仅要我们为一个ItemControl设置了ItemSource属性值,ItemControl会自己主动迭代当中的数据元素,为每一个数据元素准备一个条目容器,并使用Binding元素在条目容器和数据元素之间建立起关联,让我们来看一个样例:
UI代码例如以下:
<Window x:Class="WpfApplication1.Window8" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window8" Height="356" Width="471"> <Grid> <StackPanel Height="295" HorizontalAlignment="Left" Margin="10,10,0,0" Name="stackPanel1" VerticalAlignment="Top" Width="427"> <TextBlock Height="23" Name="textBlock1" Text="学员编号:" /> <TextBox Height="23" Name="txtStudentId" Width="301" HorizontalAlignment="Left"/> <TextBlock Height="23" Name="textBlock2" Text="学员列表:" /> <ListBox Height="208" Name="lbStudent" Width="305" HorizontalAlignment="Left"/> </StackPanel> </Grid> </Window>
窗口执行效果例如以下图:
我们要实现的效果就是把List<Student>的集合作为ListBox的ItemSource,让ListBox显示学员的Name,并使用TextBox显示当前选中学员的Id,为了实现这个功能,我们须要在窗口的构造函数中加入几行代码:
List<Student> infos = new List<Student>() { new Student(){ Id=1, Age=11, Name="Tom"}, new Student(){ Id=2, Age=12, Name="Darren"}, new Student(){ Id=3, Age=13, Name="Jacky"}, new Student(){ Id=4, Age=14, Name="Andy"} }; this.lbStudent.ItemsSource = infos; this.lbStudent.DisplayMemberPath = "Name"; this.txtStudentId.SetBinding(TextBox.TextProperty,new Binding("SelectedItem.Id"){ Source=lbStudent});执行结果例如以下图:
你可能回忆,这个样例中并没有出现刚才我们说的Binding。实际上, this.lbStudent.DisplayMemberPath = "Name";这点代码露出了一点蛛丝马迹。注意到包括Path这个单词了吗?这说明它是一个路径。当DisplayMemberPath 被赋值以后,ListBox在获得ItemSource的时候就会创建一个等量的ListBoxItem并以DisplayMemberPath的值为Path创建Binding,Binding的目标是ListBoxItem的内容插件(实际上是一个TextBox,以下就会看见)。
如过在ItemControl类的代码里刨根问底,你会发现Binding的过程是在DisplayMemberTemplateSelector类的SelectTemplate方法里完毕的。这种方法的定义格式例如以下:
public override DataTemplate SelectTemplate(object item, DependencyObject container) { //逻辑代码 }
FrameworkElementFactory text = ContentPresenter.CreateTextBlockFactory(); Binding bind = new Binding(); bind.Path = new PropertyPath(_displayMemberPath); bind.StringFormat = _stringFormat; text.SetBinding(TextBlock.TextProperty,bind);
这里对新创建的Binding设定了Path而没有指定Source,紧接这就把它关联到了TextBlock上。显然,要想得到Source,这个Binding须要向树根方向寻找包括_displayMemberPath指定属性的DataContext。
最后我们再看一个显示为数据设置DataTemplate的样例,先把C#代码中的this.lbStudent.DisplayMemberPath = "Name";一句删除,再在XAML代码中加入几行代码,ListBox的ItemTemplate属性(继承自ItemControl类)的类型是DataTemplate,以下我们就为Student类型实例量身定做“衣服”。
<Window x:Class="WpfApplication1.Window8" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window8" Height="356" Width="471"> <Grid> <StackPanel Height="295" HorizontalAlignment="Left" Margin="10,10,0,0" Name="stackPanel1" VerticalAlignment="Top" Width="427"> <TextBlock Height="23" Name="textBlock1" Text="学员编号:" /> <TextBox Height="23" Name="txtStudentId" Width="301" HorizontalAlignment="Left"/> <TextBlock Height="23" Name="textBlock2" Text="学员列表:" /> <ListBox Height="208" Name="lbStudent" Width="305" HorizontalAlignment="Left"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Name="stackPanel2" Orientation="Horizontal"> <TextBlock Text="{Binding Id}" Margin="5" Background="Beige"/> <TextBlock Text="{Binding Name}" Margin="5"/> <TextBlock Text="{Binding Age}" Margin="5"/> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </StackPanel> </Grid> </Window>
最后特别提醒大家一点:
在使用集合类型的数据作为列表控件的ItemSource时通常会考虑使用ObservableCollection<T>替换List<T>,由于ObservableCollection<T>类实现了INotifyChange和INotifyPropertyChanged接口,能把集合的变化立马通知显示到它的列表控件上,改变会立马显示出来。
1.3.8 使用ADO.NET对象作为Binding的源
在.Net开发工作中,我们用ADO.NET类对数据库进行操作。常见的工作就是从数据库中读取数据到DataTable中,在把DataTable里的数据绑定的UI的控件里面(如成绩单、博客列表)。虽然在流行的软件架构中并不把DataTable中的数据直接显示在UI列表控件里面而是先通过LINQ等手段把DataTable里的数据转换成恰当的用户自己定义类型集合,但WPF也支持DataTable也支持在列表控件和DataTable里直接建立Binding。
如今我们做一个实例来解说怎样在DataTable和UI建立Binding:
多数情况下我们会用ListView控件来显示一个DataTable,XAML代码例如以下:
<Window x:Class="WpfApplication1.Window9" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window9" Height="345" Width="482"> <StackPanel Height="279" Name="stackPanel1" Width="431"> <ListView Height="247" Name="listView1" Width="376"> <ListView.View> <GridView> <GridViewColumn Header="ID" DisplayMemberBinding="{Binding Id}" Width="60"> </GridViewColumn> <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" Width="60"> </GridViewColumn> <GridViewColumn Header="Age" DisplayMemberBinding="{Binding Age}" Width="60"> </GridViewColumn> <GridViewColumn Header="Sex" DisplayMemberBinding="{Binding Sex}" Width="60"> </GridViewColumn> </GridView> </ListView.View> </ListView> </StackPanel> </Window>
从字面上来理解,ListView和GridView应该属于同一级别的控件,实际上远非这样!ListView是ListBox的派生类而GridView是ViewBase的派生类,ListView中的View是一个ViewBase对象,所以,GridView能够做为ListView的View来使用而不能当作独立的控件来使用。这里使用理念是组合模式,即ListView有一个View,可是至于是GridView还是其他类型的View,由程序猿自己选择----眼下仅仅有一个GridView可用,预计微软在这里还会有扩展。其次,GridView的内容属性是Columns,这个属性是GridViewColumnCollection类型对象。由于XAML支持对内容属性的简写,能够省略<GridView.Columns>这层标签,直接在GridView的内容部分定义3个<GridViewColumn>对象,GridViewColumn中最重要的一个属性是DisplayBinding(类型是BindingBase),使用这个属性能够指定这一列使用什么样的Binding去关联数据------这与ListBox有点不同,ListBox使用的是DisplayMemberPath属性(类型是string)。假设想用更复杂的结构来表示这一标题或数据,则可为GridViewColumn设置HeadTemplate和CellTemplate,它们的类型都是DataTemplate。
执行效果例如以下:
后台代码例如以下:
public Window9() { InitializeComponent(); DataTable dtInfo = CreateDataTable(); for (int i = 0; i < 10; i++) { DataRow dr = dtInfo.NewRow(); dr[0] = i; dr[1] = "猴王" + i; dr[2] = i + 10; dr[3] = "男"; dtInfo.Rows.Add(dr); } this.listView1.ItemsSource = dtInfo.DefaultView; } private DataTable CreateDataTable() { DataTable dt = new DataTable("newtable"); DataColumn[] columns = new DataColumn[]{new DataColumn("Id"),new DataColumn("Name"),new DataColumn("Age"),new DataColumn("Sex")}; dt.Columns.AddRange(columns); return dt; }
InitializeComponent(); DataTable dtInfo = CreateDataTable(); for (int i = 0; i < 10; i++) { DataRow dr = dtInfo.NewRow(); dr[0] = i; dr[1] = "猴王" + i; dr[2] = i + 10; dr[3] = "男"; dtInfo.Rows.Add(dr); } this.listView1.ItemsSource = dtInfo;
错误
1无法将类型“System.Data.DataTable”隐式转换为“System.Collections.IEnumerable”。存在一个显式转换(是否缺少强制转换?)d:\我的文档\visual studio 2010\Projects\WpfApplication2\WpfApplication1\Window9.xaml.cs3642WpfApplication1
显然DataTable不能直接拿来为ItemSource赋值。只是,当你把DataTable对象放在一个对象的Context属性的时候,并把一个ItemSource与一个既没有指定Source又没有指定Path的Binding绑定起来的时候,Binding却能自己主动找到它的DefaultView并当作自己的Source来使用:
DataTable dtInfo = CreateDataTable(); for (int i = 0; i < 10; i++) { DataRow dr = dtInfo.NewRow(); dr[0] = i; dr[1] = "猴王" + i; dr[2] = i + 10; dr[3] = "男"; dtInfo.Rows.Add(dr); } this.listView1.DataContext = dtInfo; this.listView1.SetBinding(ListView.ItemsSourceProperty, new Binding());
1.3.9 使用XML数据作为Binding的源
迄今为止,.NETFramWork提供了两套处理XML数据的类库:
符合DOM(Document Object Modle,文档对象模型)标准类库:包含XmlDocument、XmlElement、XmlNode、XmlAttribute等类。这套类库的特点是中规中矩,功能强大,但也背负了太多了XML的传统和复杂。
以LINQ(Language-Intergrated Query,语言集成查询)为基础的类库:包含XDocument,XElement,XNode,XAttribute等类。这套类库的特点是能够通过LINQ进行查询和操作,方便快捷。
以下我们主要解说一下标准类型的类库,基于LINQ的查询我们放在下一节讨论。
如今程序设计仅仅要涉及到远程传输就离不开XML,由于大多数传输数据是基于SOAP(Simple Object Access Protocol,简单对象訪问协议)相关文档协议,而SOAP又是将对象序列化为XML文本进行传输。XML文本是树形结构的,所以XML能够方便的用于表示线性集合(如Array、List等)和树形结构数据。
注意:
在使用XML数据作为Binding的Source的时候我们将使用XPath属性而不是Path属性来指定数据的来源。
我们先看一个线性集合的样例。以下的XML文本是一组文本信息,我们要把它显示在一个ListView控件里:
<?xml version="1.0" encoding="utf-8" ?> <StudentList> <Student id="1"> <Name>Andy</Name> </Student> <Student id="2"> <Name>Jacky</Name> </Student> <Student id="3"> <Name>Darren</Name> </Student> <Student id="4"> <Name>DK</Name> </Student> <Student id="1"> <Name>Jim</Name> </Student> </StudentList>相应的XAML例如以下:
<Window x:Class="WpfApplication1.Window10" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window10" Height="397" Width="485"> <StackPanel Width="409" Height="331" Background="LightBlue"> <ListView Height="302" Name="listView1" Width="396"> <ListView.View> <GridView> <GridViewColumn Header="ID" DisplayMemberBinding="{Binding XPath=@id}" Width="80"> </GridViewColumn> <GridViewColumn Header="Name" DisplayMemberBinding="{Binding XPath=Name}" Width="150"> </GridViewColumn> </GridView> </ListView.View> </ListView> </StackPanel> </Window>C#代码例如以下:
private void BindingInfo() { XmlDocument doc = new XmlDocument(); doc.Load(@"d:\我的文档\visual studio 2010\Projects\WpfApplication2\WpfApplication1\StudentData.xml"); XmlDataProvider dp = new XmlDataProvider(); dp.Document = doc; dp.XPath = @"StudentList/Student"; this.listView1.DataContext = dp; this.listView1.SetBinding(ListView.ItemsSourceProperty, new Binding()); }
XMLDataProvider另一个名为Source的属性,能够直接用它指定XML文档所在位置(不管是XML文档是存储在本地硬盘还是网络位置),所以,后台代码也能够写成例如以下:
private void BindingInfo() { //XmlDocument doc = new XmlDocument(); //doc.Load(@"d:\我的文档\visual studio 2010\Projects\WpfApplication2\WpfApplication1\StudentData.xml"); XmlDataProvider dp = new XmlDataProvider(); dp.Source = new Uri(@"d:\我的文档\visual studio 2010\Projects\WpfApplication2\WpfApplication1\StudentData.xml"); // dp.Document = doc; dp.XPath = @"StudentList/Student"; this.listView1.DataContext = dp; this.listView1.SetBinding(ListView.ItemsSourceProperty, new Binding()); }
XML语言能够方便的表示树形数据结构,以下的样例是使用TreeView控件来显示拥有若干层文件夹的文件系统,并且,这次把XML数据和XMLDataProvider对象直接写在XAML里面,代码中用到了HierarchicalDataTemplate类,这个类具有ItemsSource属性,可见由这样的Template展示的数据是能够有子级集合的。代码例如以下:
<Window x:Class="WpfApplication1.Window11" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window11" Height="349" Width="545"> <Window.Resources> <XmlDataProvider x:Key="xdp" XPath="FileSystem/Folder"> <x:XData> <FileSystem xmlns=""> <Folder Name="Books"> <Folder Name="Programming"> <Folder Name="Windows"> <Folder Name="WPF"> </Folder> <Folder Name="Winform"> </Folder> <Folder Name="ASP.NET"> </Folder> </Folder> </Folder> </Folder> <Folder Name="Tools"> <Folder Name="Development"/> <Folder Name="Designment"/> <Folder Name="Players"/> </Folder> </FileSystem> </x:XData> </XmlDataProvider> </Window.Resources> <Grid> <TreeView Height="283" HorizontalAlignment="Left" Name="treeView1" VerticalAlignment="Top" Width="511" ItemsSource="{Binding Source={StaticResource ResourceKey=xdp}}"> <TreeView.ItemTemplate> <HierarchicalDataTemplate ItemsSource="{Binding XPath=Folder}"> <TextBlock Height="23" HorizontalAlignment="Left" Name="textBlock1" Text="{Binding XPath=@Name}" VerticalAlignment="Top" /> </HierarchicalDataTemplate> </TreeView.ItemTemplate> </TreeView> </Grid> </Window>
将XmlDataProvider直接写在XAML代码里面,那么他的数据须要放在<x:XData>标签中。
因为本样例设计到了StaticResource和HierarchicalDataTemplate,都是后面的内容,相对照较难懂,等学习完后面的Resource和Template章节之后再回来便会了然于胸。
程序执行效果例如以下图:
1.3.10 使用LINQ检索结果做为Binding 的源
至3.0版本号開始,.NET Framework開始支持LINQ(Language-Intergrated Query 语言集成查询),使用LINQ,我们能够方便的操作集合对象、DataTable对象和XML对象不必动辄不动把好几层foreach循环嵌套在一起却仅仅是为了完毕一个非常easy的任务。
LINQ查询的结果是一个IEnumerable<T>类型对象,而IEnumerable<T>又派生自IEnumerable,所以它能够作为列表控件的ItemsSource来使用。
先创建一个名为Student的类:
public class Student { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } }XAML代码例如以下:
<Window x:Class="WpfApplication1.Window12" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window12" Height="372" Width="538"> <Grid> <ListView Height="311" HorizontalAlignment="Left" Margin="10,10,0,0" Name="listView1" VerticalAlignment="Top" Width="494"> <ListView.View> <GridView> <GridViewColumn Header="ID" DisplayMemberBinding="{Binding Id}" Width="100"/> <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" Width="100"/> <GridViewColumn Header="Age" DisplayMemberBinding="{Binding Age}" Width="100"/> </GridView> </ListView.View> </ListView> </Grid> </Window>
private void BindingData() { List<Student> infos = new List<Student>() { new Student(){Id=1, Age=29, Name="Tim"}, new Student(){Id=1, Age=28, Name="Tom"}, new Student(){Id=1, Age=27, Name="Kyle"}, new Student(){Id=1, Age=26, Name="Tony"}, new Student(){Id=1, Age=25, Name="Vina"}, new Student(){Id=1, Age=24, Name="Mike"} }; this.listView1.ItemsSource = from stu in infos where stu.Name.StartsWith("T") select stu; }假设数据存放在一个DataTable对象里面,则后台代码例如以下:
private void BindingDataByDataTable() { DataTable dtInfo = CreateDataTable(); this.listView1.ItemsSource = from row in dtInfo.Rows.Cast<DataRow>() where Convert.ToString(row["Name"]).StartsWith("T") select new Student() { Id = Convert.ToInt32(row["Id"]), Name=Convert.ToString(row["Name"]),Age=Convert.ToInt32(row["Age"]) }; }假设数据存储在XML里面,存储格式例如以下:
<?xml version="1.0" encoding="utf-8" ?> <StudentList> <Class> <Student Id="0" Age="29" Name="Tim" /> <Student Id="0" Age="28" Name="Tom" /> <Student Id="0" Age="27" Name="Mess" /> </Class> <Class> <Student Id="0" Age="26" Name="Tony" /> <Student Id="0" Age="25" Name="Vina" /> <Student Id="0" Age="24" Name="Emily" /> </Class> </StudentList>
private void BindingDataByXml() { XDocument xd = XDocument.Load(@"d:\我的文档\visual studio 2010\Projects\WpfApplication2\WpfApplication1\testDate.xml"); this.listView1.ItemsSource = from element in xd.Descendants("Student") where element.Attribute("Name").Value.StartsWith("T") select new Student() { Name = element.Attribute("Name").Value, Id = Convert.ToInt32(element.Attribute("Id").Value), Age = Convert.ToInt32(element.Attribute("Age").Value) }; }
程序执行效果例如以下图:
1.3.11 使用ObjectDataProvider作为binding的Source
理想情况下,上游程序猿将类设计好、使用属性把数据暴露出来,下游程序猿将这些类作为Binding的Source、把属性作为Binding的Path来消费这些类。但非常难保证一个类的属性都用属性暴露出来,比方我们须要的数据可能是方法的返回值。而又一次设计底层类的风险和成本会比較高,况且黑盒引用类库的情况下我们不可能更改已经编译好的类,这时候须要使用ObjectDataProvider来包装做为Binding源的数据对象了。
ObjcetDataProvider 顾名思义就是把对对象作为数据源提供给Binding。前面还提到过XmlDataProvider,这两个类的父类都是DataSourceProvider抽象类。
如今有一个名为Calculator的类,它具有加、减、乘、除的方法:
public class Caculate { public string Add(string arg1,string arg2) { double x = 0; double y = 0; double z = 0; if(double.TryParse(arg1,out x)&&double.TryParse(arg2,out y)) { z = x + y; return z.ToString(); } return "Iput Error"; } //其他方法省略 }
private void button1_Click(object sender, RoutedEventArgs e) { ObjectDataProvider odp = new ObjectDataProvider(); odp.ObjectInstance = new Caculate(); odp.MethodName="Add"; odp.MethodParameters.Add("100"); odp.MethodParameters.Add("200"); MessageBox.Show(odp.Data.ToString()); }
通过这个程序我们能够了解到ObjectDataProvider对象和它被包装的对象关系例如以下图:
了解了ObjectDataProvider的用法,我们看看怎样把它当作Binding的Source来使用。程序的XAML代码和截图例如以下:
<Window x:Class="WpfApplication1.Window14" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window14" Height="202" Width="345"> <StackPanel Background="LightBlue"> <TextBox Height="23" Name="textBox1" Width="200" HorizontalAlignment="Left" Margin="15"/> <TextBox Height="23" Name="textBox2" Width="200" HorizontalAlignment="Left" Margin="15"/> <TextBox Height="23" Name="textBox3" Width="200" HorizontalAlignment="Left" Margin="15"/> </StackPanel> </Window>
这个程序实现的功能是,我在前两个TextBox里面输入值的时候,第三个TextBox会显示前两个文本框里面相加之和。把代码写在一个名为SetBinding的方法里面,然后在窗口的构造器里面调用这种方法:
private void SetBinding() { ObjectDataProvider objpro = new ObjectDataProvider(); objpro.ObjectInstance = new Caculate(); objpro.MethodName = "Add"; objpro.MethodParameters.Add("0"); objpro.MethodParameters.Add("0"); Binding bindingToArg1 = new Binding("MethodParameters[0]") { Source=objpro,BindsDirectlyToSource=true, UpdateSourceTrigger= UpdateSourceTrigger.PropertyChanged}; Binding bindingToArg2 = new Binding("MethodParameters[1]") { Source=objpro,BindsDirectlyToSource=true,UpdateSourceTrigger=UpdateSourceTrigger.PropertyChanged}; Binding bindToResult = new Binding(".") { Source=objpro}; this.textBox1.SetBinding(TextBox.TextProperty, bindingToArg1); this.textBox2.SetBinding(TextBox.TextProperty, bindingToArg2); this.textBox3.SetBinding(TextBox.TextProperty,bindToResult); }
//--- objpro.ObjectInstance = typeof(Caculate); objpro.ConstructorParameters.Add(arg1); objpro.ConstructorParameters.Add(arg2); //----
接着,我们使用MethodName属性指定要调用的Caculator对象中名为Add的方法---问题又来了,假设Caculator有多个构造器參数的方法Add应该怎样区分?我们知道,重载方法的差别在于參数列表,紧接着两句就是向MethodParameter属性里面增加两个string类型的參数,这就相当于告诉ObjectDataProvider对象去调用Caculator对象中具有两个string类型參数的Add方法,换句话说,MethodParameter对于參数的感应是很敏感的。
准备好数据源之后,我们准备创建Binding。前面我们已经讲过使用索引器作为Binding的Path,第一个Binding它的Source是一个ObjectDataProvider对象,Path是ObjectDataProvider中MethodParameters所引用的第一个元素。BindsDirectlyToSource这句话是告诉Binding仅仅是将UI上的值传递给源而不是被ObjectDataProvider包装的Caculator,同一时候UpdateSourceTrigger设置为UI仅仅要一有变化就更新Source。第二个Binding仅仅是对第一个的翻版,仅仅是把Path属性指向了第二个元素。第三个binding仍然使用ObjectDataProvider作为Source,但使用“.”作为Path----前面讲过,当数据源本身就是数据的时候就用“.”来做为Path,在XAML中"."能够不写。
注意:
在ObjectDataProvider对象作为Binding的Source的时候,这个对象本身就代表了数据,所以这里的Path使用的“.”,而不是Data属性。
最后几行就是将Binding对象关联到3个TextBox对象上。程序执行效果例如以下:
普通情况下数据从那里来,哪里就是Binding的Source,数据到哪里去,哪里就是Binding 的Target。按这个理论,前两个TextBox应该是ObjcetDataProvider的源,而ObjcetDataProvider对象又是最后一个TextBox的源。但实际上,三个TextBox都以ObjcetDataProvider作为数据源,仅仅是前两个在数据流向上做了限制,这样做的原因不外乎有两个:
1、ObjcetDataProvider的MethodParameter不是依赖属性,不能作为Binding的目标。
2、数据驱动UI理念要求我们尽可能的使用数据对象作为Binding的Source而把UI当做Binding的Target。
1.3.12 使用Binding的RelativeSource
当一个Binding有明白的来源的时候,我们能够通过Source或者ElementName赋值的办法让Binding与之关联。有些时候我们不能确定作为Source对象叫什么名字,可是我们知道它与做为Binding目标对象在UI上的相对关系,比方控件自己关联自己的某个数据,关联自己某级容器的数据,这时候就须要用到Binding的RelativeSource属性。
RelativeSource属性的类型是RelativeSource类,通过这个类的几个静态或者非静态的属性我们能够控制它搜索相对数据源的方式。请看以下这段代码:
<Window x:Class="WpfApplication1.Window15" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window15" Height="375" Width="516"> <Grid Background="Red" Margin="10" x:Name="gd1"> <DockPanel x:Name="dp1" Margin="10" Background="Orange"> <Grid Background="Yellow" Margin="10" x:Name="gd2"> <DockPanel Name="dp2" Margin="10" Background="LawnGreen"> <TextBox Name="textBox1" Margin="10" FontSize="24"/> </DockPanel> </Grid> </DockPanel> </Grid> </Window>界面执行结果例如以下:
我们把TextBox的Text属性关联到外层容器的Name属性上。在窗口的构造器里面加入例如以下几行代码:
RelativeSource rs = new RelativeSource(RelativeSourceMode.FindAncestor); rs.AncestorLevel = 1; rs.AncestorType = typeof(Grid); Binding bind = new Binding("Name") { RelativeSource = rs }; this.textBox1.SetBinding(TextBox.TextProperty, bind);或在XAML代码中插入等效代码:
<TextBox Name="textBox1" Margin="10" FontSize="24" Text="{Binding Path=Name, RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type Grid},AncestorLevel=1}}"/>AncestorLevel属性指定的是以Binding目标控件为起点的层级偏移量---gd2的偏移量是1,gd2的偏移量是2,依次类推。AncestorType属性告诉Binding去找什么类型的对象作为自己的源,不是这个类型的对象会被跳过。上面这段代码的意思是告诉Binding从自己的第一层依次向外找,找到第一个Grid类型对象后把它当作自己的源。执行效果例如以下图:
假设把代码改成例如以下这样:
RelativeSource rs = new RelativeSource(RelativeSourceMode.FindAncestor); rs.AncestorLevel = 2; rs.AncestorType = typeof(DockPanel); Binding bind = new Binding("Name") { RelativeSource = rs }; this.textBox1.SetBinding(TextBox.TextProperty, bind);
Text="{Binding Path=Name, RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type DockPanel},AncestorLevel=2}}"执行效果例如以下:
假设TextBox须要关联自身的Name属性,那么代码应该这样写:
RelativeSource rs = new RelativeSource(RelativeSourceMode.Self); Binding bind = new Binding("Name") { RelativeSource = rs }; this.textBox1.SetBinding(TextBox.TextProperty, bind);
Text="{Binding Path=Name, RelativeSource={RelativeSource Mode=Self}}"执行效果例如以下图:
RelativeSource类的Mode属性是RelativeSourceMode枚举,它的值有:PriviousData、TemplatedParent、Self和FindAncestor。RelativeSource还有3个静态属性:PriviousData、Self、TemplatedParent,它们的类型是RelativeSource类。实际上这3个静态属性就是创建一个RelativeSource的实例、把实例的Mode设置为相相应的值,然后返回这个实例。之所以准备这3个静态属性是为了在XAML代码里面直接获取RelativeSource实例。
在DataTemplate中经经常使用到这这3个静态属性,学习DataTemplate的时候请留意它们的用法。
1.4 binding对数据的转换和校验
前面我们已经知道Binding的作用就是架在Source和Target之间的桥梁,数据能够在这座桥梁的帮助下来流通。就像现实社会中桥梁须要设置安检和关卡一样,Binding这座桥上也能够设置关卡对数据进行验证,不仅如此,假设Binding两端须要不同的数据类型的时候我们还能够为数据设置转换器。
Binding用于数据有效性校验的关卡是他的ValidationRules属性,用于数据类型转换的关卡是它的Convert属性。
1.4.1 Binding的数据校验
Binding的ValidationRules属性是Collection<ValidationRule>,从它的名称和数据类型我们能够得知能够为每个Binding设置多个数据校验条件,每个条件是一个ValidationRule对象。ValidationRule是一个抽象类,在使用的时候我们须要创建它的派生类并实现它的Validate方法的返回值是ValidateionResult类型对象,假设通过验证,就把ValidateionResult对象的IsValidate属性设为true,反之,则须要将IsValidate设置为false并为其ErrorContent属性设置一个合适的消息内容(通常是字符串)。
以下这个程序的UI绘制一个TextBox和一个Slider,然后在后台C#代码中建立Binding把它们关联起来---- 已Slide为源,TextBox为目标。Slider的取值范围是0~100,也就是说我们须要验证TextBox中输入的值是不是在0~100之间。
程序的XAML部分例如以下:
<Window x:Class="WpfApplication1.Window16" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window16" Height="300" Width="300"> <StackPanel Background="AliceBlue" Margin="10"> <TextBox Height="23" Name="textBox1" Width="200" Margin="20"/> <Slider Height="23" Name="slider1" Width="219" Maximum="100" /> </StackPanel> </Window>为了进行校验,我们准备一个ValidationRule的派生类,内容例如以下:
public class RangeValidationRule : ValidationRule { public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo) { double d = 0; if(double.TryParse(value.ToString(),out d)) { if(d>=0&&d<=100) { return new ValidationResult(true,null); } } return new ValidationResult(false,"ErrorContent"); } }
public Window16() { InitializeComponent(); Binding bind =new Binding("Value") { UpdateSourceTrigger= UpdateSourceTrigger.PropertyChanged,Source=slider1, Mode= BindingMode.TwoWay}; ValidationRule rule = new RangeValidationRule(); rule.ValidatesOnTargetUpdated = true; bind.ValidationRules.Add(rule); this.textBox1.SetBinding(TextBox.TextProperty, bind); }完毕后执行程序,当输入0~100之间的值的时候程序正常显示,可是输入区间之外的值的时候TextBox会显示为红色边框,表示值是错误的,不能传值给Source。效果例如以下图:
先把Silider的取值范围从0~100改为-100~200:
你或许回忆,在验证错误的时候,ValidationResult会携带一条错误信息,那么怎样使用这条错误信息呢?想要用到这一点,须要用到后面会具体解说到的知识-----路由事件(Routed Event)。
首先在创建Binding 的时候要把Binding的对象的NotifyOnValidationError属性设置为true,这样,当数据校验失败的时候Binding就像报警器一样发出一个信号。这个信号会在已Binding对象的Target为起点的UI树上进行传播。信号没到达一个节点,假设这个节点设置了对这样的信号的侦听器(事件处理器),那么这个侦听器就会被触发并处理这个信号,信号处理完成后,还能够是否让信号继续沿着UI树向上传播---这就是路由事件。信号在UI树上传递的过程称为路由(Route)。
建立Binding的代码例如以下:
public Window16() { InitializeComponent(); Binding bind =new Binding("Value") { UpdateSourceTrigger= UpdateSourceTrigger.PropertyChanged,Source=slider1, Mode= BindingMode.TwoWay}; ValidationRule rule = new RangeValidationRule(); rule.ValidatesOnTargetUpdated = true; bind.ValidationRules.Add(rule); bind.NotifyOnValidationError = true; this.textBox1.SetBinding(TextBox.TextProperty, bind); this.textBox1.AddHandler(Validation.ErrorEvent, new RoutedEventHandler(ValidationError)); }
private void ValidationError(object sender, RoutedEventArgs e) { if (Validation.GetErrors(textBox1).Count > 0) { this.textBox1.ToolTip = Validation.GetErrors(textBox1)[0].ErrorContent.ToString(); } else { this.textBox1.ToolTip = ""; } }
1.4.2 Binding的数据转换
前面的非常多样例我们都在使用Binding将TextBox和Slider之间建立关联----Slider控件作为Source(Path的Value属性),TextBox作为Target(目标属性为Text)。不知道大家有没有注意到,Slider的Value属性是Double类型值,而TextBox的Text属性是string类型的值,在C#这样的强类型语言中却能够来往自如,是怎么回事呢?
原来Binding还有第二种机制称为数据转换,当Source端指定的Path属性值和Target端指定的目标属性不一致的时候,我们能够加入数据转换器(DataConvert)。上面我们提到的问题实际上就是double和stirng类型相互转换的问题,由于处理起来比較简单,所以WPF类库就自己帮我们做了,但有些数据类型转换就不是WPF能帮我们做的了,比如以下的这样的情况:
当遇到这些情况,我们仅仅能自己动手写Converter,方法是创建一个类并让这个类实现IValueConverter接口,IValueConverter定义例如以下:
public interface IValueConverter { object Convert(object value, Type targetType, object parameters, CultureInfo culture); object ConvertBack(object value, Type targetType, object parameters, CultureInfo culture); }当数据从Binding的Source流向Target的时候,Convert方法将被调用;反之ConvertBack将被调用。这两个方法的參数列表一模一样:第一个參数为Object。最大限度的保证了Convert的重要性。第二个參数用于确定返回參数的返回类型。第三个參数为了将额外的參数传入方法,若须要传递多个信息,则须要将信息做为一个集合传入就可以。
Binding对象的Mode属性将影响这两个方法的调用;假设Mode为TwoWay或Default行为与TwoWay一致则两个方法都有可能被调用。假设Mode是OneWay或者Default行为与OneWay一致则仅仅有Convert方法会被调用。其他情况同理。
以下这个样例是一个Converter的综合实例,程序的用途是向玩家显示一些军用飞机的状态信息。
首先创建几个自己定义数据类型:
public enum Category { Bomber, Fighter } public enum State { Available, Locked, Unknown } public class Plane { public Category category { get; set; } public State state { get; set; } public string name { get; set; } }
同一时候飞机的State属性在UI里面被映射为CheckBox。由于存在以上两种映射关系。我们须要提供两个Converter:一个有Categroy类型单向转换为string类型(XAML会把string解析为图片资源),还有一个是State和bool类型直接的双向转换。代码例如以下:
public class CategoryToSourceConverter:IValueConverter { public object Convert(object value, Type targetType, object parameters, System.Globalization.CultureInfo culture) { Category category = (Category)value; switch (category) { case Category.Bomber: return @"ICONS/Bomber.png"; case Category.Fighter: return @"ICONS/Fighter.png"; default: return null; } } public object ConvertBack(object value, Type targetType, object parameters, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } }
public class StateToNullableBoolConverter:IValueConverter { public object Convert(object value, Type targetType, object parameters, System.Globalization.CultureInfo culture) { State state = (State)value; switch (state) { case State.Available: return true; case State.Locked: return false; case State.Unknown: default: return null; } } public object ConvertBack(object value, Type targetType, object parameters, System.Globalization.CultureInfo culture) { bool? nb = (bool?)value; switch (nb) { case true: return State.Available; case false: return State.Locked; case null: default: return State.Unknown; } } }
<Window x:Class="WpfApplication1.Window17" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfApplication1.BLL" Title="Window17" Height="327" Width="460"> <Window.Resources> <local:CategoryToSourceConverter x:Key="cts" /> <local:StateToNullableBoolConverter x:Key="snb" /> </Window.Resources> <StackPanel Name="stackPanel1" Background="AliceBlue" Margin="10"> <ListBox Name="listBox1" Height="160" Margin="5"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <Image Height="16" Name="image1" Stretch="Fill" Width="16" Source="{Binding Path=category,Converter={StaticResource cts}}"/> <TextBlock Height="23" Name="textBlock1" Text="{Binding name}" Margin="8,0" Width="80"/> <CheckBox Height="16" Name="checkBox1" IsChecked="{Binding Path=state,Converter={StaticResource snb}}" IsThreeState="True"/> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <Button Content="Load" Height="23" Name="button1" Width="131" Margin="5" Click="button1_Click" /> <Button Content="Save" Height="23" Name="button2" Width="131" Margin="5" Click="button2_Click" /> </StackPanel> </Window>Load按钮的事件处理器负责把一组飞机的数据赋值给ListBox的ItemSource属性,Save的Click事件负责把用户改动过的数据写入文件:
/// <summary> /// Load按钮事件处理器 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void button1_Click(object sender, RoutedEventArgs e) { List<Plane> infos = new List<Plane>() { new Plane(){ category= Category.Bomber,name="B-1", state= State.Unknown}, new Plane(){ category= Category.Bomber,name="B-2", state= State.Unknown}, new Plane(){ category= Category.Fighter,name="F-22", state= State.Locked}, new Plane(){ category= Category.Fighter,name="Su-47", state= State.Unknown}, new Plane(){ category= Category.Bomber,name="B-52", state= State.Available}, new Plane(){ category= Category.Fighter,name="J-10", state= State.Unknown}, }; this.listBox1.ItemsSource = infos; } /// <summary> /// Save按钮事件处理器 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void button2_Click(object sender, RoutedEventArgs e) { StringBuilder sb = new StringBuilder(); foreach (Plane item in listBox1.Items) { sb.AppendLine(string.Format("Categroy={0},State={1},Name={2}",item.category,item.state,item.name)); } File.WriteAllText(@"D:\PlaneList.text",sb.ToString()); }
单击Save后打开D:\\PlaneList.text数据例如以下图:
1.5 MultiBinding(多路Binding)
有时候UI须要显示的数据来源不止一个数据来源决定,这个时候就须要用到MultiBinding,即多路绑定。MultiBinding与Binding一样均以BindingBase为基类,也就是说,凡是能用Binding的场合都能使用MultiBinding。MutiBinding具有一个Bindings的属性,其类型是Connection<BindingBase>,通过这个属性,MultiBinding把一组Binding对象聚合起来,处在这个Binding结合中的对象能够拥有自己的数据校验和转换机制。它们汇集起来的数据将共同决定传往MultiBinding目标的数据。例如以下图:
考虑这样一个需求,有一个用于新用户注冊的UI(4个TextBox和一个Button),还有例如以下一些限定:
此UI的XAML代码例如以下:
<Window x:Class="WpfApplication1.Window18" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window18" Height="300" Width="300"> <StackPanel Name="stackPanel1" Margin="10" Background="AliceBlue"> <TextBox Height="23" Name="textBox1" Margin="5" /> <TextBox Height="23" Name="textBox2" Margin="5" /> <TextBox Height="23" Name="textBox3" Margin="5" /> <TextBox Height="23" Name="textBox4" Margin="5" /> <Button Content="Regist" Height="23" Name="btnSubmit" Width="75" Margin="10"/> </StackPanel> </Window>
public Window18() { InitializeComponent(); SetBinding(); } private void SetBinding() { //准备基础Binding Binding bind1 = new Binding("Text") { Source=textBox1}; Binding bind2 = new Binding("Text") { Source = textBox2 }; Binding bind3 = new Binding("Text") { Source = textBox3 }; Binding bind4 = new Binding("Text") { Source = textBox4 }; //准备MultiBinding MultiBinding mb = new MultiBinding() { Mode= BindingMode.OneWay}; mb.Bindings.Add(bind1);//注意,MultiBinding对子元素的顺序是非常敏感的。 mb.Bindings.Add(bind2); mb.Bindings.Add(bind3); mb.Bindings.Add(bind4); mb.Converter = new MultiBindingConverter(); ///将Binding和MultyBinding关联 this.btnSubmit.SetBinding(Button.IsVisibleProperty, mb); }
本例的Converter代码例如以下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows.Data; namespace WpfApplication1.BLL { public class MultiBindingConverter:IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if(!values.Cast<string>().Any(text=>string.IsNullOrEmpty(text))&&values[0].ToString()==values[1].ToString()&&values[3].ToString()==values[4].ToString()) { return true; } return false; } /// <summary> /// 该方法不会被调用 /// </summary> /// <param name="value"></param> /// <param name="targetTypes"></param> /// <param name="parameter"></param> /// <param name="culture"></param> /// <returns></returns> public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } } }程序执行效果如图:
小结:
WPF的核心理念是变传统的UI驱动数据变成数据驱动UI,支撑这个理念的基础就是本章讲的Data Binding和与之相关的数据校验和数据转换。在使用Binding的时候,最重要的就是设置它的源和路径。
Data Binding到此解说完成。
转载请注明出处:http://blog.csdn.net/fwj380891124
WPF之Binding深入探讨,布布扣,bubuko.com
标签:des winform style blog http color
原文地址:http://www.cnblogs.com/zfyouxi/p/3820063.html