WPF输入验证(1)

 

通过 WPF 强制执行复杂的业务数据规则

Brian Noyes

下载代码示例

Microsoft Windows Presentation Foundation (WPF) 具有一个丰富数据绑定系统。除了作为通过 Model-View-ViewModel (MVVM) 模式从支持逻辑和数据对 UI 定义进行松散耦合的关键推动力之外,数据绑定系统还为业务数据验证方案提供强大而灵活的支持。WPF 中的数据绑定机制包括多个选项,可用于在创建可编辑视图时评估输入数据的有效性。此外,通过针对控件的 WPF 模板和样式功能,您可以轻松地自定义向用户指示验证错误的方式。

为了支持复杂规则并向用户显示验证错误,通常需要组合使用各种可用的验证机制。即使是看似简单的数据输入形式也可能在业务规则变得复杂时带来验证难题。常用方案涉及单个属性级别的简单规则以及交叉耦合属性,在交叉耦合属性中,一个属性的有效性取决于另一个属性的值。然而,通过 WPF 数据绑定中的验证支持,可以轻松地解决这些难题。

在本文中,您将了解如何使用 IDataErrorInfo 接口实现、ValidationRules、BindingGroups、异常以及与验证相关的附加属性和事件来满足数据验证需要。您还将了解如何使用自己的 ErrorTemplates 和 ToolTips 来自定义验证错误的显示。在本文中,我假设您已熟悉 WPF 的基本数据绑定功能。有关这方面的更多背景信息,请参见 John Papa 在 2007 年 12 月 MSDN 杂志中发表的文章“WPF 中的数据绑定”。

数据验证概述

几乎每当您在应用程序中输入或修改数据时,都需要确保数据是有效的,以避免与这些更改的来源(在这种情况下为用户)相去甚远。而且,您需要在用户输入的数据无效时向他们提供清晰指示,还能够向其提供一些有关如何更正数据的指示。只要您知道需使用何种功能以及何时使用,便可通过 WPF 相当轻松地完成这些任务。

在使用 WPF 中的数据绑定来呈现业务数据时,通常应使用 Binding 对象在目标控件的单个属性与数据源对象属性之间提供数据管道。若要使验证是相关的,通常需进行 TwoWay 数据绑定 — 这意味着,除了从源属性流向目标属性以进行显示的数据之外,编辑过的数据也会从目标流向源,如图 1 所示。

图 1 TwoWay 数据绑定中的数据流

可使用三种机制来确定通过数据绑定控件输入的数据是否有效。图 2 对这些机制进行了总结。

图 2 绑定验证机制

验证机制 说明
异常 通过在某个 Binding 对象上设置 ValidatesOnExceptions 属性,如果在尝试对源对象属性设置已修改的值的过程中引发异常,则将为该 Binding 设置验证错误。
ValidationRules Binding 类具有一个用于提供 ValidationRule 派生类实例的集合的属性。这些 ValidationRules 需要覆盖某个 Validate 方法,该方法由 Binding 在每次绑定控件中的数据发生更改时进行调用。如果 Validate 方法返回无效的 ValidationResult 对象,则将为该 Binding 设置验证错误。
IDataErrorInfo 通过在绑定数据源对象上实现 IDataErrorInfo 接口并在 Binding 对象上设置 ValidatesOnDataErrors 属性,Binding 将调用从绑定数据源对象公开的 IDataErrorInfo API。如果从这些属性调用返回非 null 或非空字符串,则将为该 Binding 设置验证错误。

当用户在 TwoWay 数据绑定中输入或修改数据时,将启动以下工作流:

  • 用户通过击键、鼠标、触摸或与各元素间的手写笔交互来输入或修改数据,从而更改元素的属性。
  • 如果需要,可将数据转换为数据源属性类型。
  • 设置源属性值。
  • 触发 Binding.SourceUpdated 附加事件。
  • 如果数据源属性上的 setter 引发异常,则异常会由 Binding 捕获,并可用于指示验证错误。
  • 如果实现了 IDataErrorInfo 属性,则会对数据源对象调用这些属性。
  • 向用户呈现验证错误指示,并触发 Validation.Error 附加事件。

如您所见,该过程中有多个位置可以产生验证错误,具体取决于所选择的机制。列表中未显示触发 ValidationRule 的位置。这是因为,根据为 ValidationRule 上的 ValidationStep 属性设置的值,可以在该过程中的各个位置触发 ValidationRule,包括在类型转换之前、转换之后、更新属性之后或提交更改的值时(如果数据对象实现 IEditableObject)。默认值为 RawProposedValue,它在类型转换之前发生。数据从目标控件属性类型转换为数据源对象属性类型的位置通常隐式产生,不会触及代码的任何部分(如 TextBox 中的数字输入)。此类型转换过程可能引发异常,这些异常应该用于向用户指示验证错误。

