.NET: WPF Data Binding

WPF是分离UI和Logic的最佳工具,不同于Window Form的事件驱动原理,WPF采用的是数据驱动,让UI成为了Logic的附属,达到分离的效果。

本篇主要讲讲wpf的精华:data binding

可以把binding看做数据的桥梁,两端分别是source和target,一般来说,source是logic的对象,target是UI的控件对象。

(一)简单的例子

在一个界面里有一个textbox和button,按button后textbox会不停地在Student类的实例的Name属性上增加“Name”,而TextBox显示该实例的Name属性

Student类:

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using System.ComponentModel;

 namespace WpfApplication1
 {
     class Student : INotifyPropertyChanged
     {
         public event PropertyChangedEventHandler PropertyChanged;

         private string name;

         public string Name
         {
             get { return name; }
             set {
                 name = value;
                 if (this.PropertyChanged != null)
                 {
                     this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Name"));
                 }
             }
         }
     }
 }

MainWindow.xmal:

 <Window x:Class="WpfApplication1.MainWindow"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         Title=">
     <StackPanel>
         <TextBox x:Name="/>
         <Button Content=" Click="Button_Click"/>
     </StackPanel>
 </Window>

MainWindow.xmal.cs:

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Data;
 using System.Windows.Documents;
 using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using System.Windows.Navigation;
 using System.Windows.Shapes;

 namespace WpfApplication1
 {
     /// <summary>
     /// Interaction logic for MainWindow.xaml
     /// </summary>
     public partial class MainWindow : Window
     {
         Student stu;
         public MainWindow()
         {
             InitializeComponent();

             stu = new Student();
             Binding binding = new Binding();
             binding.Source = stu;
             binding.Path = new PropertyPath("Name");

             BindingOperations.SetBinding(this.textBoxName, TextBox.TextProperty, binding);
         }

         private void Button_Click(object sender, RoutedEventArgs e)
         {
             stu.Name += "Name";
         }
     }
 }

可以看到在对Student类的Name属性加上PropertyChanged.Invoke函数后就能实现该属性的被绑定了。

MainWindow.xmal只是一个UI,并没有什么Logic部分

而在MainWindow.xmal.cs中,将textBoxName的TextProperty和stu的Name属性进行了绑定,Button_Click事件则只是单单地为Logic了,没有UI的代码,彻彻底底地将UI上数据变化的工作交给了binding自动处理。

从这里可以看出,WPF真的是一个非常牛叉的技术,将UI和Logic完美分离,让UI设计者和业务逻辑者可以并行地开发项目,减少了项目开发时间

当然上面这段代码还可以进行优化

MainWindow.xmal.cs:

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Data;
 using System.Windows.Documents;
 using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using System.Windows.Navigation;
 using System.Windows.Shapes;

 namespace WpfApplication1
 {
     /// <summary>
     /// Interaction logic for MainWindow.xaml
     /// </summary>
     public partial class MainWindow : Window
     {
         Student stu;
         public MainWindow()
         {
             InitializeComponent();

             this.textBoxName.SetBinding(TextBox.TextProperty, new Binding("Name") { Source = stu = new Student() });
         }

         private void Button_Click(object sender, RoutedEventArgs e)
         {
             stu.Name += "Name";
         }
     }
 }

(二)控件作为binding source

上面的例子让一个类的属性作为source,通过让类实现INotifyPropertyChanged接口,并在属性的set语句中激发PropertyChanged事件。

除了以类的属性为source外,还有其他的选择,这里先讲第一种:控件作为源

这个时候可以将binding语句放在xaml文件中

 <Window x:Class="WpfApplication1.MainWindow"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         Title="Simple Binding" Height="110" Width="300">
     <StackPanel x:Name="stackPanel" Background="LightBlue">
         <TextBox x:Name="textBox1" Text="{Binding Path=Value, ElementName=slider1}" BorderBrush="Black" Margin="5" />
         <Slider x:Name="slider1" Maximum="100" Minimum="0" Margin="5" />
     </StackPanel>
 </Window>

