为Binding指定源的方法
- 把普通的CLR类型单个对象指定为Source
包括.NET Framework自带类型的对象和用户自定义类型的对象。如果类型实现了INotifyPropertyChanged接口,则可通过在属性的Set语句里出发PropertyChanged事件来通知Binding来更新数据。具体例子参考这里。
- 把普通的CLR集合类型对象指定为Source
包括数组、List<T>、ObservableCollection<T>等集合类型。实际工作中,我们经常需要把一个集合作为ItemContorl派生类的数据源来使用,一般是把控件的ItemSource属性使用Binding关联到一个集合对象上。
WPF的列表式控件都派生自ItemControl,自然也都集成类ItemSource属性。ItemSource可以接收一个IEnumerable接口派生类的实例作为自己的值。只要我们为一个ItemControl对象设置了ItemSource属性,ItemControl对象就会自动迭代其中的数据元素、为每个数据元素准备一个条目容器,并使用Binding在条目容器与数据元素之间建立关联。
比如我们要把一个List<Student>集合作为ListBox的ItemSource,让ListBox显示Student的Name,UI代码如下:
<Window x:Class="WpfApplication2.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="175" Width="225" Loaded="Window_Loaded"> <StackPanel> <ListBox x:Name="listBox" Height="120" Margin="5"></ListBox> </StackPanel> </Window>
在Load里设置Binding,代码如下:
private void Window_Loaded(object sender, RoutedEventArgs e) { //准备数据 List<Student> Students = new List<Student>() { new Student(){Id=1,Name="Tim",Age=19}, new Student(){Id=2,Name="Tom",Age=18}, new Student(){Id=3,Name="Kate",Age=17}, }; //为ListBox设置Binding this.listBox.ItemsSource = Students; this.listBox.DisplayMemberPath = "Name"; }
- 把ADO.NET数据对象指定为Source
在.NET开发中,我们使用ADO.NET类对数据库进行操作。常见的工作是从数据库中把数据读取到DataTable中,再把DataTable显示在UI列表控件中。一般都是通过Linq等手段把DataTalbe里的数据转换成恰当的用户自定义类型集合,但WPF中支持在列表控件和DataTable之间直接建立Binding。
假设我们已经获得了一个DataTable的实例,里面保存了Student的多个对象,现在我们要把它显示在ListBox中,代码就可以这样写:
private void Button_Click(object sender, RoutedEventArgs e) { DataTable dt=GetDataFromDb(); this.listBox.DisplayMemberPath = "Name"; this.listBox.ItemsSource = dt.DefaultView; }
DataTable的DefaultView属性是一个DataView类型的对象,DataView实现了IEnumeralbe接口,所以可以被赋值给ItemSource属性。
- 使用XmlDataProvider把XML数据指定为Source
假设我们有一组Students信息保存在StuData.xml中,信息内容如下:
<?xml version="1.0" encoding="utf-8" ?> <StudentList> <Student Id="1"> <Name>Tim</Name> </Student> <Student Id="2"> <Name>Tom</Name> </Student> <Student Id="3"> <Name>Kate</Name> </Student> </StudentList>
我们要把它显示在一个ListView里,UI代码如下:
<Window x:Class="WpfApplication2.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="230" Width="220" Loaded="Window_Loaded"> <StackPanel> <ListView x:Name="listView" Height="150" Margin="5"> <ListView.View> <GridView> <GridViewColumn Header="Id" Width="80" DisplayMemberBinding="{Binding XPath=@Id}"></GridViewColumn> <GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding XPath=Name}"></GridViewColumn> </GridView> </ListView.View> </ListView> <Button Content="Load" Margin="5" Click="Button_Click"></Button> </StackPanel> </Window>
其中DisplayMemberBinding="{Binding XPath=@Id}"和 DisplayMemberBinding="{Binding XPath=Name}",分别为GridView的两列指明了XML的路径,使用@符号加字符串表示的是XML元素的Attribute,不加@符号的字符串表示的是子级元素。
Button的Click事件处理如下:
private void Button_Click(object sender, RoutedEventArgs e) { XmlDocument doc = new XmlDocument(); doc.Load(".\\StuData.xml"); XmlDataProvider xdp = new XmlDataProvider(); xdp.Document = doc; xdp.XPath = @"/StudentList/Student"; this.listView.DataContext = xdp; this.listView.SetBinding(ListView.ItemsSourceProperty, new Binding()); }
XmlDataProvider还有一个Source属性,可以用它直接指定XML文档所在的位置,所以Button的Click事件处理也可以这样写:
XmlDataProvider xdp = new XmlDataProvider(); xdp.Source = new Uri("D:\\StuData.xml"); xdp.XPath = @"/StudentList/Student"; this.listView.DataContext = xdp; this.listView.SetBinding(ListView.ItemsSourceProperty, new Binding());
- 把依赖对象(Dependency Object)指定为Source
- 把容器的DataContext指定为Source
DataContext被定义在FrameworkElement类里,这个类是WPF控件的基类,所以所有的WPF控件都有这个属性,而且这是一个依赖属性,依赖属性有一个很重要的特点就是当你没有为控件的某个依赖属性显示赋值时,控件会把自己容器的属性值借过来当作自己的属性值。实际上属性值沿着UI元素树向下传递了。
在实际工作中,DataContext的使用是很灵活的,不如:
- 当UI上的多个控件都使用Binding关注同一个对象时,可以使用DataContext;
- 当作为Source的对象不能被直接访问的时候,比如B窗体内的控件想把A窗体内的控件当作自己的Binding源时,但A窗体内的控件是Private的,这时候就可以把这个控件或控件的值作为A窗体的DataContext从而暴露数据。
- 通过ElementName指定Source
- 通过Binding的RelativeSource属性相对的指定Source
有些时候我们不能确定作为Source的对象叫什么名字,但直到它与作为Binding目标的对象在UI布局上有相对关系,比如控件自己关联自己的某个数据、关联自己某级容器的数据。这个时候就可以使用Binding的RelativeSource属性。
我们用Grid、DockPanel和TextBox做一个多层布局,XAML代码如下:
<Window x:Class="WpfApplication2.Window2" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window2" Height="300" 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="textBox" FontSize="24" Margin="10"></TextBox> </DockPanel> </Grid> </DockPanel> </Grid> </Window>
我们把TextBox的Text属性关联到外层容器的Name属性上,在窗体的构造函数里添加如下代码:
RelativeSource rs = new RelativeSource(RelativeSourceMode.FindAncestor); rs.AncestorLevel = 1; rs.AncestorType = typeof(DockPanel); Binding binding = new Binding("Name") { RelativeSource = rs }; this.textBox.SetBinding(TextBox.TextProperty, binding);
AncestorLevel属性指的是以Binding目标控件为起点的层级偏移量----d2的偏移量是1,g2的偏移量是2,以此类推。
AncestorType属性告诉Binding寻找哪个类型的对象作为自己的源,不是这个类型的对象会被跳过。
注意:Binding的构造函数里指定源时使用的RelativeSource,而不是Source。
之上的C#代码也可以在XAML中实现,代码如下:
<TextBox x:Name="textBox" Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type Grid}, AncestorLevel=1},Path=Name}" FontSize="24" Margin="10"></TextBox>
- 把ObjectDataProvider对象指定为Source
在实际中很难保证一个类的所有数据都使用属性暴露出来,比如我们需要的数据可能是方法的返回值,这种情况下就需要用ObjectDataProvider来包装作为Binding源的数据对象了。
现在我们有一个计算类Calculator,具有加减乘除的方法,代码如下:
public class Calculator { public string Add(string a,string b) { double x=0, y=0, z=0; if(double.TryParse(a,out x) && double.TryParse(b,out y)) { z = x + y; return z.ToString(); } return "Input Error"; } //其他方法省略 }
我们用三个TextBox分别表示两个参数和运算结果,XAML代码如下:
<Window x:Class="WpfApplication2.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="120" Width="200"> <StackPanel> <TextBox x:Name="t1" Margin="5"></TextBox> <TextBox x:Name="t2" Margin="5"></TextBox> <TextBox x:Name="t3" Margin="5"></TextBox> </StackPanel> </Window>
然后设置Binding,代码如下:
public Window1() { InitializeComponent(); SetBinding(); } private void SetBinding() { //创建并配置ObjectDataPrivoder对象 ObjectDataProvider odp = new ObjectDataProvider(); odp.ObjectInstance = new Calculator(); odp.MethodName = "Add"; odp.MethodParameters.Add("0"); odp.MethodParameters.Add("0"); //创建Binding Binding bindingToArg1 = new Binding("MethodParameters[0]") { Source = odp, BindsDirectlyToSource = true, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged, }; Binding bindingToArg2 = new Binding("MethodParameters[1]") { Source = odp, BindsDirectlyToSource = true, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged, }; Binding bindingToResult = new Binding(".") { Source = odp }; //把Binding关联到控件 this.t1.SetBinding(TextBox.TextProperty, bindingToArg1); this.t2.SetBinding(TextBox.TextProperty, bindingToArg2); this.t3.SetBinding(TextBox.TextProperty, bindingToResult); }
以第一个Binding为例,它的Source是ObjectDataProvidor对象、Path是ObjectDataProvider对象的MethodParameters属性所引用集合的第一个元素。BindsDirectlyToSource=true这句的意思是告诉Binding对象只负责把从UI收集到的数据写入其直接Source而不是被ObjectDataProvider对象包装着的Calculator对象。
- 把使用Linq检索得到的数据对象作为Binding的源
使用Linq可以方便的操作集合对象、DataTable对象和XML对象,避免类使用好几层嵌套循环。Linq查询的结果是一个IEnumerable<T>类型的对象,而IEnumerable<T>又派生自IEmumerable,所以它可以作为列表类控件的ItemSource使用。
比如我们要在ListView里显示Student信息,假设Student信息保存在List内,我们要显示所有名字以T开头的学生,代码如下:
private void Window_Loaded(object sender, RoutedEventArgs e) { //准备数据 List<Student> Students = new List<Student>() { new Student(){Id=1,Name="Tim",Age=19}, new Student(){Id=2,Name="Tom",Age=18}, new Student(){Id=3,Name="Kate",Age=17}, }; this.listView.ItemsSource = from stu in Students where stu.Name.StartsWith("T") select stu; }
如果数据存放在一个一经填充好的DataTable对象里,则代码如下:
private void Button_Click(object sender, RoutedEventArgs e) { DataTable dt = this.GetDataFromDb(); this.listView.ItemsSource = from row in dt.Rows.Cast<DataRow>() where Convert.ToString(row["Name"]).StartsWith("T") select new Student() { Id = int.Parse(row["Id"].ToString()), Name = row["Name"].ToString(), Age = int.Parse(row["Age"].ToString()), }; }
如果数据存储在XML文件里,则代码如下:
XDocument xdoc = XDocument.Load(".\\StuData.xml"); this.listView.ItemsSource = from element in xdoc.Descendants("Student") where element.Attribute("Name").Value.StartsWith("T") select new Student() { Id=int.Parse(element.Attribute("Id").Value), Name=element.Attribute("Name").Value, Age=int.Parse(element.Attribute("Age").Value), };