Binding的源是数据的来源,所以,只要一个对象包含数据并且能够通过属性将数据暴露出来,它就能当作Binding的源来使用。包含数据的对象比比皆是,但必须为Binding的Source指定合适的对象Binding才能正常工作。
1.1 没有Source的Binding----使用DataContext作为数据源
在UI树的每个节点都有DataContext属性。Binding怎么会自动向UI元素上一层查找DataContext并把它作为自己的Source呢?其实,“Binding沿着UI元素树向上找”只是WPF给我们的一个错觉,Binding并没有那么智能。之所以会这样是因为DataContext是一个“依赖属性”,依赖属性有一个很明显的特点就是你没有为某个控件的依赖属性赋值的时候,控件会把自己容器的属性值接过来当作自己的属性值。实际上属性值是沿着UI元素树向下传递的。
让我们看看下面的例子:
C#:
先创建一个名为Student的类,它具有ID,Name属性:
public class Student2 { public int Id { set; get; } public string Name { set; get; } } /// <summary> /// wnd636.xaml 的交互逻辑 /// </summary> public partial class wnd636 : Window { public wnd636() { InitializeComponent(); } private void _btnOK_Click(object sender, RoutedEventArgs e) { MessageBox.Show(_btnOK.DataContext.ToString()); } }
XAML:
<Window x:Class="WpfApplication6.wnd636" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfApplication6" Title="wnd636" Height="130" Width="300"> <StackPanel> <StackPanel.DataContext> <local:Student2 Id="36" Name="wndTom"/> </StackPanel.DataContext> <Grid > <StackPanel> <!--没有指定Source,使用DataContext--> <TextBox Text="{Binding Path=Id}" Margin="5" BorderBrush="Black"/> <TextBox Text="{Binding Path=Name}" Margin="5" BorderBrush="Black"/> <Grid DataContext="button33"> <Grid> <!--没有指定Button的DataContext,则会使用Grid的。向下传递!--> <Button x:Name="_btnOK" Margin="5" Height="22" Click="_btnOK_Click"/> </Grid> </Grid> </StackPanel> </Grid> </StackPanel> </Window>
外层StackPanel的DataContext进行了赋值----它是一个Student对象。2个TextBox通过外层StackPanel的DataContext进行了赋值----它是一个Student对象。2个TextBox通过Binding获取值,但只为Binding指定了Path,没有指定Source。
在实际工作中,DataContext属性值的运用非常的灵活。比如:
当UI上的多个控件都使用Binding关注同一个对象变化的时候,不妨使用DataContext。当作为Source的对象不能被直接访问的时候----比如B窗体内的控件想把A窗体里的控件当作自己的Binding源时,但是A窗体内的控件可访问级别是private类型,这是就可以把这个控件或者控件值作为窗体A的DataContext(这个属性是Public级别的)这样就可以暴露数据了。
形象的说,这时候外层的数据就相当于一个数据的“至高点”,只要把元素放上去,别人就能够看见。另外DataContext本身就是一个依赖属性,我们可以使用Binding把它关联到一个数据源上。
1.2 使用集合对象作为列表控件的ItemsSource
WPF中的列表式控件都派生自ItemControl类,自然也继承了ItemSource这个属性。ItemSource可以接收一个IEnumerable接口派生类的实例作为自己的值(所有可被迭代遍历的集合都实现了这个接口,包括数组、List<T>等)。每个ItemControl都具有自己的条目容器Item Container,例如,ListBox的条目容器是ListBoxItem、Combox的条目容器是ComboxItem。ItemSource里面保存的是一条一条的数据,想要把数据显示出来就要为数据穿上外衣,条目容器就起到了数据外衣的作用。这样将数据外衣和它所对应的条目容器关联起来呢?当然时依靠Binding!只要我们为一个ItemControl设置了ItemSource属性值,ItemControl会自动迭代其中的数据元素,为每个数据元素准备一个条目容器,并使用Binding元素在条目容器和数据元素之间建立起关联。
我们看看下面的例子:
XAML:
<Window x:Class="WpfApplication6.wnd637" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="wnd637" Height="200" Width="300"> <StackPanel Margin="0,10,0,0"> <TextBox x:Name="_txtBox" Height="23" /> <ListView x:Name="_listView" Margin="5"> <ListView.View> <GridView> <GridViewColumn Header="Id" Width="60" DisplayMemberBinding="{Binding Id}" /> <GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" /> </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的内容部分定义2个<GridViewColumn>对象,GridViewColumn中最重要的一个属性是DisplayBinding(类型是BindingBase),使用这个属性可以指定这一列使用什么样的Binding去关联数据------这与ListBox有点不同,ListBox使用的是DisplayMemberPath属性(类型是string)。如果想用更复杂的结构来表示这一标题或数据,则可为GridViewColumn设置HeadTemplate和CellTemplate,它们的类型都是DataTemplate。
C#:
// 在添加项、移除项或刷新整个列表时,此集合将提供通知。 // 一般不实用List等 ObservableCollection<Student2> stuList = new ObservableCollection<Student2>() { new Student2(){Id = 1, Name="Tim"}, new Student2(){Id = 2, Name="Job"}, new Student2(){Id = 3, Name="Jack"}, new Student2(){Id = 4, Name="Toky"}, }; _listView.ItemsSource = stuList; // 可以使用LINQ语言查询 //_listView.ItemsSource = from stu in stuList where stu.Name.StartsWith("T") select stu; // 显示选择的ID _txtBox.SetBinding(TextBox.TextProperty, new Binding("SelectedItem.Id") { Source = _listView , Mode = BindingMode.OneWay, });