这里Text="{Binding Path=Value, ElementName=slider1}"也可以在cs文件里进行绑定

 this.textBox1.SetBinding(TextBox.TextProperty, new Binding("Value") { ElementName = "slider1" });

推荐在xaml里进行绑定,控件作为source它不参与logic部分,因此没必要放入cs文件中

为了能够在textBox1里修改数据后slider1能实时更新(默认是要LostFocus才更新),可以这么写

 <TextBox x:Name="textBox1" Text="{Binding Path=Value, ElementName=slider1, UpdateSourceTrigger=PropertyChanged}" BorderBrush="Black" Margin="5" />

Binding的Path其实就是要绑定的source的具体的属性,这里是slider1的Value。如果要显示一个TextBox的文本长度,则Path=Text.Length,如果是TextBox第4个字符,Path=Text[3].

如果Path="."说明source本身就是数据,xaml里可以省略,cs里不行

(三)没有source的binding--使用DataContext作为Binding的源

有的时候binding的path指定了,source并没有指定,则UI元素树会一层一层往上爬,直到有Path指定的属性为止,如果都没有,则什么都没了

为了在xaml能够访问到cs里写的类,需要加上:xmlns:local="clr-namespace:WpfApplication1"。

先在cs里设计student类

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using System.ComponentModel;

 namespace WpfApplication1
 {
     class Student
     {
         public int Id { get; set; }
         public string Name { get; set; }
         public int Age { get; set; }
     }
 }

再在xaml里创建UI

 <Window x:Class="WpfApplication1.MainWindow"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:local="clr-namespace:WpfApplication1"
         Title="Simple Binding" Height="150" Width="300">
     <StackPanel x:Name="stackPanel" Background="LightBlue">
         <StackPanel.DataContext>
             <local:Student Id="6" Age="29" Name="Tim" />
         </StackPanel.DataContext>
         <Grid>
             <StackPanel>
                 <TextBox Text="{Binding Path=Id}" Margin="5" />
                 <TextBox Text="{Binding Path=Name}" Margin="5" />
                 <TextBox Text="{Binding Path=Age}" Margin="5" />
             </StackPanel>
         </Grid>
     </StackPanel>
 </Window>

如果DataContext是<sys:String>Hello DataContext!</sys:String>这种string时,Path=.,也可以将其省略了。

那么问题来了:啥时候用DataContext? 在这个例子中与其这么binding,还不如(一)里面写的改写Student类,而在xaml里进行简单的binding来得简单,而且这里xaml里需要访问cs的类,UI需要知道logic的类设计,对UI设计者来说也增加了负担,似乎不太合理。DataContext在下面两种情况下回用到

1. 当UI上的多个控件都使用Binding关注同一个对象时,不妨使用DataContext。

2. 当作为Source的对象不能被直接访问的时候,比如B窗体内的控件想把A窗体内的控件当做自己的Binding源时,但A窗体内的控件是private访问级别,这时候就可以把这个控件(或者控件的值)作为窗体A的DataContext(这个属性是public访问级别的)从而暴露数据。

形象地说,这时候外层容器的DataContext就相当于一个数据的“制高点”,只要把数据放上去,别的元素就能看见。另外,DataContext本身也是一个依赖属性,我们可以使用Binding把它关联到一个数据源上。

(四)集合对象为source

还是用上面的Student.cs,xaml为:

 <Window x:Class="WpfApplication1.MainWindow"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:local="clr-namespace:WpfApplication1"
         Title="Simple Binding" Height="240" Width="360">
     <StackPanel x:Name="stackPanel" Background="LightBlue">
         <TextBlock Text="Student ID:" FontWeight="Bold" Margin="5" />
         <TextBox x:Name="textBoxId" Margin="5" />
         <TextBlock Text="Student List:" FontWeight="Bold" Margin="5" />
         <ListBox x:Name="listBoxStudents" Height="110" Margin="5" />
     </StackPanel>
 </Window>

