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属性。这里具体的代码就不写了,可以看书