WPF快速入门系列(4)——深入解析WPF绑定
一、引言
WPF绑定使得原本需要多行代码实现的功能,现在只需要简单的XAML代码就可以完成之前多行后台代码实现的功能。WPF绑定可以理解为一种关系,该关系告诉WPF从一个源对象提取一些信息,并将这些信息来设置目标对象的属性。目标属性总是依赖属性。然而,源对象可以是任何内容,可以是一个WPF元素、或ADO.NET数据对象或自定义的数据对象等。下面详细介绍了WPF绑定中的相关知识点。
二、绑定元素对象
2.1 如何实现绑定元素对象
这里首先介绍绑定最简单的情况——绑定元素对象,即数据源是一个WPF元素对象并且源属性是依赖属性。由于依赖属性具有内置的更改通知支持,因此,当在源对象中改变依赖属性的值时,会立即更新目标对象中的绑定属性。下面通过一个简单的例子来演示下如何绑定元素对象。具体的XAML代码(这里不需要后台代码)如下所示:
<StackPanel> <Slider Name="sliderFontSize" Margin="3" Minimum="1" Maximum="40" Value="10" TickFrequency="1" TickPlacement="TopLeft"/> <TextBlock Margin="10" Text="LearningHard" Name="lbtext" FontSize="{Binding ElementName=sliderFontSize, Path=Value}"></TextBlock> </StackPanel>
在上面XAML代码中,TextBlock控件的FontSize属性绑定了Slider控件的Value属性,感觉说绑定有点拗口,你可以直接理解为TextBlock的FontSize属性的值来自与Slider控件的Value值,由于源属性Value是依赖属性,具体内置的更改通知功能,所以Slider控件Value值的改变,直接影响TextBlock控件FontSize的值。正如我们分析的那样,实际运行结果也是如此,运行结果如下图所示:
当移动上图中Slider控件上的游标时,下面的文本字体大小也会跟着一起改变。具体效果这里就不贴图了,大家可以自行尝试。从中可以看到WPF绑定的强大了吧,如果放到以前WinForm开发中,你需要监听Slider的ValueChanged事件,然后在事件处理程序中去动态改变文本的字体大小。
这里Path除了可以直接绑定属性之外,还可以绑定属性的属性,如FontFamily.Source,也可以指向属性使用的索引器,如Content.Children[0]。当然你也可以执行多层次的路径,如指向属性的属性的属性等。
另外,如果绑定失败时,WPF不会引发异常来告知绑定失败的原因。例如,指定的元素或属性不存在,此时不会收到任何提示,只是在目标属性不能显示数据罢了。然而在调试模式下,你可以在输出窗口来查看绑定失败的信息,例如,在上面XAML代码,我们绑定Slider控件一个不存在的属性,如Text属性,此时在Output窗口中就会看到如下信息:
2.2 绑定模式
绑定的一个最大的特点就是源属性改变时,目标属性会自动地更新。然而上面的示例有一个问题,即目标对象的改变不会自动更新源对象的属性。通过下面的例子可以看出这个问题所在。此时XAML代码修改为:
<Window x:Class="WPFBindingDemo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="400"> <StackPanel> <Slider Name="sliderFontSize" Margin="3" Minimum="1" Maximum="40" Value="10" TickFrequency="1" TickPlacement="TopLeft"/> <TextBlock Margin="10" Text="LearningHard" Name="lbtext" FontSize="{Binding ElementName=sliderFontSize, Path=Value}"></TextBlock> <!--在按钮的Click事件处理程序中去改变目标对象的FontSize的值--> <StackPanel Orientation="Horizontal"> <Button Margin="10" Padding="5" Click="cmd_SetSmall">Set to Small</Button> <Button Margin="10" Padding="5" Click="cmd_SetNormal">Set to Normal</Button> <Button Margin="10" Padding="5" Click="cmd_SetLarge">Set to Large</Button> </StackPanel> </StackPanel> </Window>
此时后台C#代码如下所示:
private void cmd_SetSmall(object sender, RoutedEventArgs e) { // 仅仅在双向模式下工作 lbtext.FontSize = 5; } private void cmd_SetNormal(object sender, RoutedEventArgs e) { sliderFontSize.Value = 20; } private void cmd_SetLarge(object sender, RoutedEventArgs e) { // 仅仅在双向模式下工作 lbtext.FontSize = 40; }
具体的运行效果如下图所示:
从上图可以看到,当在后台更改TextBlock的FontSize属性值,而Slider的Value值却没有进行更新。此时,你肯定会想问,能不能实现目标属性的更变也会自动改变绑定中源属性的机制呢?因为这样就不会显得那样呆板了,然而,你想到的了WPF团队肯定也想到了,WPF支持双向绑定,即从源到目标以及目标到源,要支持双向绑定,只需要设置Binding对象的Mode属性为TwoWay即可,修改后的XAML代码为:
<StackPanel> <Slider Name="sliderFontSize" Margin="3" Minimum="1" Maximum="40" Value="10" TickFrequency="1" TickPlacement="TopLeft"/> <TextBlock Margin="10" Text="LearningHard" Name="lbtext" FontSize="{Binding ElementName=sliderFontSize, Path=Value, Mode=TwoWay}"></TextBlock> <!--在按钮的Click事件处理程序中去改变目标对象的FontSize的值--> <StackPanel Orientation="Horizontal"> <Button Margin="10" Padding="5" Click="cmd_SetSmall">Set to Small</Button> <Button Margin="10" Padding="5" Click="cmd_SetNormal">Set to Normal</Button> <Button Margin="10" Padding="5" Click="cmd_SetLarge">Set to Large</Button> </StackPanel> </StackPanel>
Mode属性除了可以设置OneWay,TwoWay值外,还可以设置Default、OneTime和OneWayToSource,关于这些值更详细的介绍请自行参考MSDN:http://msdn.microsoft.com/zh-cn/library/system.windows.data.bindingmode(v=vs.110).aspx。
另外,除了可以在XAML中通过Binding标记地方式声明绑定外,还可以使用代码方式动态创建绑定。如上面的例子中代码创建绑定的实现代码如下所示:
1 Binding binding = new Binding(); 2 binding.Source = sliderFontSize; 3 binding.Path = new PropertyPath("Value"); 4 binding.Mode = BindingMode.TwoWay; 5 lbtext.SetBinding(TextBlock.FontSizeProperty, binding);
还可以通过使用BindingOperations类的ClearBinding方法来移除数据绑定。还可以使用ClearAllBindings移除一个元素的所有数据绑定。
2.3 绑定更新
在上面的例子中,还存在另一个问题,当通过在文本框中输入内容来改变显示的字体尺寸时,此时什么事情都不会发生,知道使用tab键将焦点转移到另外一个控件时,才会应用对应的改变。此时XAML代码如下所示:
<Window x:Class="WPFBindingDemo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="400"> <StackPanel> <Slider Name="sliderFontSize" Margin="3" Minimum="1" Maximum="40" Value="10" TickFrequency="1" TickPlacement="TopLeft"/> <TextBlock Margin="10" Text="LearningHard" Name="lbtext" FontSize="{Binding ElementName=sliderFontSize, Path=Value, Mode=TwoWay}"></TextBlock> <!--在按钮的Click事件处理程序中去改变目标对象的FontSize的值--> <StackPanel Orientation="Horizontal"> <Button Margin="10" Padding="5" Click="cmd_SetSmall">Set to Small</Button> <Button Margin="10" Padding="5" Click="cmd_SetNormal">Set to Normal</Button> <Button Margin="10" Padding="5" Click="cmd_SetLarge">Set to Large</Button> </StackPanel> <!--添加一个输入文本框来设置文本字体大小进行测试问题--> <StackPanel Orientation="Horizontal" Margin="5"> <TextBlock VerticalAlignment="Center">Set FontSize:</TextBlock> <TextBox Text="{Binding ElementName=lbtext, Path=FontSize, Mode=TwoWay}" Width="100"/> </StackPanel> </StackPanel> </Window>
后台代码实现与前面的一样,此时运行的效果如下图所示:
为了明白导致这个问题的原因,这里需要深入分析下绑定表达式。当使用OneWay或TwoWay绑定时,改变后的值会立即从源传播到目标。对于滑动条,然而,从目标到源传播未必会立即发生。因为,它们的行为是由Binding.UpdateSourceTrigger属性控制,该属性可以使用下图列出的某个值。注意,UpdateSourceTrigger属性值并不影响目标的更新方式,它仅仅控制TwoWay模式或OneWayToSource模式的绑定更新源的方式。而文本框正是使用LostFocus方式从目标向源进行更新的。
既然,找出了导致原因,此时可以对XAML代码进行修改,使得当用于在文本框中输入内容时将变化应用于字体尺寸,具体改变部分的XAML代码为:
<!--添加一个输入文本框来设置文本字体大小进行测试问题--> <StackPanel Orientation="Horizontal" Margin="5"> <TextBlock VerticalAlignment="Center">Set FontSize:</TextBlock> <TextBox Text="{Binding ElementName=lbtext, Path=FontSize, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="100"/> </StackPanel>
另外,需要注意的是,TextBox的Text属性的默认行为是LostFocus,这是因为当用于输入内容时,文本框中文本会不断变化,从而引起多次更新。所以PropertyChanged模式可能会使应用程序运行更缓慢,所以LostFocus默认行为可以说是合理的。
要完全控制源对象的更新时机,也可以选择UpdateSourceTrigger.Explicit模式。此时就需要额外编写代码手动触发更新,此时可以添加一个Apply按钮,并在按钮的Click事件处理程序中调用BindingExpression.UpdateSource方法触发立即刷新并更新字体大小的操作。具体的实现代码如下所示:
// 获得应用于文本框上的绑定 BindingExpression be = txtFontSize.GetBindingExpression(TextBox.TextProperty); // 调用UpdateSource更新源对象 be.UpdateSource();
三、绑定非元素对象
上面都是介绍如何链接两个元素的绑定,但是在数据驱动的应用程序中,更常见的情况是创建从一个对象中提起数据的绑定表达式。不过希望绑定的信息必须存储在一个公有属性中。因为WPF绑定不能获取私有信息或公有字段。
当绑定一个非元素对象时,不能使用Binding.ElementName属性,但可以使用以下属性中的一个:
- Source——该属性是指向源对象的引用,即提供数据的对象。
- RelativeSource——该属性使用RelativeSource对象指定绑定源的相对位置,默认值为null。
- DataContext属性——如果没有使用Source或RelativeSource属性指定一个数据源,WPF会从当前元素开始在元素树中向上查找。检查每个元素的DataContext属性,并使用第一个非空的DataContext属性。当然你也可以自己设置DataContext属性。
下面通过一个例子来演示下如何绑定到非元素对象。下面的演示如何使用DataContext属性来绑定一个自定义对象的属性。首先自定义一个实现了INotifyPropertyChanged接口的类。这个接口是为了发出属性更改的通知,即实现了这个接口将会实现当源对象的公共属性发生改变时,该属性的值会立即响应到界面上显式。当然不实现这个接口的对象也可以绑定控件中,只要被绑定是公有属性就可以。具体的实现代码如下所示:
1 using System.ComponentModel; 2 3 namespace WPFBindingDemo 4 { 5 public class Student:INotifyPropertyChanged 6 { 7 private int m_ID; 8 private string m_StudentName; 9 private double m_Score; 10 11 public int ID 12 { 13 get { return m_ID; } 14 set 15 { 16 if (value != m_ID) 17 { 18 m_ID = value; 19 Notify("ID"); 20 } 21 } 22 } 23 24 public string StudentName 25 { 26 get { return m_StudentName; } 27 set 28 { 29 if (value != m_StudentName) 30 { 31 m_StudentName = value; 32 Notify("StudentName"); 33 } 34 } 35 } 36 37 public double Score 38 { 39 get { return m_Score; } 40 set 41 { 42 if (value != m_Score) 43 { 44 m_Score = value; 45 Notify("Score"); 46 } 47 } 48 } 49 50 public event PropertyChangedEventHandler PropertyChanged; 51 private void Notify(string propertyName) 52 { 53 if (PropertyChanged != null) 54 { 55 this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); 56 } 57 } 58 } 59 }
既然源数据对象以准备好了,自然接下来就是去设计WPF界面来让控件来绑定这个源对象了,具体的XAML代码如下所示:
<Window x:Class="WPFBindingDemo.BindingToCollection" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="BindingToCollection" Height="300" Width="300"> <StackPanel Margin="50"> <StackPanel Orientation="Horizontal" Margin="10"> <TextBlock Text="学号:" /> <TextBlock Text="{Binding Path=ID}" Width="100"/> </StackPanel> <StackPanel Orientation="Horizontal" Margin="10"> <TextBlock Text="姓名:" /> <TextBlock Text="{Binding Path=StudentName}" Width="100"/> </StackPanel> <StackPanel Orientation="Horizontal" Margin="10"> <TextBlock Text="分数:" /> <TextBlock Text="{Binding Path=Score}" Width="100"/> </StackPanel> <StackPanel Orientation="Horizontal" Margin="10"> <Button Content="改变姓名" Name="changeName" Click="changeName_Click_1"/> <Button Content="改变分数" Name="changeScore" Margin="20,0,0,0" Click="ChangeScore_Click"/> </StackPanel> </StackPanel> </Window>
对应的后台代码逻辑如下所示:
public partial class BindingToCollection : Window { private Student m_student; public BindingToCollection() { InitializeComponent(); m_student = new Student() { ID = 1, StudentName = "LearningHard", Score = 60 }; // 设置Window对象的DataContext属性 this.DataContext = m_student ; } private void ChangeScore_Click(object sender, RoutedEventArgs e) { m_student.Score = 90; } private void changeName_Click_1(object sender, RoutedEventArgs e) { m_student.StudentName = "Learning"; } }
完成了示例所有代码的编写之后,下面具体看看示例的运行效果,看看是否可以成功完成绑定并源对象的属性的更改会立即反应到界面中,具体的效果图如下所示:
从上图示例的演示动画效果可以看出,上面的代码确实实现我们预期的功能。从上面代码可以看出,我们并没有对每个控件单独设置它的Source属性,而是直接设置了Window对象的DataContext属性。这样绑定的控件发现没有设置source属性或RelativeSource属性,就会从元素树中查找DataContex属性不为null的值来作为自己的DataContext。通过这样的方式可以省去重复在多个控件中设置相同的DataContext属性。
这里只是演示了绑定单个数据对象的情况,就如之前所说的,数据源还可以是XAML文件,ADO.NET数据对象、集合等,这里就不一一实现了,只要了解具体思路,具体问题具体搜索解决就好了。这里给出两个非常的好例子。
Simple Demo of Binding to a Database in WPF using LINQ-SQL
How to Perform WPF Data Binding Using LINQ to XML
四、提高大列表的性能
如果绑定的数据源具有大量记录时,此时就需要考虑性能的问题了。然而,幸运的是,WPF很多列表控件都已经帮我们做好了相应的支持,这里只是提出来让大家知道这点。
对于大列表显示性能问题,WPF做了以下几种支持:
- UI虚拟化——UI虚拟化是列表仅为当前显示项创建容器对象的一种技术,例如,如果有一个具有5万条记录的列表,但是可见区域只能包含30条记录,ListBox控件只创建30个ListBoxItem对象。如果ListBox控件不支持UI虚拟化的话,它将需要生成全部5万个ListBoxItem对象,这显然需要占用更多的内存。并且分配这些对象的时间用户明显可以感觉到,这就带来了非常不好的用户体验。WPF中UI虚拟化是通过VirtualizingStackPanel容器实现的。像ListBox、ListView和DataGrid都自动使用VirtualizingStackPanel面板布局它们的子元素,所以,这些控件都默认支持虚拟化功能。然而,ComboBox需要支持虚拟化支持,必须明确提供新的ItemPanelTemplate添加虚拟化支持,具体实现如下所示:
<ComboBox> <ComboBox.ItemsPanel> <ItemsPanelTemplate> <VirtualizingStackPanel></VirtualizingStackPanel> </ItemsPanelTemplate> </ComboBox.ItemsPanel> </ComboBox>
TreeView控件也支持虚拟化,但它在默认情况下,关闭了该支持,你需要显式启用该特性,具体使用的启用代码如下所示:
<TreeView VirtualizingPanel.IsVirtualizing="True" />
- 项目容器再循环——WPF 3.5 SP1使用项目容器再循环改进了虚拟化。通常支持虚拟化的列表在滚动时,控件不断地创建新的项目容器对象来保存新的可见项。例如,当具有5万个项的ListBox控件,在滚动时,ListBox需要重新生成新的ListBoxItem对象。但是如果启用了项目容器再循环,ListBox控件会保存少量ListBoxItem对象存活,当滚动时,将新数据加到这些之前的ListBoxItem对象,从而重复使用它们。具体支持代码如下所示
<ListBox VirtualizingPanel.VirtualizationMode="Recycling"/>
项目容器再循环提供了滚动性能,并降低了内存消耗量,因为垃圾回收器不需要查找旧对象进行回收。为了确保向后兼容,除了DataGrid之后的所有列表控件默认都禁用该特性,如需支持,需要显式启用。
- 延迟滚动——为了进一步提供滚动性能,可以开启延迟滚动功能。使用延迟滚动,当用户在滚动条上拖动滚动滑块时不更新列表显示,只有用户释放了滚动滑块时才刷新。当使用常规滚动时,在拖动的同时会刷新列表,使列表显示正在改变的位置。这个特性也需要显式启用,启用代码如下:
<ListBox ScrollViewer.IsDeferredScrollingEnabled="True"/>
显然,需要在响应性和易用性之间平衡。如果有一个复杂的模板和大量数据,对于提高速度可能会选择使用延迟滚动特性,但当用户需要在滚动时查看目前滚动位置,则就可以不启用该特性。
上面介绍了这么多,其实提供列表控件的性能主要在两方面:UI虚拟化提高了列表项初始化的时间,因为UI虚拟化支持一次性不初始化所有项,而在滚动是自动创建新的项。项目容器再循环和延迟滚动提高了滚动性能。
另外WPF绑定还有两个知识点:数据验证和数据转换,对于数据验证与Asp.net中验证类似,都是为了保证输入数据的合法性,而数据转换指的是在源数据绑定到目标依赖属性之前要做对应的转换,例如WPF显示人民币都需要显示一个¥符号,但是如果数据源的内容只是“120”这样的字符串怎么办呢?这时候就可以通过数据转换在绑定之前,把数据源的值转换成显示所需要的格式。对于这两个知识点,我觉得在遇到问题时再去学就好了,因为我们已经明白了解决问题的思路了。所以,在快速入门系列中不想太深入的介绍这两个知识点,以使大家可以快速掌握WPF要领。这里给出几个学习链接:
数据绑定概述
WPF Data Binding - Part 1
WPF Simple Data Converter Example
五、小结
到这里,这篇博文的内容就介绍结束了,时间不知不觉的已经2点多了。下面一篇博文将分享WPF命令的内容。
本文所有源码下载:WPFBindingDemo.zip