xaml.cs:

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Data;
 using System.Windows.Documents;
 using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using System.Windows.Navigation;
 using System.Windows.Shapes;

 namespace WpfApplication1
 {
     /// <summary>
     /// Interaction logic for MainWindow.xaml
     /// </summary>
     public partial class MainWindow : Window
     {
         public MainWindow()
         {
             InitializeComponent();

             List<Student> stuList = new List<Student>()
             {
                 , Name=},
                 , Name=},
                 , Name=},
                 , Name=},
                 , Name=},
                 , Name=},
             };

             this.listBoxStudents.ItemsSource = stuList;
             this.listBoxStudents.DisplayMemberPath = "Name";
             this.textBoxId.SetBinding(TextBox.TextProperty, new Binding("SelectedItem.Id") { Source = this.listBoxStudents });
         }
     }
 }

如果想要在listBox里显示Id, Name, Age,则要把this.listBoxStudents.DisplayMemberPath = "Name";得删掉,另外在xaml的listBox里设置它的ItemTemplate为特定的DataTemplate,具体如下

 <Window x:Class="WpfApplication1.MainWindow"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         Title="Simple Binding" Height="240" Width="360">
     <StackPanel x:Name="stackPanel" Background="LightBlue">
         <TextBlock Text="Student ID:" FontWeight="Bold" Margin="5" />
         <TextBox x:Name="textBoxId" Margin="5" />
         <TextBlock Text="Student List:" FontWeight="Bold" Margin="5" />
         <ListBox x:Name="listBoxStudents" Height="110" Margin="5">
             <ListBox.ItemTemplate>
                 <DataTemplate>
                     <StackPanel Orientation="Horizontal">
                         <TextBlock Text="{Binding Path=Id}" Width="30"/>
                         <TextBlock Text="{Binding Path=Name}" Width="60"/>
                         <TextBlock Text="{Binding Path=Age}" Width="30"/>
                     </StackPanel>
                 </DataTemplate>
             </ListBox.ItemTemplate>
         </ListBox>
     </StackPanel>
 </Window>

(五)ADO.NET对象为source

可以用listBox控件来显示

xaml:

 <Window x:Class="WpfApplication1.MainWindow"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         Title="Simple Binding" Height="206" Width="250">
     <StackPanel x:Name="stackPanel" Background="LightBlue">
         <ListBox x:Name="listBoxStudents" Height="130" Margin="5" />
         <Button Content="Load" Height="25" Margin="5" Click="Button_Click" />
     </StackPanel>
 </Window>

xaml.cs:

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Data;
 using System.Windows.Documents;
 using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using System.Windows.Navigation;
 using System.Windows.Shapes;
 using System.Data;
 using MySql.Data;
 using MySql.Data.Entity;
 using MySql.Data.MySqlClient;

 namespace WpfApplication1
 {
     /// <summary>
     /// Interaction logic for MainWindow.xaml
     /// </summary>
     public partial class MainWindow : Window
     {
         public MainWindow()
         {
             InitializeComponent();
         }

         private void Button_Click(object sender, RoutedEventArgs e)
         {
             DataTable dt = this.Load();
             this.listBoxStudents.DisplayMemberPath = "Name";
             this.listBoxStudents.ItemsSource = dt.DefaultView;
         }

         private DataTable Load()
         {
             DataTable dt = new DataTable();
             using (MySqlConnection con = new MySqlConnection("server=localhost; database=persons; uid=root; pwd=0000; connect timeout=30; pooling=true"))
             {
                 con.Open();
                 MySqlDataAdapter adapter = new MySqlDataAdapter("SELECT * FROM Student", con);
                 DataSet ds = new DataSet();
                 adapter.Fill(ds, "Student");
                 dt = ds.Tables["Student"];
             }
             return dt;
         }
     }
 }

但是这样显示的内容太少了,多数情况会选择ListView来显示