在使用集合类型的数据作为列表控件的ItemSource时一般会考虑使用ObservableCollection<T>替换List<T>,因为ObservableCollection<T>类实现了INotifyChange和INotifyPropertyChanged接口,能把集合的变化立刻通知显示到它的列表控件上,改变会立刻显示出来。
1.3 使用XML数据作为Binding的源
在使用XML数据作为Binding的Source的时候我们将使用XPath属性而不是Path属性来指定数据的来源。
下面的XML文本是一组文本信息,我们要把它显示在一个ListView控件里:
<?xml version="1.0" encoding="utf-8" ?> <StudentList> <Student Id ="1"> <Name>Tom</Name> </Student> <Student Id ="2"> <Name>Jack</Name> </Student> <Student Id ="3"> <Name>Joe</Name> </Student> </StudentList>
XAML:
<Window x:Class="WpfApplication6.wnd639" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="wnd639" Height="200" Width="300"> <StackPanel> <ListView x:Name="_listView"> <ListView.View> <GridView> <!--@表示Attribute--> <GridViewColumn Header="Id" Width="80" DisplayMemberBinding="{Binding XPath = @Id}"/> <!--表示子集元素--> <GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding XPath = Name}"/> </GridView> </ListView.View> </ListView> </StackPanel> </Window>
C#:
// 将XML赋值给它 XmlDataProvider xdp = new XmlDataProvider(); xdp.Source = new Uri(@"E:\RefCode\C#\WPF\深入浅出WPF\第六章Binding\WpfApplication6\WpfApplication6\stuData.xml"); xdp.XPath = @"StudentList/Student"; _listView.DataContext = xdp; // 自动查找Source _listView.SetBinding(ListView.ItemsSourceProperty, new Binding()); // 也可以使用LINQ查询 //XDocument xDoc = XDocument.Load(@"E:\RefCode\C#\WPF\深入浅出WPF\第六章Binding\WpfApplication6\WpfApplication6\stuData.xml"); //_listView.ItemsSource = from ele in xDoc.Descendants("Student") // where ele.Element("Name").Value.StartsWith("T") // select new Student() // { // Id = int.Parse(ele.Attribute("Id").Value), // Name = ele.Element("Name").Value, // };

