转载自:李小龙的博客http://blog.163.com/bruce_lee04/blog/static/45755321201008102135814/
Windows Presentation Foundation (WPF) 数据绑定为应用程序提供了一种简单而一致的方法来显示数据以及与数据交互。元素可以以公共语言运行库 (CLR) 对象和 XML 的形式绑定到各种数据源的数据。ContentControl(如 Button)和 ItemsControl(如 ListBox 和 ListView)具有内置功能,使单个数据项或数据项集合可以进行灵活的样式设置。可以在数据之上生成排序、筛选和分组视图。
WPF 中的数据绑定功能与传统模型相比具有一些优势,包括本质上支持数据绑定的各种属性、灵活的数据 UI 表示形式,以及业务逻辑与 UI 的完全分离。
本主题首先讨论 WPF 数据绑定的基本概念,然后详细介绍 Binding 类以及数据绑定的其他功能的用法。
什么是数据绑定?
数据绑定是在应用程序 UI 与业务逻辑之间建立连接的过程。如果绑定具有正确设置并且数据提供正确通知,则当数据更改其值时,绑定到数据的元素会自动反映更改。数据绑定可能还意味着如果元素中数据的外部表现形式发生更改,则基础数据可以自动更新以反映更改。例如,如果用户编辑 TextBox 元素中的值,则基础数据值会自动更新以反映该更改。
数据绑定的一种典型用法是将服务器或本地配置数据放置到窗体或其他 UI 控件中。在 WPF 中,此概念得到扩展,包括了大量属性与各种数据源的绑定。在 WPF 中,元素的依赖项属性可以绑定到 CLR 对象(包括 ADO.NET 对象或与 Web 服务和 Web 属性相关联的对象)和 XML 数据。
有关数据绑定的示例,请看一看来自数据绑定演示的以下应用程序 UI:
上面是显示拍卖项列表的应用程序 UI。该应用程序演示数据绑定的以下功能:
ListBox 的内容绑定到 AuctionItem 对象的集合。AuctionItem 对象具有一些属性,如 Description、StartPrice、StartDate、Category、SpecialFeatures。
ListBox 中显示的数据(AuctionItem 对象)进行模板化,以便显示每个拍卖项的说明和当前价格。这是使用一个 DataTemplate 实现的。此外,每个项的外观取决于要显示的 AuctionItem 的 SpecialFeatures 值。如果 AuctionItem 的 SpecialFeatures 值为 Color,则该项具有蓝色边框。如果该值为 Highlight,则该项具有橙色边框和一个星号。
用户可以使用提供的 CheckBox 对数据进行分组、筛选或排序。在上面的图像中,选中了“Group by category”(按类别分组)和“Sort by category and date”(按类别和日期排序)CheckBox。您可能已经注意到数据是根据产品类别分组的,而且类别名称按字母顺序排序。这些项在每个类别中也是按照起始日期排序的,虽然从该图像中很难注意到这一点。这是使用集合视图 实现的。
当用户选中一个项时,ContentControl 会显示选定项的详细信息。这称为主从方案。
StartDate 属性的类型为 DateTime,该类型返回一个日期,包括精确到毫秒的时间。在此应用程序中,使用了一个自定义转换器,以便显示较短的日期字符串。
当用户单击“Add Product”(添加产品)按钮时,会出现下面的窗体:
用户可以编辑窗体中的字段,使用简略预览和详细预览窗格来预览产品清单,然后单击“submit”(提交)以添加新的产品清单。任何现有的分组、筛选和排序功能都会应用于新项。在这种特殊情况下,在上面图像中输入的项会作为 Computer 类别中的第二项显示。
“Start Date”(起始日期)TextBox 中提供的验证逻辑未在此图像中显示。如果用户输入一个无效日期(无效的格式或过去的日期),则会通过一个 ToolTip 和 TextBox 旁的一个红色感叹号来通知用户。数据验证一节讨论了如何创建验证逻辑。
在详细介绍数据绑定的上述不同功能之前,我们会先在下一节中讨论一些对理解 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 值的示例方案:
创建绑定
前面几节中讨论的一些概念可以概括为:使用 Binding 对象建立绑定,每个绑定通常都具有四个组件:绑定目标、目标属性、绑定源、要使用的源值的路径。本节讨论如何设置绑定。
请看下面的示例,其中的绑定源对象是一个名为 MyData 的类,该类在 SDKSample 命名空间中定义。出于演示的目的,MyData 类具有一个名为 ColorName 的字符串属性,该属性的值设置为“Red”。因此,此示例生成一个具有红色背景的按钮。
<DockPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:c="clr-namespace:SDKSample"> <DockPanel.Resources> <c:MyData x:Key="myDataSource"/> </DockPanel.Resources> <DockPanel.DataContext> <Binding Source="{StaticResource myDataSource}"/> </DockPanel.DataContext> <Button Background="{Binding Path=ColorName}" Width="150" Height="30">I am bound to RED!</Button> </DockPanel>
如果将此示例应用于基本关系图,则生成的图如下所示。这是一个 OneWay 绑定,因为 Background 属性在默认情况下支持 OneWay 绑定。
您可能会奇怪为什么会这样,即使 ColorName 属性为类型字符串,而 Background 属性为 Brush 类型。这是由于进行了默认类型转换,此类型转换在数据转换一节中进行了讨论。
指定绑定源
请注意,在上一个示例中,绑定源是通过设置 DockPanel 元素上的 DataContext 属性来指定的。Button 随后从 DockPanel(这是其父元素)继承 DataContext 值。在这里重复一下,绑定源对象是绑定的四个必需组件之一。因此,如果未指定绑定源对象,则绑定将没有任何作用。
可通过多种方法指定绑定源对象。在将多个属性绑定到相同源时,可以使用父元素上的 DataContext 属性。但是,在各个绑定声明上指定绑定源有时可能更为合适。对于上一个示例,可以不使用 DataContext 属性,而是通过在按钮的绑定声明上直接设置 Source 属性来指定绑定源,如下面的示例中所示:
<DockPanel.Resources> <c:MyData x:Key="myDataSource"/> </DockPanel.Resources> <Button Width="150" Height="30" Background="{Binding Source={StaticResource myDataSource}, Path=ColorName}">I am bound to RED!</Button>
除了在元素上直接设置 DataContext 属性、从上级继承 DataContext 值(如第一个示例中的按钮)、通过设置 Binding 上的 Source 属性来显式指定绑定源(如最后一个示例中的按钮),还可以使用 ElementName 属性或 RelativeSource 属性指定绑定源。当绑定到应用程序中的其他元素时(例如在使用滑块调整按钮的宽度时),ElementName 属性是很有用的。当在 ControlTemplate 或 Style 中指定绑定时,RelativeSource 属性是很有用的。
指定值的路径
如果绑定源是一个对象,则可使用 Path 属性指定要用于绑定的值。如果要绑定到 XML 数据,则可使用 XPath 属性指定该值。在某些情况下,可以使用 Path 属性,即使在数据为 XML 时。例如,如果要访问返回的 XmlNode(作为 XPath 查询的结果)的 Name 属性,则应使用 Path 属性和 XPath 属性。
请注意,虽然我们已强调要使用的值的 Path 是绑定的四个必需组件之一,但在要绑定到整个对象的情况下,要使用的值会与绑定源对象相同。在这些情况下,不指定 Path 比较合适。请看下面的示例:
<ListBox ItemsSource="{Binding}" IsSynchronizedWithCurrentItem="true"/>
上面的示例使用空绑定语法:{Binding}。在此情况下,ListBox 从父 DockPanel 元素继承 DataContext(此示例中未演示)。当未指定路径时,默认为绑定到整个对象。换句话说,在此示例中路径已被省略,因为要将 ItemsSource 属性绑定到整个对象。
除了绑定到集合以外,在希望绑定到整个对象,而不是仅绑定到对象的单个属性时,也可以使用此方案。例如,在源对象为类型字符串,并且您仅仅希望绑定到该字符串本身时。另一种常见情况是您希望将一个元素绑定到具有多个属性的一个对象。
请注意,您可能需要应用自定义逻辑,使数据对您的绑定目标属性有意义。自定义逻辑的形式可以是自定义转换器(如果默认类型转换不存在)。
Binding 和 BindingExpression
在详细介绍数据绑定的其他功能和用法之前,介绍 BindingExpression 类会十分有用。如前面几节所述,Binding 类是用于绑定声明的高级别类;Binding 类提供了很多属性,您可以利用这些类来指定绑定的特征。相关类 BindingExpression 是维持源与目标之间的连接的基础对象。一个绑定包含可以在多个绑定表达式之间共享的所有信息。BindingExpression 是无法共享的实例表达式,其中包含有关 Binding 的所有实例信息。
例如,请看下面的示例,其中 myDataObject 是 MyData 类的实例,myBinding 是源 Binding 对象,MyData 类是包含一个名为 MyDataProperty 的字符串属性的已定义类。此示例将 mytext(TextBlock 的实例)的文本内容绑定到 MyDataProperty。
//make a new source MyData myDataObject = new MyData(DateTime.Now); Binding myBinding = new Binding("MyDataProperty"); myBinding.Source = myDataObject; myText.SetBinding(TextBlock.TextProperty, myBinding);
Dim data1 As New MyData(DateTime.Now) Dim binding1 As New Binding("MyDataProperty") binding1.Source = data1 Me.myText.SetBinding(TextBlock.TextProperty, binding1)
您可以使用相同的 myBinding 对象来创建其他绑定。例如,可以使用 myBinding 对象将复选框的文本内容绑定到 MyDataProperty。在这种情况下,将有两个 BindingExpression 实例共享 myBinding 对象。
可以通过对数据绑定对象调用 GetBindingExpression 的返回值来获取 BindingExpression 对象。以下主题演示 BindingExpression 类的一些用法:
如何:从绑定目标属性获取绑定对象
如何:控制文本框文本更新源的时间
数据转换
在上面的示例中,按钮是红色的,因为其 Background 属性绑定到一个值为“Red”的字符串属性。可以这样做的原因是 Brush 类型上提供了一个类型转换器,可以将字符串值转换为 Brush。
要将此信息添加到创建绑定一节的图中,关系图如下所示:
但是,如果绑定源对象不是具有类型字符串的属性,而是具有类型 Color 的 Color 属性,该怎么办? 在这种情况下,为了使绑定正常工作,您需要先将 Color 属性值转换为 Background 属性所接受的值。您需要通过实现 IValueConverter 接口来创建一个自定义转换器,如下面的示例中所示:
[ValueConversion(typeof(Color), typeof(SolidColorBrush))] public class ColorBrushConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { Color color = (Color)value; return new SolidColorBrush(color); } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return null; } }
现在使用自定义转换器而不是默认转换,关系图如下所示:
在这里重复一下,由于要绑定到的类型中提供了类型转换器,因而可以使用默认转换。此行为取决于目标中可用的类型转换器。如果无法确定,请创建您自己的转换器。
下面提供了一些典型方案,在这些方案中,实现数据转换器是非常有意义的:
数据应根据区域性以不同方式显示。例如,可能需要根据在特定区域性中使用的值或标准,来实现货币转换器或日历日期/时间转换器。
使用的数据不一定会更改属性的文本值,但会更改其他某个值(如图像的源,或显示文本的颜色或样式)。在这种情况下,可以通过转换可能不合适的属性绑定(如将文本字段绑定到表单元格的 Background 属性)来使用转换器。
将多个控件或控件的多个属性绑定到相同数据。在这种情况下,主绑定可能仅显示文本,而其他绑定则处理特定的显示问题,但仍使用同一绑定作为源信息。
到目前为止,我们尚未讨论 MultiBinding(其目标属性具有绑定集合)。对于 MultiBinding,可以使用自定义 IMultiValueConverter 从绑定的值生成最终值。例如,可以从红色、蓝色和绿色的值来计算颜色,这些值可以是来自于相同或不同绑定源对象的值。
绑定到集合
绑定源对象可以视为其属性包含数据的单个对象,也可以视为通常组合在一起的多态对象的数据集合(如查询数据库的结果)。到目前为止,我们仅讨论了绑定到单个对象,但是绑定到数据集合是一个常见方案。例如,一个常见方案是使用 ItemsControl(如 ListBox、ListView 或 TreeView)来显示数据集合,如什么是数据绑定?一节中的应用程序所示。
幸运的是,基本关系图仍然适用。如果要将 ItemsControl 绑定到集合,则关系图如下所示:
正如此图中所示,若要将 ItemsControl 绑定到集合对象,应使用 ItemsSource 属性。可以将 ItemsSource 属性视为 ItemsControl 的内容。请注意,绑定是 OneWay,因为 ItemsSource 属性默认情况下支持 OneWay 绑定。
如何实现集合
可以枚举实现 IEnumerable 接口的任何集合。但是,若要设置动态绑定,以使集合中的插入或移除操作可以自动更新 UI,则该集合必须实现 INotifyCollectionChanged 接口。此接口公开一个事件,只要基础集合发生更改,都应该引发该事件。
WPF 提供 ObservableCollection<(Of <(T>)>) 类,它是公开 INotifyCollectionChanged 接口的数据集合的内置实现。请注意,为了完全支持将数据值从源对象传送到目标,支持可绑定属性的集合中的每个对象还必须实现 INotifyPropertyChanged 接口。
在实现自己的集合之前,请先考虑使用 ObservableCollection<(Of <(T>)>) 或一个现有的集合类,如 List<(Of <(T>)>)、Collection<(Of <(T>)>) 和 BindingList<(Of <(T>)>) 等。如果您有高级方案并且希望实现自己的集合,请考虑使用 IList,它提供可以按索引逐个访问的对象的非泛型集合,因而可提供最佳性能。
集合视图
一旦 ItemsControl 绑定到数据集合,您可能希望对数据进行排序、筛选或分组。若要执行此操作,可以使用集合视图,这是实现 ICollectionView 接口的类。
什么是集合视图?
可以将集合视图视为位于绑定源集合顶部的层,您可以通过它使用排序、筛选和分组查询来导航和显示源集合,所有这些操作都无需操作基础源集合本身。如果源集合实现了 INotifyCollectionChanged 接口,则 CollectionChanged 事件引发的更改将传播到视图。
由于视图不会更改基础源集合,因此每个源集合可以有多个关联的视图。例如,您可以有 Task 对象的集合。通过使用视图,可以通过多种不同的方式来显示相同数据。例如,您可能希望在页面左侧显示按优先级排序的任务,而在页面右侧显示按区域分组的任务。
如何创建视图
创建和使用视图的一种方式是直接实例化视图对象并将其用作绑定源。例如,请考虑在什么是数据绑定?一节中演示的数据绑定演示应用程序。 该应用程序的实现方式是将 ListBox 绑定到数据集合上的视图,而不是直接绑定到数据集合。下面的示例摘自数据绑定演示应用程序。CollectionViewSource 类是 CollectionView 的可扩展应用程序标记语言 (XAML) 代理。在此特定示例中,视图的 Source 绑定到当前应用程序对象的 AuctionItems 集合(类型为 ObservableCollection<(Of <(T>)>))。
<Window.Resource> ... <CollectionViewSource Source="{Binding Source={x:Static Application.Current}}", x:Key="listingDataView"/> ... </Window.Resource>
资源 listingDataView 随后用作应用程序中元素(如 ListBox)的绑定源:
<ListBox Name="Master" Grid.Row="2" Grid.ColumnSpan="3" Margin="8" ItemsSource="{Binding Source={StaticResource listingDataView}}"> ... </ListBox>
若要为同一集合创建另一个视图,可以创建另一个 CollectionViewSource 实例并为其提供另一个 x:Key 名称。
将视图用作绑定源并不是创建和使用集合视图的唯一方式。所有集合都具有一个默认集合视图。例如,对于所有实现 IEnumerable 的集合,CollectionView 都是默认视图对象。ListCollectionView 是实现 IList 的集合的默认视图对象,而 BindingListCollectionView 是用于那些实现 IBindingList 的集合的集合视图类。若要获取默认视图,请使用 GetDefaultView 方法。
排序
如前所述,视图可以将排序顺序应用于集合。如同在基础集合中一样,数据可能具有或不具有相关的固有顺序。通过集合上的视图,您可以根据您提供的比较条件来应用顺序或更改默认顺序。由于这是基于客户端的数据视图,因此一种常见情况是用户可能希望根据列对应的值,对多列表格数据进行排序。通过使用视图,可以应用这种用户驱动的排序,而无需对基础集合进行任何更改,甚至不必再次查询集合内容。
下面的示例演示什么是数据绑定?一节中的应用程序 UI 的“Sort by category and date”(按类别和日期排序)CheckBox 的排序逻辑:
private void AddSorting(object sender, RoutedEventArgs args) { // This sorts the items first by Category and within each Category, // by StartDate. Notice that because Category is an enumeration, // the order of the items is the same as in the enumeration declaration listingDataView.SortDescriptions.Add( new SortDescription("Category", ListSortDirection.Ascending)); listingDataView.SortDescriptions.Add( new SortDescription("StartDate", ListSortDirection.Ascending)); }
筛选
视图还可以将筛选器应用于集合。这意味着即使集合中可能存在一个项,此特定视图也仅用于显示整个集合的某个子集。可以根据条件在数据中进行筛选。例如,正如在什么是数据绑定? 一节中的应用程序的工作方式那样,“Show only bargains”(仅显示成交商品)CheckBox 包含了筛选出成交价为 25 美元或更高的项的逻辑。执行下面的代码可以在选中该 CheckBox 时将 ShowOnlyBargainsFilter 设置为 Filter 事件处理程序:
listingDataView.Filter += new FilterEventHandler(ShowOnlyBargainsFilter);
ShowOnlyBargainsFilter 事件处理程序具有以下实现:
private void ShowOnlyBargainsFilter(object sender, FilterEventArgs e) { AuctionItem product = e.Item as AuctionItem; if(product != null) { //Filter out products with price 25 or above if(product.CurrentPrice < 25) { e.Accepted = true; } else { e.Accepted = false; } } }
如果直接使用一个 CollectionView 类而不是 CollectionViewSource,则应使用 Filter 属性来指定回调。
分组
视图支持分组功能,此功能使用户能够将集合视图中的集合分区为逻辑组。这些组可以是显式的,其中的用户提供组列表,也可以是隐式的,其中的组依据数据动态生成。
下面的示例演示“Group by category”(按类别分组)CheckBox 的逻辑:
// This groups the items in the view by the property "Category" PropertyGroupDescription groupDescription = new PropertyGroupDescription(); groupDescription.PropertyName = "Category"; listingDataView.GroupDescriptions.Add(groupDescription);
当前记录指针
视图还支持当前项的概念。可以在集合视图中的对象之间导航。在导航时,您是在移动记录指针,该指针可用于检索存在于集合中的特定位置的对象。
请注意,当前记录指针的移动与应用于该集合的任何排序或筛选会相互产生某些影响。排序将当前记录指针保留在所选的最后一条记录上,但集合视图现在是围绕此指针重构的。(或许所选记录以前曾位于列表的开头,但现在所选记录可能在中间的某个位置。) 如果所选内容在筛选之后保留在视图中,则筛选操作会保留所选记录。否则,当前记录指针会设置到经过筛选的集合视图的第一条记录。
主-从绑定方案
当前项的概念不仅用于集合中项的导航,而且用于主-从绑定方案。再考虑一下什么是数据绑定?中的应用程序 UI。 在该应用程序中,ListBox 的所选内容决定了在 ContentControl 中显示的内容。换句话说,当选中一个 ListBox 项时,ContentControl 会显示选定项的详细信息。
通过将两个或更多控件绑定到同一视图可以轻松地实现主-从方案。下面这个摘自数据绑定演示的示例演示什么是数据绑定?中的应用程序 UI 上看到的 ListBox 和 ContentControl 的标记:
<ListBox Name="Master" Grid.Row="2" Grid.ColumnSpan="3" Margin="8" ItemsSource="{Binding Source={StaticResource listingDataView}}"> ... </ListBox> ... <ContentControl Name="Detail" Grid.Row="3" Grid.ColumnSpan="3" Content="{Binding Source={StaticResource listingDataView}}" ContentTemplate="{StaticResource detailsProductListingTemplate}" Margin="9,0,0,0"/>
请注意,这两个控件都绑定到同一个源,即 listingDataView 静态资源(请参见如何创建视图一节中的此资源的定义)。可以这样做的原因是当单一实例对象(此示例中为 ContentControl)绑定到一个集合视图时,该对象会自动绑定到该视图的 CurrentItem。请注意,CollectionViewSource 对象会自动同步货币与所选内容。如果列表控件没有像示例中那样绑定到 CollectionViewSource 对象,则您需要将其 IsSynchronizedWithCurrentItem 属性设置为 true 以达到此目的。
您可能已经注意到上面的示例使用了一个模板。实际上,如果不使用模板(由 ContentControl 显式使用的模板以及由 ListBox 隐式使用的模板),则数据不会按照我们希望的方式显示。现在,我们开始介绍下一节中的数据模板化。
数据模板化
如果不使用数据模板,则什么是数据绑定?一节中的应用程序 UI 将如下所示:
如上一节中的示例所示,ListBox 控件和 ContentControl 都绑定到 AuctionItem 的整个集合对象(更具体地说,是绑定到集合对象上的视图)。如果没有关于如何显示数据集合的特定说明,则 ListBox 会显示基础集合中的每个对象的字符串表示形式,而 ContentControl 会显示绑定到的对象的字符串表示形式。
若要解决该问题,应用程序应定义 DataTemplate。如上一节中的示例所示,ContentControl 显式使用 detailsProductListingTemplate DataTemplate。在显示集合中的 AuctionItem 对象时,ListBox 控件隐式使用下面的 DataTemplate:
<DataTemplate DataType="{x:Type src:AuctionItem}"> <Border BorderThickness="1" BorderBrush="Gray" Padding="7" Name="border" Margin="3" Width="500"> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="20"/> <ColumnDefinition Width="86"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Polygon Grid.Row="0" Grid.Column="0" Grid.RowSpan="4" Fill="Yellow" Stroke="Black" StrokeThickness="1" StrokeLineJoin="Round" Width="20" Height="20" Stretch="Fill" Points="9,2 11,7 17,7 12,10 14,15 9,12 4,15 6,10 1,7 7,7" Visibility="Hidden" Name="Star"/> <TextBlock Grid.Row="0" Grid.Column="1" Margin="0,0,8,0" Name="descriptionTitle" Style="{StaticResource smallTitleStyle}">Description:</TextBlock> <TextBlock Name="DescriptionDTDataType" Grid.Row="0" Grid.Column="2" Text="{Binding Path=Description}" Style="{StaticResource textStyleTextBlock}"/> <TextBlock Grid.Row="1" Grid.Column="1" Margin="0,0,8,0" Name="currentPriceTitle" Style="{StaticResource smallTitleStyle}">Current Price:</TextBlock> <StackPanel Grid.Row="1" Grid.Column="2" Orientation="Horizontal"> <TextBlcok Text="$" Style="{StaticResource textStyleTextBlock}"/> <TextBlock Name="CurrentPriceDTDataType" Text="{Binding Path=CurrentPrice}" Style="{StaticResource textStyleTextBlock}"/> </StackPanel> </Grid> </Border> <DataTemplate.Triggers> <DataTrigger Binding="{Binding Path=SpecialFeatures}"> <DataTrigger.Value> <src:SpecialFeatures>Color</src:SpecialFeatures> </DataTrigger.Value> <DataTrigger.Setters> <Setter Property="BorderBrush" Value="DodgerBlue" TargerName="border" /> <Setter Property="Foreground" Value="Navy" TargerName="descriptionTitle" /> <Setter Property="Foreground" Value="Navy" TargerName="currentPriceTitle" /> <Setter Property="BorderThickness" Value="3" TargerName="border" /> <Setter Property="Padding" Value="5" TargerName="border" /> </DataTrigger.Setters> </DataTrigger> <DataTrigger Binding="{Binding Path=SpecialFeatures}"> <DataTrigger.Value> <src:SpecialFeatures>Highlight</src:SpecialFeatures> </DataTrigger.Value> <DataTrigger.Setters> <Setter Property="BorderBrush" Value="Orange" TargerName="border" /> <Setter Property="Foreground" Value="Navy" TargerName="descriptionTitle" /> <Setter Property="Foreground" Value="Navy" TargerName="currentPriceTitle" /> <Setter Property="Visibility" Value="Visible" TargerName="star" /> <Setter Property="BorderThickness" Value="3" TargerName="border" /> <Setter Property="Padding" Value="5" TargerName="border" /> </DataTrigger.Setters> </DataTrigger> </DataTemplate.Triggers> </DataTemplate>
使用这两个 DataTemplate,生成的 UI 如什么是数据绑定?所示。 如屏幕快照所示,除了可以在控件中放置数据以外,使用 DataTemplate 还可以为数据定义引人注目的视觉效果。例如,上面的 DataTemplate 中使用了 DataTrigger,因而 SpecialFeatures 值为 HighLight 的 AuctionItem 会显示为带有橙色边框和一个星号。
数据验证
接受用户输入的大多数应用程序都需要具有验证逻辑,以确保用户输入了需要的信息。验证检查可以基于类型、范围、格式或其他应用程序特定的要求。本节讨论了数据验证在 WPF 中的工作方式。
将验证规则与绑定关联
使用 WPF 数据绑定模型可以将 ValidationRules 与 Binding 对象相关联。例如,下面是什么是数据绑定?一节中的 Add Product Listing(添加产品清单)“Start Price”(起始价格)TextBox 的 XAML:
<TextBox Name="StartPriceEntryForm" Grid.Row="2" Grid.Column="1" Style="{StaticResource textStyleTextBlock}" Margin="8,5,0,5"> <TextBox.Text> <Binding Path="StartPrice" UpdateSourceTrigger="PropertyChanged"> <Binding.ValidationRules> <ExceptionValidationRule /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox>
ValidationRules 属性采用 ValidationRule 对象的集合。ExceptionValidationRule 是内置的 ValidationRule,用于检查在绑定源属性的更新过程中引发的异常。在此特定示例中,绑定源属性为 StartPrice(属于整数类型),而目标属性为 TextBox.Text。当用户输入的值无法转换为整数时,将引发异常,这会导致将绑定标记为无效。
也可以通过从 ValidationRule 类派生和实现 Validate 方法来创建自己的验证规则。下面的示例演示什么是数据绑定?一节中的 Add Product Listing(添加产品清单)“Start Date”(起始日期)TextBox 使用的规则:
class FutureDateRule : ValidationRule { public override ValidationResult Validate(object value, CultureInfo cultureInfo) { DateTime date; try { date = DateTime.Parse(value.ToString()); } catch (FormatException) { return new ValidationResult(false, "Value is not a valid date."); } if(DateTime.Now.Date > date) { return new ValidationResult(false, "Please enter a date in the future."); } else { return ValidationResult.ValidResult; } } }
StartDateEntryForm TextBox 使用此 FutureDateRule,如下面的示例中所示:
<TextBox Name="StartDateEntryForm" Grid.Row="3" Grid.Column="1" Validation.ErrorTemplate="{StaticResource validationTemplate}" Style="{StaticResource textStyleTextBlock}" Margin="8,5,0,5"> <TextBox.Text> <Binding Path="StartDate" UpdateSourceTrigger="PropertyChanged" Converter="{StaticResource dateConverter}"> <Binding.ValidationRules> <src:FutureDateRule /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox>
请注意,由于 UpdateSourceTrigger 值为 PropertyChanged,因此在每次键击时,绑定引擎都会更新源值,这意味着该引擎还会在每次键击时检查 ValidationRules 集合中的每条规则。我们会在“验证过程”一节中进行更深入的讨论。
提供视觉反馈
如果用户输入的值无效,则您可能希望在应用程序 UI 上提供一些有关错误的反馈。提供这种反馈的一种方式是将 Validation..::.ErrorTemplate 附加属性设置为自定义 ControlTemplate。如上一小节中所示,StartDateEntryForm TextBox 使用一个名为 validationTemplate 的 ErrorTemplate。下面的示例显示 validationTemplate 的定义:
<ControlTemplate x:Key="validationTemplate"> <DockPanel> <TextBlock Foreground="Red" FontSize="20">!</TextBlock> <AdornedElementPlaceholder /> </DockPanel> </ControlTemplate>
AdornedElementPlaceholder 元素指定要装饰的控件应放置的位置。
此外,您可能还要使用 ToolTip 显示错误消息。StartDateEntryForm 和 StartPriceEntryForm TextBox 都使用样式 textStyleTextBox,该样式创建一个显示错误消息的 ToolTip。下面的示例显示 textStyleTextBox 的定义。当绑定元素的属性的一个或多个绑定发生错误时,附加属性 Validation.HasError 为 true。
<Style x:Key="textStyleTextBox" TargetType="TextBox"> <Setter Property="Foreground" Value="#333333" /> <Setter Property="MaxLength" Value="40" /> <Setter Property="Width" Value="392" /> <Style.Triggers> <Trigger Property="Validation.HasError" Value="true"> <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/}" </Trigger> </Style.Triggers> </Style>
使用自定义 ErrorTemplate 和 ToolTip,StartDateEntryForm TextBox 在发生验证错误时会如下所示:
如果您的 Binding 具有关联验证规则,但是您未在绑定控件上指定 ErrorTemplate,则在出现验证错误时会使用默认 ErrorTemplate 来通知用户。默认 ErrorTemplate 是一个在装饰器层中定义红色边框的控件模板。使用默认 ErrorTemplate 和 ToolTip,StartPriceEntryForm TextBox 的 UI 在发生验证错误时会如下所示:
有关如何提供逻辑以验证对话框中的所有控件的示例,请参见对话框概述中的“自定义对话框”一节。
验证过程
由于数据验证与从目标到源的更新有关,因此它仅应用于 TwoWay 和 OneWayToSource 绑定。每次将输入值传输到绑定源属性时会发生验证。在这里重复一下,导致源更新的原因取决于 UpdateSourceTrigger 属性的值,如触发源更新的原因一节中所述。
下面的插图提供了在数据绑定过程中适合进行验证的位置的可视表示形式:
如上图所示,验证在调用转换器之前从目标到源的值传输过程中发生。下面将介绍验证 过程,如上面的关系图中所标记的那样:
在将值从目标属性传输到源属性时,数据绑定引擎首先移除可能已添加到绑定元素的 Validation.Errors 附加属性的任何 ValidationError。然后,数据绑定引擎检查是否为该 Binding 定义了自定义 ValidationRule;如果已经定义,则它将调用每个 ValidationRule 上的 Validate 方法,直到其中一个规则出错或者全部规则都通过为止。
如果某个自定义规则未通过,则绑定引擎会创建一个 ValidationError 对象,并将该对象添加到绑定元素的 Validation.Errors 集合。如果 Validation.Errors 不为空,则元素的 Validation.HasError 附加属性会设置为 true。此外,如果 Binding 的 NotifyOnValidationError 属性设置为 true,则绑定引擎将引发该元素上的 Validation.Error 附加事件。
如果所有规则都通过,则绑定引擎会调用转换器(如果存在)。
如果转换器通过,则绑定引擎会调用源属性的 setter。
如果绑定具有与其关联的 ExceptionValidationRule,并且在步骤 4 中引发异常,则绑定引擎将检查是否存在 UpdateSourceExceptionFilter。您可以选择使用 UpdateSourceExceptionFilter 回调来提供用于处理异常的自定义处理程序。如果未对 Binding 指定 UpdateSourceExceptionFilter,则绑定引擎将对异常创建 ValidationError 并将其添加到绑定元素的 Validation.Errors 集合。
还应注意,任何方向(目标到源或源到目标)的有效值传输操作都将清除 Validation.Errors 附加属性。
调试机制
可以在绑定相关对象上设置附加属性 PresentationTraceSources..::.TraceLevel 以接收有关特定绑定的状态的信息。