提高大列表的性能
如果处理大量数据——例如,数万条记录而不止几百条–您知道良好的数据绑定系统不仅仅需要绑定功能,还需要能够处理超大量的数据而不会严重降低显示速度或消耗大量的内存幸运的是,WPF 优化了其列表控件以为您提供帮助。
所有WOF列表控件(所有继承自ItemsControl
的控件)都支持这些增强特性,包括低级的ListBox
和ComboBox
虚拟化
WPF列表控件提供的最重要功能是UI虚拟化(UI virtualization
),UI虚拟化是列表仅为当前显示项创建容器对象的一种技术。 例如,如果有一个具有50 000条记录的ListrBox控件,但可见区域只能包含30条记录,ListBox控件将只创建30个ListBoxItem对象(为了确保良好的滚动性能,会再增加即可ListBoxItem对象)。如果ListBox控件不支持UI虚拟化,就需要生成全部50 000个ListBoxItem对象,这显然需要占用更多的内存。更有意义的是,分配这些对象需要的时间能够明显感觉到,当代码设置ListBox.ItemsSource属性时这会短暂锁定应用程序。
UI虚拟化支持实际上没有呗构建进ListBox
或ItemsControl
类。相反,而是被硬编码到VirtualizingStackPanel
容器,除增加了虚拟化支持,该面板和StackPanel
面板的功能类似。 ListBox、ListView 以及 DataGrid 都自动使用 VirtualizingStackPanel 面板来布局它们的子元素。所以,为了获得虚拟化支持,不需要采取任何额外的步骤。然而,ComboBox 类使用标准的没有虚拟化支持的StackPanel面板。如果需要虚拟化支持,就必须明确地通过提供新的ItemsPanelTemmplate来添加虚拟化支持,如下所示:
1 | <ComboBox> |
TreeView是另一个支持虚拟化的控件,当在默认情况下,它关闭该支持。问题是在早期的WPF发布版本中,VirtualizingStackPanel
面板不支持层次化数据。现在虽然支持,但TreeView禁用了该特性以确保向后兼容性。幸运的是,只通过设置一个属性即可启用该特性,在包含大量数据的树控件中总是推荐启用该特性:
1 | <TreeView VirtualizingStackPanel.IsVirtualizing="True" .../> |
从技术角度该,VirtualizingStackPanel
继承自抽象类VirtualizingPanel
。如果想要使用不同类型的虚拟化面板,比如支持虚拟化的Grid面板,就需要成第三方组件供应商那里购买
有许多因素可能会破坏UI虚拟化支持,而且有时是意想不到的:
- 在ScrollViewer中放置列表控件。ScrollViewer为其子内容提供了一个窗口。问题是为子内容提供了无限的“虚拟”空间。在这个虚拟空间中,ListBox以完整尺寸渲染自身,显示所有子项。副作用是,每项在内存中都有各自的ListBoxItem。只要将ListBox控件放入不会视图限制其尺寸的容器中,就会发生这一问题:例如,如果将ListBox控件放到StackPanel面板而不是Grid面板中,也会发生类似问题。
- 改变列表控件的模板并且没有使用ItemsPresenter。ItemsPresenter使用ItemsPanelTemplate,该模板指定了
VirutalizingStackPanel
面板。如果破坏了这种关系或自己改变了ItemsPanelTemplate,从而使其不使用VirtualizingStackPanel
面板,将会丢失虚拟化特性。 - 不实用数据绑定。这应当是显而易见的,当如果通过编程填充列表——例如,通过动态创建需要的ListBoxItem对象——那么不会发生虚拟化。当然,个考虑使用自己的优化策略,例如创建所需的对象并只在需要时创建
如果有一个大列表,需要避免这些问题以确保得到良好的性能。
即使当使用U虚拟化时,仍然必须为实例化内存中的数据对象付出代价。例如,在具有50000 项的 ListBox控件示例中,仍有50000个数据对象,每个对象具有与产品、客户、订单记录或其他内容相关的不同数据。如果希望优化应用程序的这一部分,可考虑使用数据虚拟化(data virtualization)–每次只获取一批记录的一种技术。数据虚拟化是更复杂的技术,因为它假定检索数据的代价比保存数据的代价更低。根据数据的大小和检索数据所需的时间,这不一定是正确的。例如,如果当用户在列表中滚动时,应用程序不断地连接到网络数据库以获取更多的产品信息,最终结果会降低滚动性能,并会增加数据库服务器的负担。
当前,WPF没有提供任何支持数据虚拟化的控件或类。然而,这不会阻止且以及开发人员创建这一缺失的功能:假装具有所有项的“伪”集合,但直到控件需要数据时才成后台数据源中查询数据。
项容器再循环
通常当滚动支持虚拟化列表时,控件不断地创建新的项容器对象以保存新的可见项。例如,当在具有 50 000个项的 ListBox 控件中滚动时,ListBox控件将生成新的 ListBoxItem 对象。但如果启用了项容器再循环,ListBox 控件将只保持少量 ListBoxItem 对象存活,并当滚动时通过新数据加载这些 ListBoxItem 对象,从而重复使用它们。
1 | <ListBox VirtualizingStackPanel.VirtualizationMode="Recycling" .../> |
**项容器再循环提高了滚动性能,降低了内存消耗量,因为垃圾收集器不需要查找旧的项对象并释放它们。**通常,为确保向后兼容,对于除DataGrid之外的所有控件,该特性默认是禁用的。如果有一个大列表,应当总是启用该特性。
缓存长度
如前所述,VirtualizingStackPanel
创建了即可超过其显示范围的附加项。这样,在开始滚动时,可以立即显示这些项。
在以前的WPF版本中,将多个附加项硬编码到VirtualizlingStackPanel
中。当在WPF4.5中,您个使用CacheLength
和CacheLengthUnit
这两个VirtualizlingStackPanel
属性进一步调整精确数量。CacheLengthUnit
允许选择如何指定附加项的数量:项数、页数(其中,单页包含适用于控件可视“窗口”的所有项)或像素数(如果项显示不同大小的图片,这将是合理选择)。
默认的CacheLength
和CacheLengthUnit
属性在当前可见项之前和之后存储项的附加页,如下所示:
1 | <ListBox VirtualizingStackPanel.CacheLength="1" VirtualizingStackPanel.CacheLengthUnit="Page" /> |
下面的代码正在在当前可见项之前存储100项,在当前可见项之后存储100项:
1 | <ListBox VirtualizingStackPanel.CacheLength="100" VirtualizingStackPanel.CacheLengthUnit="Item" /> |
下面的代码在当前可见项之前存储100项,在当前可见项之后存储500项(原因可能是您预估用户将耗费大部分时间向下滚动,而不是向上滚动):
1 | <ListBox VirtualizingStackPanel.CacheLength="100,500" VirtualizingStackPanel.CacheLengthUnit="Item" /> |
有必要指出,附加项的缓存用背景来填充。这意味着,VirtualizingStackPanel
将立即显示创建的可见项集。此后,VirtualizingStackPanel
将开始在优先级较低的后台线程上填充缓存,因此不能锁定应用程序。
延迟滚动
当为进一步提高滚动性能,可开启延迟滚动(deferred scrolling)特性。使用延迟滚动特性,用户在滚动条上拖动滚动滑块时不会更新列表显示。**只有当用户释放了滚动滑块时才刷新。**比较起来,当使用常规滚动时,在拖动的同时会刷新列表,从而使列表显示正在改变的位置。
与为列表控件使用项容器再循环一样,需要明确地启用延迟滚动特性:
1 | <ListBox ScrollViewer.IsDeferredScrollingEnabled="True" /> |
显然,需要在响应性和易用性之间取得平衡。如果有一个复杂的模板和大量数据,对于提高速度可能更愿意使用延迟滚动特性。但与此相反,当滚动时用户可能更愿意能够查看目前滚动到了什么位置。
VirtualizingStackPanel通常使用基于项的滚动(item-based scrolling)。这意味着当向下滚动少许时,下一项将显示出来。无法滚动查看项的一部分。无论是单击滚动条,单击滚动箭头,还是调用诸如 ListBox.ScrolllntoViewO的方法,在面板上至少会滚动一个完整项。
然而,可通过将 VirtualizingStackPanel.ScrollUnit 属性设置为 Pixel 来覆盖该行为,并使用基于像素的滚动:
1 | <ListBox VirtualizingStackPanel.ScrollUnit="Pixel" /> |
应该根据在列表中显示的内容类型以及个人爱好,在“基于项的滚动”与“基于像素的滚动”之间加以选择。一般而言,基于像素的滚动更流畅,因为它允许使用较小的滚动间隔;而基于项的滚动更清晰,因为可看到项的全部内容。
验证
在任何数据绑定中,另一个要素是验证(validation
)——换句话说,是指用于捕获非法数值并拒绝这些非法数值的逻辑。可直接在控件中构建验证(例如,通过响应文本框中的输入并拒绝非法字符),但这种低级的方法限制了灵活性。
供了以下两种方法用于捕获非法值:幸运的是,WPF提供了能与前面讨论过的数据绑定系统紧密协作的验证功能。验证另外提
- 可在数据对象中引发错误。为告知WPF发生了错误,只需要成属性设置过程中跑出异常。通常,WPF会忽略所有在设置属性时跑出的异常,但可以进行配置,从而显示更有帮助的可视化指示。另一种选择是在自定义的数据类中实现
INotifyDataErrorInfo
或IDataErrorInfo
接口,从而可得到错误的功能而不会抛出异常。 - 个在绑定级别上定义验证。这种方法个获得使用相同验证的灵活性,而不必考虑使用的是哪个输入控件。更好的是,因为是在不同的类中定义验证,所以个很容易地在存类似数据类型的多个绑定中重用验证。
如果数据对象已经在它们的属性设置过程中硬编码了验证逻辑并且希望使用该逻辑,通常将使用第一种方法。当第一次定义验证逻辑,并希望在不同上下文和不同控件中重用时,将使用第二种方法。然而,一些开发人员同时选用这两种方法。他们在数据对象中使用验证预防一小部分基本的错误,并在绑定中使用验证捕获更大范围的用户输入错误。
只有当来自目标的值正被用于更新源时才会应用验证——换句话说,只有当使用TwoWay
模式或OneWayToSource
模式的绑定时才应用验证
在数据对象中进行验证
一些开发人员直接在数据对象中构建错误检查逻辑,如下:
1 | private decimal _unitCost; |
这个示例中显示的验证逻辑防止使用负的价格值,但不能为用户提供任何与问题相关的反馈信息。正如在前面学过的,WPF 会不加通告地忽略当设置和获取属性时发生的数据绑定错误。对于这种情况,用户无法知道更新已经被拒绝。实际上,非法的值仍然保留在文本框中——只是没有被应用于绑定的数据对象。为改善这一状况,需要借助于ExceptionValidationRule
验证规则,稍后将介绍该验证规则。
ExceptionValidationRule验证规则
ExceptionValidationRule
是预先构建的验证规则,它项WPF报告所有异常。要使用ExceptionValidationRule
验证规则,必须将它添加到Binding.ValidationRules
集合中,如下所示:
1 | <TextBox Width="120"> |
这个示例同时使用了值转换器和验证规则。通常是在转换值之前执行验证,但ExceptionValidationRule 验证规则是一个例外。它捕获在任何位置发生的异常,包括当编辑的值不能转换成正确数据类型时发生的异常、由属性设置器抛出的异常以及由值转换器抛出的异常。
那么,当验证失败时会发生什么情况? System.Windows.Controls.Validation
类的附加属性记录下了验证错误。对于每个失败的验证规则,WPF采取以下三个步骤:
- 在绑定的元素上(在此是TextBox控件),将
Validation.HasError
附加属性设置为true。 - 创建包含错误细节的
ValidationError
对象(作为ValidationRule.Validate()
方法的返回值),并将该对象添加到关联的Validation.Erros
集合中 - 如果
Bindfing.NotifyOnValidationError
属性被设置为true,WPF就在元素上引发Validation.Error
附加事件
当发生错误时,绑定控件的可视化外观也会发生变化。当控件的 Validation.HasError 属性被设置为 true 时,WPF 自动将控件使用的模板切换为由 Validation.ErorTemplate 附加属性定义的模板。在文本框中,新模板将文本框的轮廓改成一条细的红色边框。
在大多数情况下,您会希望以某种方式增强错误指示,并提供与引发问题的错误相关的特定信息。可使用代码处理 Error 事件,或提供自定义控件模板,从而提供不同的可视化指示信息。但在执行这些任务之前,有必要分析一下 WPF 提供的其他两种捕获错误的方式—— 通过使用数据对象中的 INotifyDataErrorInfo 或 IDataErrorInfo 接口以及通过编写自定义验证规则捕获错误。
INotifyDataErrorInfo接口
许多面向对象的支持者更愿意引发异常来提示用户输入错误,这样做的原因有很多。例如,用户输入错误并非异常条件,错误条件可能依赖于多个属性值之间的交互,以及有时不应立即丢弃错误值而值得保留它们以便进一步加以处理。WPF提供了两个接口,允许您构建报告错误的对象而不会抛出异常,这两个接口名为IDataErrorInfo
和 INotifyDataErrorInfo
。
IDataErorInfo 和 INotifyDataErrorInfo 接口具有共同的目标,即用更加人性化的错误通知系统替换未处理的异常。IDataErrorInf0 是初始的错误跟踪接口,可追溯至第一个,NET版本,WPF包含它是为了达到向后兼容的目的。INotifyDataErrorInfo 接口具有类似的作用,但界面更丰富,是针对 Silverlight 创建的,并且已移植到了 WPF 4.5。它还支持其他功能,如每个属性多个错误以及异步验证。
下面演示如何使用INotifyDataErrorInfo
接口来检测Product对象存在的问题。
1 | public class Product : ObservableObject, INotifyDataErrorInfo |
INotifyDataErrorInfo
接口需要三个成员。ErrorChanged
事件在添加或删除错误时引发。HasErros
属性返回true或false来指示数据对象是否包含错误。最佳选择是使用私有集合,如下所示:
1 | private Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>(); |
需要了解两个事实。首先,该集合初看起来有些怪异。为理解其中的原因,INotifyDataErrorInfo 接口要求将错误链接到特定属性。其次,每个属性可以有多个错误。要跟踪此错误信息,最简单的方法是使用 Dictionary<T,K>集合,按属性名为该集合编写索引
然后,可使用功能完备的错误对象,将多个错误信息片段绑定在一起,包括文本消息、错误代码和严重级别等详情。
准备好该集合后,只需要在错误发生时添加即可(如果错误得到纠正,就删除错误信息)。为简化该过程,该例中的Product类添加了一堆名为SetErros()
和ClearErrors()
的私有方法:
下面显示了错误处理逻辑,这段代码确保将 Product.ModeINumber 属性限制为包含字母和数字的字符串(不允许使用标点符号、空格以及其他特殊字符):
1 | public class Product : ObservableObject, INotifyDataErrorInfo |
1 | <Window |
为告知 WPF 使用 INotifyDataErrorInfo 接口,并通过该接口在修改属性时检査错误,绑定的 ValidatesOnNotifyDataErrors 属性必须为 true。
从技术角度看,并非一定要明确设置 ValidatesOnNotifyDataErrors,因为默认情况下其值为true(类似的 ValidatesOnDataErrors属性与IDataErrorlnfo 接口一起使用,该属性与ValidatesOnNotifyDataErrors 是不同的)。但最好还是明确设置,以便清晰地表明准备在标记中使用它。
另外,可通过创建数据对象来综合使用这两种方法;数据对象为某些错误类型抛出异常并使用 IDataErrorlnfo 或 INotifDataErrorInfo 报告其他错误。但务必记住,这两种方法差异极大。当触发异常时,不会在数据对象中更新属性。但当使用 IDataErrorInfo或INotifyDataErrorinfo接口时,允许使用非法值,但会标记出来。数据对象会被更新,但您可使用通知和BindingValidationFailed 事件告知用户。
自定义验证规则
应用自定义验证规则的方法和应用自定义转换器的方法类似。该方法定义继承自System.Windows.Controls.ValidationRule
类,并为了执行验证而重写Validate()
方法。如有必要,个添加接受其他细节的属性,个使用这些属性影响验证(例如,用于检查文本的验证规则个包含Boolean类型的CaseSensitive属性)。
下面是一条完整的验证规则,该规则将decimal数值限制在指定的最小值和最大值之间。因为这条验证规则用于货币数值,所以在默认情况下,最小值是0,而最大值时decimal类型能够容纳的最大值。然而,为了获得最大的灵活性,个通过属性来配置这些细节:
1 | public class PositivePriceRule : ValidationRule |
注意,验证逻辑使用了 Decimal.Parse()方法的一个重载版本,该重载版本接受一个NumberStyles 枚举值。这是因为验证总在转换之前进行。**如果为同一字段同时应用验证器和转换器,就需要确保当存在货币符号时能够成功地进行验证。**验证逻辑的成败通过返回的ValidationResult对象标识。IsValid 属性指示验证是否成功,并且如果验证不成功,ErrorContent属性会提供描述问题的对象。在这个示例中,错误内容被设置为将会显示在用户界面中的字符串,这是最常用的方法。
一旦完善自定义的验证规则,就准备好通过验证规则添加到Binding.ValidationRules
集合中来将之关联到元素
。下面是使用PositivePriceRule验证规则的一个示例,该规则的Maximum属性被设置为999.99:
1 | <ControlTemplate x:Key="displayErrorMsgFomatte"> |
Binding.ValidationRules
集合个包含任意数量的验证规则。将值提交到源时,WPF将顺序检查每个验证规则(情记住,当文本框拾失去焦点时文本框的值被提交到源,除非使用UpdateSourceTrigger
属性另行指定)。如果所有验证都成功了,WPF接着回调用转换器(如果存在的话)并为源应用值。
当使用 PositivePriceRule 验证规则执行验证时,其行为和使用 ExceptionValidationRule 验证规则的行为相同——文本框使用红色轮廓,设置 HasError 和 Errors 属性,并引发 Error 事件。为给用户提供一些更有帮助作用的反馈信息,需要添加一些代码或自定义 ErrorTemplate 模板。
自定义验证规则可非常特殊,从而可用于约束特定的属性,或更为通用,从而可在各种情况下重用。例如,可很容易地创建一条自定义验证规则,借助于NET提供的 System.TextRegularExpression.Regex 类,使用指定的正则表达式验证字符串。根据使用的正则表达式,可对各种基于模式的文本数据使用这条验证规则,如电子邮件地址、电话号码、IP地址以及邮政编码。
响应验证错误
有关用户接收到错误的唯一提示是在违反规则的文本框周围的红色轮廓。为提供更多信息,个处理Error
事件,当存储或清除错误时会引发该事件,当前提是首先必须确保已将Binding.NotifyOnValidationError
属性设置为true。
1 | <Binding NotifyOnValidationError="True" Path="UnitCost"> |
Error事件是使用冒泡策略的路由事件,所有个通过在父容器中关联事件处理程序来为对哦可控件处理Error事件,如下所示:
1 | <Grid Validation.Error="validationError"> |
1 | private void validationError(object sender, ValidationErrorEventArgs e) |
ValidationErrorEventArgs.Error属性提供了一个 ValidationError 对象,该对象将几个有用的细节捆绑在一起,包括引起问题的异常(Exception)、违反的验证规则(ValidationRule)、关联的绑定对象(BindingInError)以及 ValidationRule 对象返回的任何自定义信息(ErrorContent)。
**如果正使用自定义的验证规则,几乎总会选择在 ValidationError.ErrorContent属性中放置错误信息。如果使用 ExceptionValidationRule 验证规则,ErrorContent 属性将返回相应异常的Message 属性。**然而,存在一个问题,如果是因为数据类型不能转换为正确的值而引起的异常,ErrorContent 属性会如您所期望的那样工作,并报告发生了问题。但如果在数据对象的属性设置器中抛出了异常,那么异常会被封装到 TargetInvocationException对象中,并且 ErrorContent属性提供来自 TargetInvocationException.Message属性的文本,内容是“Exception has been thrown这段文本内容实际没有什么作用。by the target of an invocation.
因此,如果正在使用属性设置器引发异常,就需要添加代码来检査TargetlnvocationException对象的 ImnnerException属性。如果不是 null,就可以检索原始异常对象,并使用原始异常对象的Message属性而不是使用ValidationError.ErrorContent 属性。
获取错误列表
在某些情况下,您可能希望获取当前窗口(或窗口中的给定容器)中所有未处理错误的列表这项任务较简单——需要做的所有工作就是遍历元素树,测试每个元素的 Validation.HasError属性。
下面的代码示例演示一个专门查找TextBox
对象中非法数据的示例。这个示例使用地柜代码遍历整个元素层次。同时将错误信息聚集到一条单独的消息中,然后显示给用户:
1 | private void cmdOk_Click(object sender, RoutedEventArgs e) |
显示不同的错误指示符号
为最大限度地利用 WPF 验证,您可能希望创建自己的错误模板,以适当的方式标识错误。乍一看,这像是一种报告错误的低级方法–毕竟,可使用标准的控件模板详细地自定义控件的构成。然而,错误模板和普通控件模板是不同的。
**错误模板使用的是装饰层,装饰层是位于普通窗口内容之上的绘图层。使用装饰层,可添加可视化装饰来指示错误,而不用替换控件背后的控件模板或改变窗口的布局。**文本框的标准错误模板通过在相应文本框的上面添加红色的 Border 元素(背后的文本框没有发生变化)来指示发生了错误。可使用错误模板添加其他细节,如图像、文本或其他能吸引用户关注问题的图形细节。
下面的标记显示了一个示例,该示例定义了一个错误模板,该模板使用绿色边框并在具有非法输入的控件的旁边太廉价一个星号。该模板被封装进一条样式规则中,从而个自动将之应用到当前窗口的所有文本框:
1 | <Style TargetType="{x:Type TextBox}"> |
AdornedElementPlaceholder
是支持这种技术的粘合剂。它代表控件自身,位于元素层中。通过使用AdornedElementPlaceholder
元素,能在文本框的背后安排自己的内容.
因此,在该例中,边框被直接放在文本框上,而不管文本框的尺寸是多少。在这个示例中,星号放在右边。最令人满意的是,新的错误模板内容叠加在已存在的内容之上从而不会在原始窗口的布局中触发任何改变(实际上,如果不小心在装饰层中包含了过多内容最终会改变窗口的其他部分)。
如果希望使错误模板叠加显示在元素之上(而不是位于元素的周围),可在 Grid 控件的同一个单元格中同时放置自定义的内容和 AdornerElementPlaceholder 元素。此外,也可以不用AdormerElementPlaceholder 元素,但这样就会丧失在元素之后精确定位自定义内容的能力。
错误模板仍存在一个问题–没有提供任何有关错误的附加信息。为显示这些细节,需要使用数据绑定提取它们。一个好方法是使用第一个错误的错误内容,并将其用作自定义错误指示器的工具提示文本。下面的模板实现了这一功能:
1 | <Style TargetType="{x:Type TextBox}"> |
绑定表达式有些复杂,需要注意如下几点
- 绑定表达式的源时
AdornedElementPlaceholder
元素 AdornedElementPlaceholder
类通过AdornedElement
属性提供了指向背后元素(在这个示例中国是存在错误的TextBox对象)的引用- 为检索实际错误,需要检查这个元素的
Validation.Error
属性。然而,需要用圆括号包围Validation.Erros
属性,从而指示它是附加属性而不是TextBox类的属性 - 需要使用索引器成集合中检索第一个
ValidationError
对象,然后提取该对象的ErrorContent
属性
此外,如果希望在Border
或TextBox
元素本身的工具提示中显示错误消息,从而当用户将鼠标移到控件上的任何部分时都会显示错误消息。个实现这一功能而无需借助自定义错误模板——只需要一个用于TextBox控件的触发器,当Validation.HasError
属性变为true时应用该触发器,并且应用具有错误消息的工具提示。
1 | <Style TargetType="{x:Type TextBox}"> |
验证多个值
在许多情况下需要执行包含两个或更多个绑定值的验证。(例如,如果有些字段基于其他字段的信息判断得出是不合适的,个选择禁用它们),在其他情况下,会将这一逻辑构建到数据类自身(然而,如果数据在某些情况下是有效的,只是在特定的编辑任务中不能接受,那么这种方法行不通)。最后,可通过 WPF 数据绑定系统使用绑定组(binding group)创建应用这种规则的自定义验证规则。
绑定组背后的基本思想很简单。创建继承自 ValidationRule 类的自定义验证规则,如前面所述。但不是将该规则应用到单个绑定表达式,而将其附加到包含所有绑定控件的容器上(通常也就是将 DataContext 设置为数据对象的同一容器)。然后当提交编辑时,WPF 会使用该验证规则验证整个数据对象,这就是所谓的项级别验证(item-levelvalidation)。
例如,下面的标记通过设置BindingGroup
属性为Grid面板(该面板包含了所有元素)创建了一个绑定组。然后添加一个名为NoBlankProductRule
的验证规则。该规则自动应用到绑定的Product对象,该对象存储在Grid.DataContext属性中:
1 | public class NoBlankProductRule : ValidationRule |
1 | <Grid |
1 | private void txt_LostFocus(object sender, RoutedEventArgs e) |
在NoBlankProductRule
验证规则中,Validate()
方法接收审查的单个数值。但当使用绑定组时,Validate()
方法接收一个BindingGroup
对象,BindingGroup
对象封装了绑定数据对象(在这里上面的示例中是Product
对象,因为给整个Grid表单绑定的对象是Product
)
NoBlankProductRule
中从 BindingGroup.Items
集合检索第一个对象。在这个示例中,在该集合中只有一个数据对象。但可以创建应用到多个不同对象的绑定组(尽管不常见)。在这种情况下您会收到包含所有数据对象的集合。
为创建应用于多个数据对象的绑定组,必须设置 BindingGroup.Name 属性来为绑定组提供一个描述性的名称。然后在绑定表达式中设置BindGroupName
属性使它们相互匹配:
Text = {Binding Path=ModelNumber,BindingGroupName=MyBindingGroup}
这样一来,每个绑定表达式明确选择绑定组,并且可为针对不同数据对象的表达式使用相同的绑定组。
Validate()方法使用绑定组的方式还有一个意想不到的区别。在默认情况下,接收到的数据对象是针对原始对象的,没有应用任何新的修改。为得到希望验证的新值,需要调用BindingGroup.GetValue()方法并传递数据对象和属性名:
1 | string newModelName = (string)bindingGroup.GetValue(product, "ModelName"); |
这种设计具有一定意义。不为数据对象实际应用新值,从而使WPF能够确保在这些修改生效前,不会触发其他更新或应用程序中的同步任务。
当使用项级别验证时,通常需要创建与此类似的紧耦合验证规则。这是因为归纳验证逻辑通常并不容易(换句话说,不太可能为不同的数据对象应用类似但稍微不同的验证逻辑)。当调用 GetValue()方法时,还需要使用特定的属性名。所以,为项级别验证创建的验证规则不可能像为验证单个值所创建的验证规则那样整洁和精炼,而且可重用性更差。
如果验证失败,整个 Grid 面板都被认为是无效的,并在其周围显示红色的细边框。就像类似 TextBox 的编辑控件那样,可通过修改 Validation,ErrorTemplate 改变 Grid 面板的外观。
数据提供者
所有数据提供者都继承自System.Windows.Data.DataSourceProvider
类。目前,WPF只提供了以下两个数据提供者:
- ObjectDataProvider:该数据提供者通过调用另一个类中的方法获取信息
- XmlDataProvider:该数据提供者直接成XML文件获取信息
这两个对象的目标都是让用户能在XAML中实例化数据对象,而不必使用事件处理代码。
还有一种选择:可作为XAML中的资源明确创建视图对象,将控件绑定到视图,并使用代码为视图填充数据。尽管有些开发人员更喜欢尝试这种选择,不过这种选择主要用于希望通过应用排序和过滤定制视图的情况。
ObjectDataProvider
ObjectDataProvider
数据提供者可以从应用程序的另一个类中获取信息。它添加了以下功能:
- 它能创建需要的对象并为构造函数传递参数
- 它能调用所创建对象中的方法,并向它传递方法参数
- 它能异步地创建数据对象(换句话说,它能在窗口加载之前一直等待,此后在后台完成工作)
下面是一个基本的ObjectDataProvider
数据提供者,它创建了StoreDB
类的一个实例,调用该实例的*GetProducts()*方法,并且在窗口的其他部分获取数据:
1 | <Window.Resources> |
与所有数据提供者类似,ObiectDataProvider 是专门针对检索数据而设计的,而不是针对更新数据。换句话说,无法强制 ObiectDataProvider 数据提供者调用 StoreDB 类中的另一个方法来触发更新。
错误处理
这个示例有一个极大的限制。当创建这个窗口时,XAML解析器创建窗口并调用 GetProducts()方法,从而可以设置绑定。如果 GetProducts()方法返回期望的数据,切都会运行得很好,但如果抛出未处理的异常(例如,如果数据库太忙碌以至于不可访问),结果就不是很理想了。这时,异常从窗口构造函数中的InitializeComponent()调用上传。显示此窗口的代码需要捕获这个错误,这在概念上有些混乱,并且无法继续执行并显示窗口–即使在构造函数中捕获了异常,窗口的其他部分也不会被正确地初始化。
但没有较容易的方法能解决这个问题。**ObiectDataProvider 类提供了IsInitialLoadEnabled 属性,当第一次创建窗口时可将该属性设置为false,阻止调用 GetProducts()方法。如果将该属性设置为 false,可在后面调用 Refesh()方法触发调用。**但如果使用这种技术,绑定表达式就会失败,因为列表不能检索到它的数据源,这与大多数数据绑定错误是不同的(失败后不会给出提示,不会引发异常)。
那么,解决方法是什么呢?
可在代码中构造 ObiectDataProvider 数据提供者,但这会丧失声明式绑定的优点(而这可能是首先使用 ObiectDataProvider 数据提供者的原因)。另一种解决方法是配置 ObjectDataProvider 数据提供者,使其异步地执行工作,如后面所述。对于这种情况,发生异常时,不会通告失败信息(但仍会在 Debug 窗口中显示跟踪消息来详细地描述错误)。
异步支持
大多数开发人员会发现,没多少理由使用 ObjectDataProvider 数据提供者。通常,简单地直接绑定到数据对象,并添加少许代码来调用査询数据的类(如 StoreDB 类)更加容易。然而,有一个理由可能会让您使用 ObjectDataProvider 数据提供者——利用它执行异步数据查询。
1 | <ObjectDataProvider |
这看似很简单。只要将 ObjectDataProvider.IsAsynchronous 属性设置为 tmue,ObjectDataProvider数据提供者就会在后台进程中执行工作。因此,当在后台执行工作时,用户界面就会有反应。一旦数据对象构造完毕并从方法返回,所有绑定元素就可以使用 ObiectDataProvider 数据提供者了。
如果不希望使用 ObiectDataProvider 数据提供者,仍可异步加载数据访问代码。技巧是使用WPF对多线程应用程序的支持。一个有用的工具是在BackgroundWorker 组件。如果使用 BackgroundWorker 组件,还可得到取消支持和进度报告的优点。然而,将BackgroundWorker组件添加到用户界面比简单设置ObiectDataProvider.IsAsynchronous属性需要完成更多工作。
XmlDataProvider
XmlDataProvider 数据提供者提供了一种简捷方法,用于从单独的文件、Web 站点或应用程序资源中提取 XML数据,并使应用程序中的元素能使用提取到的数据。XmIDataProvider 数据提供者被设计为只读的(换句话说,它不具有提交数据修改的能力),而且不能处理来自其他源(如数据库记录、Web 服务消息等)的 XML数据。所以,XmlDataProrider 是一款专用工具。
为使用XmlDataProvider
数据提供者,首先要定义它并通过设置Source
属性将它指向恰当的文件:
store.xml需要设置为内容并开启复制到输入目录。
1 |
|
1 | <Window.Resources> |
当在XmlDataProvider
数据提供者中使用XPath
时,第一个任务是确定根节点。下一步是绑定列表,当使用XmlDataProvider
数据提供者时,使用Binding.XPath
属性而不是Binding.Path
属性。这样个灵活地以事多需的深度挖掘XML文档。
最后,需要关联显示产品细节的每个元素。同样可以便携XPath
表达式相对于当前节点进行计算:
1 | <TextBox Text="{Binding XPath=ModelNumber}"/> |