如果无法将值写入源对象属性,则该值显然是无效输入。如果选择挂接 ValidationRules,则会在该过程中由 ValidationStep 属性指示的位置处调用 ValidationRules,它们可基于嵌入其中或从其调用的任何逻辑来返回验证错误。如果源对象属性 setter 引发异常,则几乎应总是将该异常视为验证错误,这与类型转换的情况相同。

最后,如果实现 IDataErrorInfo,则将针对为了基于从接口返回的字符串来检查是否存在验证错误而设置的属性,来调用向该接口的数据源对象添加的索引器属性。稍后我会更详细地介绍每种机制。

您必须决定需要何时进行验证。验证将在 Binding 向基础源对象属性写入数据时进行。何时进行验证由 Binding 的 UpdateSourceTrigger 属性来指定,对于大多数属性,该属性设置为 PropertyChanged。某些属性(如 TextBox.Text)会将该值更改为 FocusChange,这意味着验证将在焦点离开正用于编辑数据的控件时发生。也可将该值设置为 Explicit,这意味着必须对绑定显式调用验证。在本文后面讨论的 BindingGroup 将使用 Explicit 模式。

在验证方案中(尤其是对于 TextBoxes),通常需要立即向用户提供反馈。若要对此提供支持,应将 Binding 上的 UpdateSourceTrigger 属性设置为 PropertyChanged:

Text="{Binding Path=Activity.Description, UpdateSourceTrigger=PropertyChanged}

 

事实证明,对于许多实际验证方案,您需要利用这些机制中的多种机制。根据您所关心的验证错误类型以及验证逻辑的位置,每种机制各有其优缺点。

业务验证方案

为了更具体地说明这一点,让我们来看一个具有半真实业务环境的编辑方案,您会看到每种机制如何发挥作用。此方案和验证规则基于我为某个客户编写的一个实际应用程序,其中有一个相当简单的窗体,它因用于验证的支持业务规则而要求使用几乎每种验证机制。对于本文中使用的更加简单的应用程序,我会使用每种机制来演示其用法,即使并未明确要求使用所有这些机制。

假设您需要编写一个应用程序以便为在家中提供客户支持电话服务的现场技术人员(可以是网络专家,但也可以是尝试追加销售其他功能和服务的人员)提供支持。对于技术人员在现场进行的每个活动,该技术人员都需要向一个列明他所进行的各项活动的报告进行输入,并将该报告与多个数据片段相关联。图 3 中显示了对象模型。

图 3 示例应用程序的对象模型

用户填写的主要数据片段为 Activity 对象,包括 Title、ActivityDate、ActivityType(预定义活动类型的下拉选择)和 Description。他们还需要将其活动与三种可能性之一相关联。他们需要从分配给他们的客户列表中选择已为其执行活动的 Customer,或从公司目标列表中选择与活动相关的公司 Objective;或者,如果没有适用于此活动的 Customer 或 Objective,则可以手动输入 Reason。

下面是应用程序需要强制执行的验证规则:

  • Title 和 Description 为必填字段。
  • ActivityDate 不得早于当前日期之前七天,也不得晚于当前日期之后七天。
  • 如果选择了 ActivityType Install,则 Inventory 字段为必填字段,应指示技术人员货车中所耗用的设备。清单项目需要以逗号分隔的列表形式输入,具有适用于输入项目的预期型号结构。
  • 必须至少提供一个 Customer、Objective 或 Reason。

这些要求可能看似十分简单,但是并不那么易于满足(尤其是最后两个),因为它们指示出属性间的交叉耦合。图 4 显示了正在运行的应用程序,其中包含一些无效数据(通过红色框来指示)。

图 4 显示了工具提示和无效数据的对话框

异常验证

最简单的验证形式是在设置目标属性过程中引发一个异常,该异常将被视为验证错误。异常可能来自 Binding 设置目标属性之前的类型转换过程;可能来自属性 setter 中的显式异常引发;也可能来自从 setter 对业务对象的调用(此时将进一步在堆栈靠下面的位置引发异常)。

若要使用此机制,只需在 Binding 对象上将 ValidatesOnExceptions 属性设置为 true:

Text="{Binding Path=Activity.Title, ValidatesOnExceptions=True}"

 

