WPF中的数据绑定提供了很强大的功能。与普通的WinForm程序相比,其绑定功能为我们提供了很多便利,例如Binding对象的自动通知/刷新,Converter,Validation Rules,Two Way Binding等功能,省去了很多维护的繁琐工作。另外对于WPF中提供的数据模板功能,让我们可以轻松定制可以被复用的控制呈现的模块—但这是以数据绑定为前提来做到轻松易用的效果的。数据提供者例如XmlDataProvider和ObjectDataProvider更是简化了将对象以特定方式绑定并呈现的过程。可以说,数据绑定是WPF中让我们真正能够开始体现其便利性的特征之一,而对以数据驱动的应用来讲,其重要性不言而喻。
基本数据绑定概念
不论要绑定什么元素,不论数据源的特性是什么,每个绑定都始终遵循下图所示的模型:
如上图所示,数据绑定实质上是绑定目标与绑定源之间的桥梁。该图演示以下基本的 WPF 数据绑定概念:
通常,每个绑定都具有四个组件:绑定目标对象、目标属性、绑定源,以及要使用的绑定源中的值的路径。例如,如果要将 TextBox 的内容绑定到 Employee 对象的 Name 属性,则目标对象是 TextBox,目标属性是 Text 属性,要使用的值是 Name,源对象是 Employee 对象。
目标属性必须为依赖项属性。大多数 UIElement 属性都是依赖项属性,而大多数依赖项属性(除了只读属性)默认情况下都支持数据绑定。(只有 DependencyObject 类型可以定义依赖项属性,所有 UIElement 都派生自 DependencyObject。)
尽管图中并未指出,但应该注意,绑定源对象并不限于自定义 CLR 对象。WPF 数据绑定支持 CLR 对象和 XML 形式的数据。举例来说,绑定源可以是 UIElement、任何列表对象、与 ADO.NET 数据或 Web 服务关联的 CLR 对象,或是包含 XML 数据的 XmlNode。
在阅读其他软件开发工具包 (SDK) 主题时,请务必记住一点:当您建立绑定时,您是将绑定目标绑定到 绑定源。例如,如果您要使用数据绑定在一个 ListBox 中显示一些基础 XML 数据,就是将 ListBox 绑定到 XML 数据。
若要建立绑定,请使用 Binding 对象。本主题的剩余部分会讨论与 Binding 对象相关的许多概念以及该对象的一些属性和用法。
数据流的方向
正如上文所述和上图中箭头所示,绑定的数据流可以从数据目标流向数据源(例如,当用户编辑 TextBox 的值时,源值会发生更改)和/或(如果绑定源提供正确的通知)从绑定源流向绑定目标(例如,TextBox 内容会随绑定源中的更改而进行更新)。
您可能希望应用程序使用户可以更改数据并将数据传播回源对象。或者,您可能不希望允许用户更新源数据。您可以通过设置 Binding 对象的 Mode 属性来对此进行控制。下图演示不同类型的数据流:
OneWay 绑定导致对源属性的更改会自动更新目标属性,但是对目标属性的更改不会传播回源属性。此绑定类型适用于绑定的控件为隐式只读控件的情况。例如,您可能绑定到如股票行情自动收录器这样的源,或许目标属性没有用于进行更改的控件接口(如表的数据绑定背景色)。如果无需监视目标属性的更改,则使用 OneWay 绑定模式可避免 TwoWay 绑定模式的系统开销。
TwoWay 绑定导致对源属性的更改会自动更新目标属性,而对目标属性的更改也会自动更新源属性。此绑定类型适用于可编辑窗体或其他完全交互式 UI 方案。大多数属性都默认为 OneWay 绑定,但是一些依赖项属性(通常为用户可编辑的控件的属性,如 TextBox 的 Text 属性和 CheckBox 的 IsChecked 属性)默认为 TwoWay 绑定。确定依赖项属性在默认情况下是单向绑定还是双向绑定的一种编程方式是使用 GetMetadata 获取属性的属性元数据,然后检查 BindsTwoWayByDefault 属性的布尔值。
OneWayToSource 与 OneWay 绑定相反;它在目标属性更改时更新源属性。一个示例方案是您只需要从 UI 重新计算源值的情况。
OneTime 绑定未在图中显示,该绑定会导致源属性初始化目标属性,但不传播后续更改。这意味着,如果数据上下文发生了更改,或者数据上下文中的对象发生了更改,则更改会反映在目标属性中。如果您使用的数据的当前状态的快照适于使用,或者这些数据是真正静态的,则适合使用此绑定类型。如果要使用源属性中的某个值初始化目标属性,并且事先不知道数据上下文,则也可以使用此绑定类型。此绑定类型实质上是 OneWay 绑定的简化形式,在源值不更改的情况下可以提供更好的性能。
请注意,若要检测源更改(适用于 OneWay 和 TwoWay 绑定),则源必须实现一种合适的属性更改通知机制(如 INotifyPropertyChanged)。
触发源更新的原因
TwoWay 或 OneWayToSource 绑定侦听目标属性的更改,并将这些更改传播回源。这称为更新源。例如,可以编辑文本框中的文本以更改基础源值。如上一节中所述,数据流的方向由绑定的 Mode 属性的值确定。
但是,源值是在您编辑文本的同时进行更新,还是在您结束编辑文本并将鼠标指针从文本框移走后才进行更新呢? 绑定的 UpdateSourceTrigger 属性确定触发源更新的原因。下图中右箭头的点演示 UpdateSourceTrigger 属性的角色:
如果 UpdateSourceTrigger 值为 PropertyChanged,则 TwoWay 或 OneWayToSource 绑定的右箭头指向的值会在目标属性更改时立刻进行更新。但是,如果 UpdateSourceTrigger 值为 LostFocus,则仅当目标属性失去焦点时,该值才会使用新值进行更新。
与 Mode 属性类似,不同的依赖项属性具有不同的默认 UpdateSourceTrigger 值。大多数依赖项属性的默认值都为 PropertyChanged,而 Text 属性的默认值为 LostFocus。这意味着,只要目标属性更改,源更新通常都会发生,这对于 CheckBox 和其他简单控件很有用。但对于文本字段,每次键击之后都进行更新会降低性能,用户也没有机会在提交新值之前使用退格键修改键入错误。这就是为什么 Text 属性的默认值是 LostFocus 而不是 PropertyChanged 的原因。
下表使用 TextBox 作为示例提供每个 UpdateSourceTrigger 值的示例方案:
数据绑定的关键是System.Windows.Data.Binding对象,它会把两个对象(UI对象与UI对象之间,UI对象与.NET数据对象之间)按照指定的方式粘合在一起,并在他们之间建立一条通信通道,绑定一旦建立,接下来的应用生命周期中它可以自己独立完成所有的同步工作。根据其应用场合的不同我们将在本文中从以下几个部分分别讨论:
· 对象间的绑定
· 绑定到集合
· 数据模板
· 向绑定添加规则和转换器
1. UI对象间的绑定
UI对象间的绑定,也是最基本的形式,通常是将源对象Source的某个属性值绑定 (拷贝) 到目标对象Destination的某个属性上。源属性可以是任意类型,但目标属性必须是依赖属性(Dependency Property)。通常情况下我们对于UI对象间的绑定源属性和目标属性都是依赖属性 (有些属性不是) ,因为依赖属性有垂直的内嵌变更通知机制,WPF可以保持目标属性和源属性的同步。
看个简单的例子是如何在XAML中实现数据绑定的:
<Window x:Class="Allan.WpfBinding.Demo.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Basic Bindings" Height="400" Width="700" Style="{StaticResource windowStyle}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="40" /> <RowDefinition Height="*" /> <RowDefinition Height="40" /> Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="5" HorizontalAlignment="Right"> <Button x:Name="btnBasicBinding" Content="Basic" Style="{StaticResource buttonStyle}">Button> <Button x:Name="btnCollectionBinding" Content="Collection" Style="{StaticResource buttonStyle}">Button> <Button x:Name="btnDataTemplate" Content="Data Template" Style="{StaticResource buttonStyle}">Button> <Button x:Name="btnAdvanceBindings" Content="Advance" Style="{StaticResource buttonStyle}">Button> <Button x:Name="btnExit" Content="Exit" Style="{StaticResource buttonStyle}">Button> StackPanel>
<StackPanel Grid.Row="1" HorizontalAlignment="Left"> <TextBox x:Name="txtName" Margin="5" Width="400" BorderThickness="0" Height="50" Text="Source Element">TextBox> <TextBlock x:Name="tbShowMessage" Margin="5" Width="400" Height="50" Text="{BindingElementName=txtName,Path=Text }" /> StackPanel> Grid> Window> |
· XAML绑定语法:
上边的代码我们将名为txtName的对象的Text属性作为源对象分别绑定给了两个TextBlock的Text属性。这里我们用了Binding关键字并指定了ElementName和Path,这两个就是指定源对象(Source)和源属性(Source Property). 通常我们在设定绑定时都用与StaticResource标记类似的语法{Binding… }并设置ElementName和Path属性:
Text=”{Binding ElementName=SourceObjectName, Path=SourceProperty}” |
·用Coding(C#)添加Binding
而对于C#里和绑定相关的代码,则看起来会罗嗦很多。但它们都同样的使用了Binding对象,然后指定PropertyPath的一个实例为源属性,然后可以有两个方法来加载绑定规则:
1. 调用FrameworkElement 或FrameworkContentElement对象的SetBinding方法
2. 调用BindingOperations.SetBinding静态方法
以下代码实现了和上边XAML文件类似的功能:
Binding binding = new Binding(); //设置源对象 binding.Source = txtName; //设置源属性 binding.Path = new PropertyPath("Text"); //添加到目标属性 this.tbShowMessage.SetBinding(TextBlock.TextProperty, binding); //or //BindingOperations.SetBinding(tbShowMessage, TextBlock.TextProperty, binding); |
用Coding(C#)移除Binding
当你在应用程序中某个地方添加了绑定,而在某个时候又不想这个绑定在接下来继续有效时,你可以有两种方式来断开这个绑定:
1. 用BindingOperations.ClearBinding静态方法。
例如BindingOperations.ClearBinding(currentTextBlock, TextBlock.TextProperty); BindingOperations同时还提供了ClearAllBindings方法,只需要传入要清除绑定的目标对象的名称,它就会将所有这个对象的绑定移除。
2. 简单的将目标属性设置为一个新的值。
这个简单的方法同样有效,可以断开与前边设置的binding的连接。简单的设置为任何值即可:如:currentTextBlock.Text = “it’s a new value.”;
· Binding对象的属性
Property |
Description |
Converter |
转换器 |
ElementName |
绑定的源对象 |
FallbackValue |
绑定无法返回有效值时的默认显示。 |
Mode |
绑定方式 |
Path |
属性 |
RelativeSource |
常用于自身绑定或者数据模板中来指定绑定的源对象。 |
Source |
源对象 |
StringFormat |
格式化表达式 |
UpdateSourceTrigger |
Sets the events on which binding will occur. |
ValidationRules |
验证规则 |
总结:对于对象间的绑定,绑定源为ElementName,Path为绑定源属性。ElementName必须为以下可选项之一:
DataContext |
DataContext是WPF最后才试图查找的源。一旦RelativeSource和Source对象都没有被设置,则会在逻辑树种向上搜寻。 |
RelativeSource |
用来标识和当前控件关联的对象,通常用于自我引用或数据模板。 |
Source |
数据提供者/对象 |
2. 绑定到集合
· 利用ItemsSource来绑定数据源
常用标记:{Binding Path =””} ItemSource DisplayMemberPath
通常来说这是我们在做以数据驱动为主的应用时最经常用到的绑定方式。WPF支持任何类型的.NET对象作为数据源绑定到WPF对象。对于所有的ItemsControl对象都有一个ItemsSource依赖属性,这是专门为数据绑定而准备的。ItemsSource的类型是IEnumerable,所以对于我们几乎所有的集合类型我们都可以轻易的改变成ItemsSource的源对象。通过以下语句我们可以将一个名为photos的集合赋予ListBox对象,并以显示Name属性的值:
ItemsSource=”(Binding {DynamicResource photos}” |
我们知道,依赖属性内建的垂直通知功能让UI对象间的绑定可以自己负责同步处理,但是对于.NET集合/对象来讲,它不具备这样的能力。为了让目标属性与源集合的更改保持同步,源集合必须实现一个叫INotifyCollectionChanged的接口,但通常我们只需要将集合类继承于ObservableCollection类即可。因为ObservableCollection实现了INotifyPropertyChanged和INotifyCollectionChanged接口。示例代码中我们这么去定义Photos集合类:
public class Photos : ObservableCollection |
· 利用DataContext来作为共享数据源
常用标记:{Binding Path=””} DataContext
顾名思义,DataContext就是数据上下文对象,它是为了避免多个对象共享一个数据源时重复的对所有对象显式地用binding标记每个Source/RelativeSource/ElementName,而把同一个数据源在上下文对象的某个范围内共享,这样当一个绑定没有显式的源对象时,WPF会便利逻辑数找到一个非空的DataContext为止。
例如我们可以通过以下代码给ListBox和Title设置绑定:
|
对于这些简单的绑定我们可以很灵活的组合他们的应用来达到我们的要求,这也是我们通常使用的方法。例如:
<Window.Resources> <local:Employee x:Key="MyEmployee" EmployeeNumber="123" FirstName="John" LastName="Doe" Department="Product Development" Title="QA Manager" /> Window.Resources> <Grid DataContext="{StaticResource MyEmployee}"> <TextBox Text="{Binding Path=EmployeeNumber}">TextBox> <TextBox Text="{Binding Path=FirstName}">TextBox> <TextBox Text="{Binding Path=LastName}" /> <TextBox Text="{Binding Path=Title}">TextBox> <TextBox Text="{Binding Path=Department}" /> Grid> |
总结:对于集合的绑定,通常会需要用到以下几个标记:
DisplayMemberPath |
指定源对象中被显示的属性。ToString()方法会被默认调用。 |
ItemsSource |
指定要显示的数据源 |
ItemsTemplate |
指定以什么样的格式来显示数据(类似于符合控件,可以在数据模板中利用多种控件来控制展现方式) |
Path |
数据源对象中的属性—控制显示 |
DataContext |
共享数据源 |
3. 数据模板 – Data Template
当源属性和目标属性为兼容的数据类型,且源所显示的东西正是你需要显示的东西时,数据绑定确实很简单,你只需要向Section 1中讲的来匹配对象关系即可。而通常情况下我们对数据绑定都要做一些定制,特别对于.NET对象的绑定,你需要将数据源按照不同的方式分割显示。Data Template就负责来完成这样的功能:按照预想的数据展现模式将数据源的不同部分显示,而其作为可以被复用的独立结构,一旦定义可以被添加到一个对象内部,将会创建一个全新的可视树。
数据模板通常会被应用到以下几类控件来填充其类型为DataTemplate的属性:
· 内容控件(Content Control):ContentTemplate属性,控制Content的显示
· 项控件(Items Control) : ItemTemplate属性,应用于每个显示的项
· 头控件(Header Content Control) : HeaderTemplate属性,控制Header的展现。
每个数据模板的定义都是类似的方式,你可以像设计普通的窗体一样来设计其展现的方式,而且他们共享数据模板父空间所赋予的绑定源。例如下边的代码我们用一个图片来替代ListBox中的每一项:
<ListBox x:Name="pictureBox" ItemsSource="{Binding}"ScrollViewer.HorizontalScrollBarVisibility="Disabled"> <ListBox.ItemTemplate> <DataTemplate> <Image Source="{Binding Path=FullPath}" Margin="3,8" Height="35"> <Image.LayoutTransform> <StaticResource ResourceKey="st"/> Image.LayoutTransform> <Image.ToolTip> <StackPanel> <TextBlock Text="{Binding Path=Name}"/> <TextBlock Text="{Binding Path=DateTime}"/> StackPanel> Image.ToolTip> Image> DataTemplate> ListBox.ItemTemplate> ListBox> |
最终的ListBox中每一项的展现将按照我们在数据模板中设定的样式以图片来显示:
通常数据模板是不需要被内联声明的,它可以被定义成一个资源存放在Application.Resources这样的全局资源辞典中,或者单独的Resource Dictionary中在多个元素间共享。
4. 向绑定添加规则和转换器
· 使用值转换器Value Converter
无论你的绑定XAML写得多么漂亮,所有的绑定值毫无疑问你都可以得到,但是它不总是可以满足你不经过任何程序变化显示出来就能满足要求的。例如对于本文示例代码的照片总数的显示,我们还想显示得更为智能一些:对于一些符合某种要求的数据我们将其背景显示为黄色,而对于有多于一条记录时我们显示15 Items,仅有一条时显示1 Item。这时Value Converter就派上用场了。
要定义一个Value Converter需要声明一个类让其继承于System.Windows.Data.IValueConverter接口,并实现其中的两个方法Convert和ConvertBack方法。
public class RawCountToDescriptionConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { // Let Parse throw an exception if the input is bad int num = int.Parse(value.ToString()); return num + (num == 1 ? " item" : " items"); }
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotSupportedException(); } } |
在XAML中声明资源,然后将其通过静态资源引用的方式赋予Binding对象的Converter属性。
<Window.Resources> <local:CountToBackgroundConverter x:Key="myConverter"/> <local:RawCountToDescriptionConverter x:Key="myConverter2"/> Window.Resources> <TextBlock x:Name="filePath" DockPanel.Dock="Top" Style="{StaticResource titleStyle}" Text="{Binding Count, Converter={StaticResource myConverter2}}">TextBlock> |
同样,我们可以对输入进行转换。如果数据的输入是被验证规则(如果有的话)标记为有效的,那么值转换器将会被调用,来对输入进行转换后反应出来。 (参考附件代码中的BindingConverter窗体)
· 向绑定添加规则
每个Binding对象都有一个ValidationRules属性,可以被设置为一个或多个派生自ValidationRule的对象,每个规则都会检查特定的条件并更具结果来标记数据的有效性。就像我们在ASP.NET中应用RequiredValidator, CustomValidator一样,你只需要定义自己的规则,WPF会在每次调用数据时(通常是TextBox等输入控件失去焦点)会调用验证检查。这些是在值转换器之前发生的,如果数据无效,它会标记此次更新无效,并将数据标记为无效—这是通过设置目标元素的Validation.HasError属性为true并触发Validation.Error事件(ValidationResult会被返回,并且其IsValid属性为false)。我们可以通过一个触发器来设定当数据无效时对用户的提示。例如下边的代码我们就通过定义一个JpgValidationRule,当数据无效时通过tooltip来提示用户输入无效。
public class JpgValidationRule : ValidationRule { public override ValidationResult Validate(object value, CultureInfo cultureInfo) { string filename = value.ToString();
// Reject nonexistent files: if (!File.Exists(filename)) { return new ValidationResult(false, "Value is not a valid file."); }
// Reject files that don’t end in .jpg: if (!filename.EndsWith(".jpg", StringComparison.InvariantCultureIgnoreCase)) { return new ValidationResult(false, "Value is not a .jpg file."); } else { return new ValidationResult(true, null); } } } |
上边的代码定义了我们验证的规则。接下来在XAML中来应用这个规则。我们将这个规则用来检测输入框中的数据是否合法:
<TextBox Style="{StaticResource validateTextBoxStyle}"> <TextBox.Text> <Binding UpdateSourceTrigger="PropertyChanged" Path="Department"> <Binding.ValidationRules> <local:JpgValidationRule/> Binding.ValidationRules> Binding> TextBox.Text> TextBox> |
当数据不合法时我们以什么样的方式来告诉用户呢?这里有两个方法可以做,一个是定义你自己的ErrorTemplate,另外一个是根据Trigger来设置一些可见信息。通常我们都可以来自己定义一些Error Provider和可以复用的ErrorTemplate,这个话题我们会在下一篇文章中讲。这里我们只让背景做改变并用tooltip来提示用户—显示的是ValidationRule返回的出错信息。因为都是控制显示的,所以定义成共用的Style:
<Style x:Key="validateTextBoxStyle" TargetType="{x:Type TextBox}"> <Setter Property="Width" Value="300" /> <Style.Triggers> <Trigger Property="Validation.HasError" Value="True"> <Setter Property="Background" Value="Red"/> <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/> Trigger> Style.Triggers> Style> |
总的来说,对于验证,我们常用一下几个属性来定义错误验证规则和错误展现方式:
· Errors – 错误信息集合
· HasError – 是否有错误出现.
· ErrorTemplate – 错误提示的展现方式.
· Binding.ValidationRules 绑定验证规则