xaml:

 <Window x:Class="WpfApplication1.MainWindow"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         Title="Simple Binding" Height="206" Width="250">
     <StackPanel x:Name="stackPanel" Background="LightBlue">
         <ListView x:Name="listViewStudents" Height="130" Margin="5">
             <ListView.View>
                 <GridView>
                     <GridViewColumn Header="Id" Width="60" DisplayMemberBinding="{Binding Id}" />
                     <GridViewColumn Header="Name" Width="80" DisplayMemberBinding="{Binding Name}" />
                     <GridViewColumn Header="Age" Width="60" DisplayMemberBinding="{Binding Age}" />
                 </GridView>
             </ListView.View>
         </ListView>
         <Button Content="Load" Height="25" Margin="5" Click="Button_Click" />
     </StackPanel>
 </Window>

在cs文件里注意将this.listBoxStudents.DisplayMemberPath = "Name";删除,this.listBoxStudents.ItemsSource = dt.DefaultView;改为this.listViewStudents.ItemsSource = dt.DefaultView;

(六)XML为source

先写一个Student.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>Vina</Name>
   </Student>
   <Student Id="4">
     <Name>Emily</Name>
   </Student>
 </StudentList>

xaml:

 <Window x:Class="WpfApplication1.MainWindow"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         Title="Simple Binding" Height="206" Width="250">
     <StackPanel x:Name="stackPanel" Background="LightBlue">
         <ListView x:Name="listViewStudents" Height="130" Margin="5">
             <ListView.View>
                 <GridView>
                     <GridViewColumn Header="Name" Width="80" DisplayMemberBinding="{Binding XPath=@Id}" />
                     <GridViewColumn Header="Age" Width="60" DisplayMemberBinding="{Binding XPath=Name}" />
                 </GridView>
             </ListView.View>
         </ListView>
         <Button Content="Load" Height="25" Margin="5" Click="Button_Click" />
     </StackPanel>
 </Window>

这里"{Binding XPath=@Id}"的@表示XML元素的Attribute,而没有@说明是子集元素

xaml.cs:

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Data;
 using System.Windows.Documents;
 using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using System.Windows.Navigation;
 using System.Windows.Shapes;
 using System.Data;
 using MySql.Data;
 using MySql.Data.Entity;
 using MySql.Data.MySqlClient;
 using System.Xml;
 using System.Xml.Linq;

 namespace WpfApplication1
 {
     /// <summary>
     /// Interaction logic for MainWindow.xaml
     /// </summary>
     public partial class MainWindow : Window
     {
         public MainWindow()
         {
             InitializeComponent();
         }

         private void Button_Click(object sender, RoutedEventArgs e)
         {
             XmlDataProvider xdp = new XmlDataProvider();
             xdp.Source = new Uri(@"C:\Users\Administrator\Desktop\Demo\Student.XML");
             xdp.XPath = @"/StudentList/Student";
             this.listViewStudents.DataContext = xdp;
             this.listViewStudents.SetBinding(ListView.ItemsSourceProperty, new Binding());
         }
     }
 }

无论是list,还是DataTable,还是XML文件,我们都可以用LINQ技术来写source,这里就不介绍了,具体可以看书

(七)ObjectDataProvider为source

ObjectDataProvider是为第一种情况的补充,因为不能保证每个类的属性都是暴露在外面可以绑定的,有的时候控件上要绑定的值只是一个类的某个方法里的一些参数,这种情况下第一种方法就没用了,而如果重新设计底层类的风险又太大,这个时候用ObejectDataProvider会比较合适。

现在有一个Calculator的类

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;

 namespace WpfApplication1
 {
     class Calculator
     {
         public string Add(string arg1, string arg2)
         {
             ;
             ;
             ;
             if (double.TryParse(arg1, out x) && double.TryParse(arg2, out y))
             {
                 z = x + y;
                 return z.ToString();
             }
             return "Input Error!";
         }
     }
 }

在xaml里加个button(这里省略不写了)