XAML最关键的两句:DisplayMemberBinding="{Binding XPath=@id}"和DisplayMemberBinding="{Binding XPath=Name}",他们分别为GridView两列指定了要关注的XML路径----很明显,使用@符号加字符串表示的是XML元素的Attribute,不加@符号表示的是子级元素。
1.4 使用ObjectDataProvider作为binding的Source
理想情况下,上游程序员将类设计好、使用属性把数据暴露出来,下游程序员将这些类作为Binding的Source、把属性作为Binding的Path来消费这些类。但很难保证一个类的属性都用属性暴露出来,比如我们需要的数据可能是方法的返回值。而重新设计底层类的风险和成本会比较高,况且黑盒引用类库的情况下我们不可能更改已经编译好的类,这时候需要使用ObjectDataProvider来包装做为Binding源的数据对象了。ObjcetDataProvider 顾名思义就是把对象作为数据源提供给Binding。
ObjectDataProvider对象和它被包装的对象关系如下图:
了解了ObjectDataProvider的使用方法,我们看看如何把它当作Binding的Source来使用,这个程序实现的功能是,我在前两个TextBox里面输入值的时候,第三个TextBox会显示前两个文本框里面相加之和。
XAML:
<Window x:Class="WpfApplication6.wnd6311" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="wnd611" Height="200" Width="300"> <StackPanel> <TextBox x:Name="_txtBox1" Margin="5" /> <TextBox x:Name="_txtBox2" Margin="5" /> <TextBox x:Name="_txtBox3" Margin="5" /> </StackPanel> </Window>
C#:
public class Calculate { public string Add(string str1, string str2) { double val1 = 0; double val2 = 0; if(double.TryParse(str1, out val1) && double.TryParse(str2, out val2)) { return (val1 + val2).ToString(); } return("Error"); } } /// <summary> /// wnd6311.xaml 的交互逻辑 /// </summary> public partial class wnd6311 : Window { public wnd6311() { InitializeComponent(); ObjectDataProvider odp = new ObjectDataProvider(); odp.ObjectInstance = new Calculate(); odp.MethodName = "Add"; // MethodParameters是类型敏感的,相当于告诉参数为两个string类型 odp.MethodParameters.Add("0"); odp.MethodParameters.Add("0"); _txtBox1.SetBinding(TextBox.TextProperty, new Binding("MethodParameters[0]") { Source = odp, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged, BindsDirectlyToSource = true, //Mode = BindingMode.OneWayToSource, // MethodParameters为非依赖属性 }); _txtBox2.SetBinding(TextBox.TextProperty, new Binding("MethodParameters[1]") { Source = odp, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged, BindsDirectlyToSource = true, //Mode = BindingMode.OneWayToSource, // MethodParameters为非依赖属性 }); _txtBox3.SetBinding(TextBox.TextProperty, new Binding(".") { Source = odp, }); } }

一般情况下数据从那里来,哪里就是Binding的Source,数据到哪里去,哪里就是Binding 的Target。按这个理论,前两个TextBox应该是ObjcetDataProvider的源,而ObjcetDataProvider对象又是最后一个TextBox的源。但实际上,三个TextBox都以ObjcetDataProvider作为数据源,只是前两个在数据流向上做了限制,这样做的原因不外乎有两个:
1、ObjcetDataProvider的MethodParameter不是依赖属性,不能作为Binding的目标。
2、数据驱动UI理念要求我们尽可能的使用数据对象作为Binding的Source而把UI当做Binding的Target。
1.5 使用Binding的RelativeSource
当一个Binding有明确的来源的时候,我们可以通过Source或者ElementName赋值的办法让Binding与之关联。有些时候我们不能确定作为Source对象叫什么名字,但是我们知道它与做为Binding目标对象在UI上的相对关系,比如控件自己关联自己的某个数据,关联自己某级容器的数据,这时候就需要用到Binding的RelativeSource属性。
RelativeSource属性的类型是RelativeSource类,通过这个类的几个静态或者非静态的属性我们可以控制它搜索相对数据源的方式。
看下面的例子:
XAML:
<Window x:Class="WpfApplication6.wnd6312" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="wnd6312" Height="200" Width="300"> <Grid x:Name="_g1" Background="Red" Margin="10"> <DockPanel x:Name="_d1" Background="Orange" Margin="10"> <Grid x:Name="_g2" Background="Yellow" Margin="10"> <DockPanel x:Name="_d2" Background="LawnGreen" Margin="10"> <TextBox x:Name="_t1" Margin="10"/> </DockPanel> </Grid> </DockPanel> </Grid> </Window>
/// <summary> /// wnd6312.xaml 的交互逻辑 /// </summary> public partial class wnd6312 : Window { public wnd6312() { InitializeComponent(); // 绑定具有相对关系的控件 // 绑定上级元素 // 类型为Grid // 层级为1,即上级节点的序号位置 _t1.SetBinding(TextBox.TextProperty, new Binding("Name") { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(Grid), 1), }); // 绑定自身 //_t1.SetBinding(TextBox.TextProperty, new Binding("Name") //{ // RelativeSource = new RelativeSource(RelativeSourceMode.Self), //}); } }
AncestorLevel属性指定的是以Binding目标控件为起点的层级偏移量---gd2的偏移量是1,gd2的偏移量是2,依次类推。AncestorType属性告诉Binding去找什么类型的对象作为自己的源,不是这个类型的对象会被跳过。上面这段代码的意思是告诉Binding从自己的第一层依次向外找,找到第一个Grid类型对象后把它当作自己的源。
参考:《深入浅出WPF》