程序的本质是数据加算法。数据会在存储、逻辑和展示三个层流通,在数据角度上看,三层都很重要,但从算法角度上来看,算法在程序中的分布就不是很均匀,对于一个三层结构的程序来说,算法一般分布在:
A、数据库内部
B、读取和写会数据
C、业务逻辑
D、数据展示
E、界面和逻辑的交互
A、B两个部分的算法一般都非常稳定,不会轻易改动,复用性也很高;C处与客户需求关系最紧密,最复杂,变动也最大,大多数算法都集中在此;D、E两层负责UI与逻辑的交互,也占有一定量的算法。
C是程序的核心,是占用开发精力最多的地方,但D、E却因为与逻辑层紧密相关,而容易把本应该写在逻辑层里的算法写在D、E部分(所以才有了MVC等模式)。WPF可以让展示出永远处于逻辑层的从属地位,这种能力的实现关键是它引入了Data Binding概念以及配套的Dependency Property系统和DataTemplate。
Data Binding在WPF系统中起到的是数据告诉公路的作用,加工好的数据会自动送达用户界面加以显示,被用户修改过的数据也会自动传回逻辑层,一旦数据被加工好又会送达用户界面,这就是数据驱动UI,也是现在前端流行的各类MVVM框架的核心概念。
Binding的两端分别是源Source和目标Target,数据源是一个对象,这个对象可能有和很多数据,这些数据通过属性暴露给外界,UI上的元素关心的是哪个属性值的变化,这个属性就成为Binding的路径,但Binding还需要有一个自动机制,即当值变化后属性要有能力通知Binding,让Binding把变化传递给UI元素,这个功能的实现是在属性的set语句中激发一个PropertyChanged事件。这个事件不需要手动声明,要做的是让作为数据源的类实现System.ComponentModel名称空间中的INotifyPropertyChanged接口。当为Binding设置了数据源后,Binding就会自动监听来自这个接口的PropertyChanged事件。
public 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")); } } } }
为了在Student的值发生改变时有能力通知Binding,让Binding把变化传递给UI元素。方法是在属性的set语句中激发一个PropertyChanged事件。这个事件不需要自行声明,而是让作为数据源的类实现System.ComponentModel名称空间中的INotifyPropertyChanged接口。
在窗体上准备一个TextBox和一个Button,TextBox将作为Binding目标,Button的Click事件发生时会改变Student对象的Name属性值。
<StackPanel> <TextBox x:Name="textBoxName" BorderBrush="Black" Margin="5"/> <Button Content="AddAge" Margin="5" Click="Button_Click"/> </StackPanel>
然后使用Binding把数据源和UI元素连接起来。
public partial class MainWindow : Window { Student stu; public MainWindow() { InitializeComponent(); //准备数据源 stu = new Student(); //准备Binding Binding binding = new Binding(); binding.Source = stu; binding.Path = new PropertyPath("Name"); //使用Binding连接数据源与Binding目标 BindingOperations.SetBinding(this.textBoxName, TextBox.TextProperty, binding); } private void Button_Click(object sender, RoutedEventArgs e) { stu.Name += "Name"; } }
Student stu;声明在构造函数和按钮事件外是为了让Window的构造器和按钮事件都能访问由它引用的Student实例。
Binding部分,先创建Binding的实例,然后使用binding.Source=stu;为Binding实例指定数据源,最后使用binding.Path=new PropertyPath("Name");来为Binding指定访问路径。
把数据源和目标连接在一起的任务是使用"BindingOperations.SetBinding(...)"方法完成的,这个方法有三个重要参数:
1、第一个参数用于指定Binding的目标,本例中是this.textBox.Name
2、第二个参数用于为Binding指明把数据送达目标的哪个属性。但这个参数并没有使用对象的属性,而只是一个静态只读的DependencyProperty类型成员变量,这就是依赖属性。这类属性的值可以通过Binding依赖在其他对象的属性值上,被其他对象的属性值所驱动。
3、第三个参数很明了,就是指定使用哪个Binding实例将数据源与目标关联起来。
按钮事件用于对Name属性进行更新。
一、Binding的源和路径
Bingding的源也就是数据的源头。Binding对源的要求并不苛刻,只要它是一个对象,并且通过属性公开自己的数据,就能作为Binding的源。
数据源不仅仅可以是类(比如实现INotifyPropertyChanged接口,并在属性set语句中激发PropertyChanged事件),还可以是控件自己、自己的容器、子元素,另一个控件、集合(作为ItemsControl)的数据源、XML(TreeView、Menu的数据源)、把多个控件关联到一个“数据制高点”上,甚至不给Binding指定数据源,让他自己去寻找。
1、把控件作为Binding源和Binding标记扩展
大多数情况下Binding的源是逻辑层的对象,但有时候为了让UI元素产生一些联动效果,也会使用Binding在控件间关联。
<TextBox x:Name="textBox1" Text="{Binding Path=Value,ElementName=slider}" BorderBrush="Black" Margin="5"/> <Slider x:Name="slider" Maximum="100" Minimum="0" Margin="5"/>
WPF中,代码可以访问XAML代码中声明的变量,但XAML代码却无法访问C#代码中声明的变量、
上一句话等价于this.TextBox1.SetBinding(TextBox.TextProperty,new Binding("Value")){ElementName="slider"}
2、控制Binding的方向和数据更新
Binding在源和目标之间架起了沟通的桥梁,默认情况下数据既能够通过Binding送达目标,也能够从目标返回源(手机用户对数据的修改)。有时候数据只需要展示给用户、不允许用户修改,这时候可以把Binding模式更改为源向目标的单项沟通。
控制Binding数据流向的属性是Mode,它的类型是BindingMode枚举。BindingMode可取值为TwoWay、OneWay、OnTime、OneWayToSource和Default。这里的Default值是指Binding的模式会根据目标的实际情况来确定,比如若是可编辑的(TextBox.Text属性),Default就采用双向模式;若是只读的(TextBlock.Text)就采用单向模式。
Binding的另一个属性——UpdateSourceTrigger,它的类型是UpdateSourceTrigger枚举,可取值为PropertyChanged、LostFocus、Explicit、Default、
3、Binding的路径
作为Binding源的对象可以由很多属性,通过这些属性Binding源可以把数据暴露给外界。Binding Path属性来指定Binding到底关注那个属性的值。例如把Slider控件对象作为源、把它的Value属性作为路径。
<TextBox x:Name="textBox1" Text="{Binding Path=Value,ElementName=slider}"/>
等价于C#:
Binding binding=new Binding(){Path=new PropertyPath("Value"),Source=this.slider};
this.textBox1.SetBinding(TextBox.TextProperty,binding);
也可以让一个TextBox显示另一个TextBox的文本长度
<TextBox x:Name="textBox2" BorderBrush="Black" Margin="5"/> <TextBox x:Name="textbox3" Text="{Binding Path=Text.Length,ElementName=textBox2,Mode=OneWay}" BorderBrush="Black" Margin="5"/>
索引器也能作为Path来使用,比如让一个TextBox显示另一个TextBox文本的第四个字符
<TextBox x:Name="textbox4" BorderBrush="Black" Margin="5"/> <TextBox x:Name="textbox5" Text="{Binding Path=Text.[3],ElementName=textbox4,Mode=OneWay}" BorderBrush="Black" Margin="5"/>
Text.[3]中的.可以去掉。
当使用一个集合或者DataView作为Binding源时,如果我们想把它的默认元素当做Path使用,则需要这样的语法:
List<string> stringList = new List<string>() { "Tim", "Tom", "Blog" }; this.textbox4.SetBinding(TextBox.TextProperty, new Binding("/Length") { Source = stringList, Mode = BindingMode.OneWay });
如果要是用默认元素,则直接/即可,如果要取第3个元素/[2],如果要取子集合元素,则可以一路/下去。
4、没有path的Binding
当Binding源本身就是数据且不需要Path类指定时,Path就是一个.或者干脆没有Path的Binding。典型的,string、int等基本类型就是这样,它们的实例本身就是数据,则无法通过它的某个属性来访问这个数据,此时只需要把Path的值设置为.即可。XAML中可以省略,而C#中不可以。
<StackPanel.Resources> <sys:String x:Key="myString"> static string </sys:String> </StackPanel.Resources> <TextBlock x:Name="textBlock1" TextWrapping="Wrap" Text="{Binding Path=.,Source={StaticResource ResourceKey=myString}}" FontSize="16" Margin="5"/>
等价于C#代码:
Text="{Binding Source={StaticResource ResourceKey=myString}}"
4、为Binding指定源的集中方法
Binding的源时数据的来源,所以只要一个对象包含数据并能通过属性把数据暴露出来,它就能高档做Binding的源来使用,包含数据的对象比比皆是,但必须为Binding的Source指定合适的对象Binding才能正确工作。
常见的绑定方法有:
A、把普通CLR类型单个对象指定为Source:包括.Net Framework自带类型对象和用户自定义类型的对象。如果类型实现了INotifyPropertyChanged接口,则可通过在属性的set语句里激发PropertyChanged事件来通知Binding数据已被更新。
B、把普通CLR集合类型对象指定为Source:包括数组、List<T>、ObservableCollection<T>等集合类型,实际工作中,我们经常需要把一个集合作为ItemsControl派生类的数据源来使用,一般是把控件的ItemsSource属性使用Binding关联到一个集合对象上。
C、把ADO.NET数据对象指定为Source,包括DataTable和DataView等对象。
D、使用XmlDataProvider把XML数据指定为Source:XML作为标准的数据存储和传输格式几乎无处不在,可以用它标识单个数据对象或集合;
E、把依赖对象(Dependency Object)指定为Source:依赖对象不仅可以作为Binding的目标,同时也可以作为Binding的源。这样就有可能形成Binding链。依赖对象中的依赖属性可以作为Binding的Path。
F、把容器的DataContext指定为Source:(WPF Data Binding默认行为),直到从哪个属性获取数据,但把哪个对象作为Binding源还不能确定。此时,就可以先建立一个Binding、只给它设置Path而不设置Source,让这个Binding自己去寻找Source。此时,Binding会自动把控件的DataContext当做自己的Source(会沿着控件树一层一层向外找,直到找到带有Path指定属性的对象为止)。
G、通过ElementName指定Source:在C#代码里可以直接把对象作为Source赋值给Binding,但XAML无法访问对象,所以只能使用对象的Name属性来找到对象。
H、通过Binding的RelativeSource属性相对地指定Source,当控件需要关注自己的、自己容器的或自己内部元素的某个值就需要使用这种方法。
I、通过Binding的RelativeSource属性相对地指定Source,当控件需要关注自己的、自己容器的或者自己内部元素的某个值就需要使用这种方法。
J、把ObjectDataProvider对象指定为Source:当数据源的数据不是通过属性而是通过方法暴露给外界的时候,可以使用这两种对象来包装数据源并再把他们指定为Source。
5、没有Source的Binding-使用DataContext作为Binding的源
Binding可以把单个CLR类型对象指定为Binding的Source,但如果一个Binding只知道自己的Path而不知道自己的Source时,就会沿着UI元素树一路向树根找过去,没过一个节点就会看这个节点的DataContext是否具有Path所制定的属性。如果有,这把这个对象作为自己的Source,否则继续寻找。
<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>
此时属性结构就有一个DataContext:Student。
使用xmlns:local="clr-namespace:WPF_XMAL_6_Binding"就可以使用在C#中定义的Student类。
<TextBox Text="{Binding Path=Id}" Margin="5"/> <TextBox Text="{Binding Path=Name}" Margin="5"/> <TextBox Text="{Binding Path=Age}" Margin="5"/>
3个TextBox虽然没有指定Source,但Binding就会自动向UI树的长层去寻找可用的DataContext对象。Binding的Path可以设置为.,也可以直接省略不写。
Binding这么智能是因为,当没有为空间的某个依赖属性显式赋值时,空间会把自己容器的属性值借过来当自己的属性值,例如如果为一个Button所在的Grid设置DataContext=“6”,然后Button的事件中设置MessageBox.Show(btn.DataContext.ToString());,虽然Button本身没有值,但它会显式Grid的DataContext。
<StackPanel DataContext="LiaoRan">
<Button x:Name="btn" Content="OK" Click="btn_Click"/>
.... private void btn_Click(object sender, RoutedEventArgs e) { MessageBox.Show(btn.DataContext.ToString()); }
属性值沿着UI树向下传递了。
实际工作中DataContext用法非常灵活,比如:
1、当UI上的多个控件都使用Binding关注同一个对象时,不妨使用DataContext
2、当作为Source的对象不能被直接访问的时候,例如A窗体内的控件是private访问级别,B要去访问A窗体内的控件作为自己的Binding源,就可以把A窗体内的这个控件或控件的值作为窗体A的DataContext(这个属性是public级别)从而暴露数据。
6、使用集合对象作为列表控件的ItemSource
WPF中的列表式控件都派生自ItemsControl类,也就自然继承了ItemsControl这个属性。ItemsSource属性可以接受一个IEnumerable接口派生类的实例作为自己的值,每个ItemsControl派生类都具有自己对应的条目容器,比如ListBox的条目容器是ListBoxItem,ItemSource里存放的是一条一条的数据,要想把数据显示出来需要为其穿上外衣,就是Binding。只要为每一个ItemsControl对象设置ItemsSource属性值,ItemsControl对象就会自动迭代其中的数据元素,为每个元素准备一个条目容器,并建立起关联。
<StackPanel x:Name="stackPanel" Background="LightBlue"> <TextBlock Text="StudentID:" FontWeight="Bold" Margin="5"/> <TextBox x:Name="textBoxId" Margin="5"/> <TextBlock Text="StudentList:" FontWeight="Bold" Margin="5"/> <ListBox x:Name="listBoxStudents" Height="110" Margin="5"/> </StackPanel>
public MainWindow() { InitializeComponent(); //准备数据 List<Student> stulist = new List<Student>() { new Student(){Id=0,Name="Tim",Age=21}, new Student(){Id=1,Name="Tom",Age=22}, new Student(){Id=2,Name="Cloud",Age=23}, new Student(){Id=3,Name="Strife",Age=24}, new Student(){Id=4,Name="Tifa",Age=25}, new Student(){Id=5,Name="Victor",Age=26}, }; //为ListBox设置Binding this.listBoxStudents.ItemsSource = stulist; this.listBoxStudents.DisplayMemberPath = "Name"; //为TextBox设置Binding Binding binding = new Binding("SelectedItem.Id") { Source = this.listBoxStudents }; this.textBoxId.SetBinding(TextBox.TextProperty, binding); }
7、使用Linq作为Binding的源
<StackPanel Background="LightBlue"> <ListView x:Name="listViewStudent" Height="143" Margin="5"> <ListView.View> <GridView> <GridViewColumn Header="Id" Width="60" DisplayMemberBinding="{Binding Id}"/> <GridViewColumn Header="Id" Width="100" DisplayMemberBinding="{Binding Name}"/> <GridViewColumn Header="Id" Width="80" DisplayMemberBinding="{Binding Age}"/> </GridView> </ListView.View> </ListView> <Button Content="Local" Height="25" Margin="5,0" Click="Button_Click_1"/> </StackPanel>
private void Button_Click_1(object sender, RoutedEventArgs e) { //获取DataTable实例 List<Student> list = new Student().GetStudentList(); //绑定 this.listViewStudent.ItemsSource = list.Where(x => x.Name.StartsWith("T")); }
绑定到listViewStudents.ItemsSource上即可。