在cs里可以先写个简单的objectDataProvider

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Data;
 using System.Windows.Documents;
 using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using System.Windows.Navigation;
 using System.Windows.Shapes;
 using System.Data;
 using MySql.Data;
 using MySql.Data.Entity;
 using MySql.Data.MySqlClient;
 using System.Xml;
 using System.Xml.Linq;

 namespace WpfApplication1
 {
     /// <summary>
     /// Interaction logic for MainWindow.xaml
     /// </summary>
     public partial class MainWindow : Window
     {
         public MainWindow()
         {
             InitializeComponent();
         }

         private void Button_Click(object sender, RoutedEventArgs e)
         {
             ObjectDataProvider odp = new ObjectDataProvider();
             odp.ObjectInstance = new Calculator();
             odp.MethodName = "Add";
             odp.MethodParameters.Add(");
             odp.MethodParameters.Add(");
             MessageBox.Show(odp.Data.ToString());
         }
     }
 }

这里将对象放在ObjectDataProvider.ObjectInstance属性里,将Method放在ObjectDataProvider.MethodName里,而参数得要用ObjectDataProvider.MethodParameters.Add方法来添加,最后它的结果放在ObjectDataProvider.Data里,这里的参数也可以用data binding的方法。

xaml:

 <Window x:Class="WpfApplication1.MainWindow"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         Title="Simple Binding" Height="135" Width="300">
     <StackPanel x:Name="stackPanel" Background="LightBlue">
         <TextBox x:Name="textBoxArg1" Margin="5" />
         <TextBox x:Name="textBoxArg2" Margin="5" />
         <TextBox x:Name="textBoxResult" Margin="5" />
     </StackPanel>
 </Window>

cs:

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Data;
 using System.Windows.Documents;
 using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using System.Windows.Navigation;
 using System.Windows.Shapes;
 using System.Data;
 using MySql.Data;
 using MySql.Data.Entity;
 using MySql.Data.MySqlClient;
 using System.Xml;
 using System.Xml.Linq;

 namespace WpfApplication1
 {
     /// <summary>
     /// Interaction logic for MainWindow.xaml
     /// </summary>
     public partial class MainWindow : Window
     {
         public MainWindow()
         {
             InitializeComponent();
             this.SetBinding();
         }

         private void SetBinding()
         {
             ObjectDataProvider odp = new ObjectDataProvider();
             odp.ObjectInstance = new Calculator();
             odp.MethodName = "Add";
             odp.MethodParameters.Add(");
             odp.MethodParameters.Add(");

             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
             };

             this.textBoxArg1.SetBinding(TextBox.TextProperty, bindingToArg1);
             this.textBoxArg2.SetBinding(TextBox.TextProperty, bindingToArg2);
             this.textBoxResult.SetBinding(TextBox.TextProperty, new Binding(".") { Source = odp });
         }
     }
 }

这里的odp.MethodParameters.Add("0");不一定是0,可以换成其他数字,区别就在于开始的时候是显示其他数字了。

BindsDirectlyToSource = true这句的意思是告诉Binding对象只负责把从UI元素收集到的数据写入其直接Source(即ObjectDataProvider对象)而不是被ObjectDataProvider对象包装着的Calculator对象。

一般情况下,数据从哪里来哪里就是Binding的source,数据到哪里去哪里就应该是binding的target。按这个理论,前两个textbox应该是ObjectDataProvider对象的数据源,而ObjectDataProvider对象又是最后一个TextBox的数据源。但实际上,三个TextBox都以ObjectDataProvider对象为数据源,只是前两个TextBox在Binding的数据流向上做了限制。这样做的原因不外乎有两个:

1. ObjectDataProvider的MethodParameters不是依赖属性,不能作为Binding的目标。

2. 数据驱动UI的理念要求尽可能地使用数据对象作为Binding的Source而把UI元素当做Binding的Target。

(八)RelativeSource为Source

不能确定Source的对象叫什么名字,但知道它与作为Binding目标的对象在UI布局上有相对关系,比如控件自己关联自己的某个数据,关联自己某级容器的数据,这个时候我们就要使用Binding的RelativeSource属性。这里具体的代码就不写了,可以看书

上一篇:iOS微信内存监控


下一篇:MySQL字符集设置及字符转换(latin1转utf8)