如果在尝试设置源对象属性(本例中为 Activity.Title)时引发异常,则将对控件设置验证错误。默认验证错误通过控件四周的红色边框来指示,如图 5 所示。

图 5 验证错误

因为异常可能在类型转换过程中发生,所以最好只要有可能发生类型转换失败,就要在输入 Bindings 上设置此属性,即使支持属性只对不会发生异常的成员变量设置值。

例如,假设要将 TextBox 用作 DateTime 属性的输入控件。如果用户输入无法转换的字符串,则 ValidatesOnExceptions 是 Binding 可以指示错误的唯一方式,因为永远不会调用源对象属性。

如果需要在存在无效数据时执行某些特定操作(如禁用某个命令),则可以在控件上挂接 Validation.Error 附加事件。您还需要在 Binding 上将 NotifyOnValidationError 属性设置为 true。

<TextBox Name="ageTextBox" 
  Text ="{Binding Path=Age, 
    ValidatesOnExceptions=True, 
    NotifyOnValidationError=True}" 
    Validation.Error="OnValidationError".../>

ValidationRule 验证

在某些方案中,可能需要在 UI 级别嵌入验证,并需要使用更加复杂的逻辑来确定输入是否有效。对于示例应用程序,请考虑用于 Inventory 字段的验证规则。如果输入数据,则该数据应是遵循特定模式的逗号分隔的型号列表。ValidationRule 可以轻松满足此要求,因为它完全取决于设置的值。ValidationRule 可使用 string.Split 调用将输入转换为字符串数组,然后使用正则表达式来检查各个部分是否符合给定模式。为此,可以按图 6 所示来定义 ValidationRule。

图 6 用于验证字符串数组的 ValidationRule

public class InventoryValidationRule : ValidationRule {

  public override ValidationResult Validate(
    object value, CultureInfo cultureInfo) {

    if (InventoryPattern == null)
      return ValidationResult.ValidResult;

    if (!(value is string))
      return new ValidationResult(false, 
     "Inventory should be a comma separated list of model numbers as a string");

    string[] pieces = value.ToString().Split(‘,’);
    Regex m_RegEx = new Regex(InventoryPattern);

    foreach (string item in pieces) {
      Match match = m_RegEx.Match(item);
      if (match == null || match == Match.Empty)
        return new ValidationResult(
          false, "Invalid input format");
    }

    return ValidationResult.ValidResult;
  }

  public string InventoryPattern { get; set; }
}

  

在 ValidationRule 上公开的属性可以在使用位置通过 XAML 进行设置,从而允许这些属性更加灵活一些。此验证规则将忽略无法转换为字符串数组的值。但在规则可以执行 string.Split 时,它将使用 RegEx 来验证逗号分隔列表中的每个字符串是否都符合通过 InventoryPattern 属性设置的模式。

当返回有效标志设置为 false 的 ValidationResult 时,可以在 UI 中使用您所提供的错误消息,以向用户呈现错误(我将在后面加以说明)。ValidationRule 的一个弊端是需要使用 XAML 中的扩展 Binding 元素来将其挂接,如下面的代码所示:

<TextBox Name="inventoryTextBox"...>
  <TextBox.Text>
    <Binding Path="Activity.Inventory" 
             ValidatesOnExceptions="True" 
             UpdateSourceTrigger="PropertyChanged" 
             ValidatesOnDataErrors="True">
      <Binding.ValidationRules>
        <local:InventoryValidationRule 
          InventoryPattern="^\D?(\d{3})\D?\D?(\d{3})\D?(\d{4})$"/>
      </Binding.ValidationRules>
    </Binding>
  </TextBox.Text>
</TextBox>

在此示例中,由于 ValidatesOnExceptions 属性设置为 true,因此我的 Binding 仍会在发生异常时引发验证错误,我还会根据设置为 true 的 ValidatesOnDataErrors 来支持 IDataErrorInfo 验证(接下来我将对此进行讨论)。

如果将多个 ValidationRules 附加到同一个属性,则这些规则可以各自具有不同的 ValidationStep 属性值,或具有相同的值。将按声明的顺序对同一 ValidationStep 中的规则进行评估。很明显,较早 ValidationSteps 中的规则将在较晚 ValidationStep 中的规则之前运行。可能不那么明显的地方是,如果 ValidationRule 返回错误,则不会评估任何后续规则。因此,第一个验证错误是在 ValidationRules 导致错误时所指示的唯一验证错误。

 

posted @ 2013-04-10 23:15  风痕天下  阅读(2108)  评论(2)    收藏  举报