WPF 输入验证(2)

IDataErrorInfo 验证

IDataErrorInfo 接口需要实现者公开一个属性和一个索引器:

public interface IDataErrorInfo {
  string Error { get; }
  string this[string propertyName] { get; }
}


Error 属性用于指示整个对象的错误,而索引器用于指示单个属性级别的错误。两者的工作原理相同:如果返回非 null 或非空字符串,则表示存在验证错误。此外,返回的字符串可用于向用户显示错误(我将在后面加以说明)。

当使用绑定到数据源对象上的各个属性的各个控件时,该接口的最重要部分就是索引器。只有在将对象显示在 DataGrid 或 BindingGroup 中的这种方案中,才使用 Error 属性。Error 属性用于指示行级别的错误,而索引器用于指示单元格级别的错误。

实现 IDataErrorInfo 具有一个很大的弊端:索引器的实现通常会导致较大的 switch-case 语句(对象中的每个属性名称都对应于一种情况),您必须基于字符串进行切换和匹配,并返回指示错误的字符串。而且,在对象上设置属性值之前,不会调用 IDataErrorInfo 的实现。如果其他对象订阅了该对象上的 INotifyPropertyChanged.PropertyChanged,则已通知这些对象发生了更改,它们可能已基于 IDataErrorInfo 实现要声明为无效的数据开始工作。如果这对于您的应用程序可能是个问题,则需要在对设置的值不满意时从属性 setter 引发异常。

IDataErrorInfo 的优点是,它可用于轻松地处理交叉耦合属性。例如,除了使用 ValidationRule 来验证 Inventory 字段的输入格式之外,还应记住这一要求:当 ActivityType 为 Install 时,必须填写 Inventory 字段。ValidationRule 本身无权访问数据绑定对象上的其他属性。它只是传递为 Binding 挂接的属性所设置的值。若要满足此要求,当设置了 ActivityType 属性时,您需要使验证在 Inventory 属性上发生,并在 ActivityType 设置为 Install 而 Inventory 的值为空时返回无效结果。

若要实现此目的,您需要使用 IDataErrorInfo,以便可在评估 Inventory 时检查 Inventory 和 ActivityType 属性,如下所示:

public string this[string propertyName] {
  get { return IsValid(propertyName); }
}

private string IsValid(string propertyName) {
  switch (propertyName) {
    ...
    case "Inventory":
      if (ActivityType != null && 
        ActivityType.Name == "Install" &&  
        string.IsNullOrWhiteSpace(Inventory))
        return "Inventory expended must be entered for installs";
      break;
}

此外,您需要让 Inventory Binding 在 ActivityType 属性发生更改时调用验证。通常,仅当 UI 中的该属性发生更改时,Binding 才查询 IDataErrorInfo 实现或调用 ValidationRules。在本例中,即使 Inventory 属性尚未更改但相关 ActivityType 已更改,我也需要触发 Binding 验证的重新评估。

可通过两种方式让 Inventory Binding 在 ActivityType 属性发生更改时进行刷新。第一种(也是较为简单)的方式是在设置 ActivityType 时为 Inventory 发布 PropertyChanged 事件:

ActivityType _ActivityType;
public ActivityType ActivityType {
  get { return _ActivityType; }
  set { 
    if (value != _ActivityType) {
      _ActivityType = value;
      PropertyChanged(this, 
        new PropertyChangedEventArgs("ActivityType"));
      PropertyChanged(this, 
        new PropertyChangedEventArgs("Inventory"));
    }
  }
}

这会使 Binding 刷新并重新评估该 Binding 的验证。

第二种方式是在 ActivityType ComboBox 或其父元素之一的上面挂接 Binding.SourceUpdated 附加事件,并从该事件的代码隐藏处理程序来触发 Binding 刷新:

<ComboBox Name="activityTypeIdComboBox" 
  Binding.SourceUpdated="OnPropertySet"...

private void OnPropetySet(object sender, 
  DataTransferEventArgs e) {

  if (activityTypeIdComboBox == e.TargetObject) {
    inventoryTextBox.GetBindingExpression(
      TextBox.TextProperty).UpdateSource();
  }
}

以编程方式调用 Binding 上的 UpdateSource 会使其将绑定目标元素中的当前值写入源属性,从而如同用户刚刚编辑过控件那样来触发验证链。

将 BindingGroup 用于交叉耦合属性

Microsoft .NET Framework 3.5 SP1 中新增了 BindingGroup 功能。BindingGroup 专门用于一次性评估一组绑定上的验证。例如,它允许用户填写整个窗体,并一直等到其按“提交”或“保存”按钮以评估窗体的验证规则,然后一次性呈现验证错误。在示例应用程序中,我要求必须至少提供一个 Customer、Objective 或 Reason。BindingGroup 也可以用于评估窗体的子集。

若要使用 BindingGroup,需要一组具有普通 Bindings 并共享公共上级元素的控件。在示例应用程序中,Customer ComboBox、Objective ComboBox 和 Reason TextBox 都位于同一个布局 Grid 中。BindingGroup 是 FrameworkElement 上的属性。它具有 ValidationRules 集合属性,可以使用一个或多个 ValidationRule 对象来填充该属性。下面的 XAML 演示示例应用程序的 BindingGroup 挂接:

<Grid>...
<Grid.BindingGroup>
  <BindingGroup>
    <BindingGroup.ValidationRules>
      <local:CustomerObjectiveOrReasonValidationRule 
        ValidationStep="UpdatedValue" 
        ValidatesOnTargetUpdated="True"/>
    </BindingGroup.ValidationRules>
  </BindingGroup>
</Grid.BindingGroup>
</Grid>

在此示例中,我向集合中添加了 CustomerObjectiveOrReasonValidationRule 的一个实例。使用 ValidationStep 属性,可以在某种程度上控制传递给规则的值。UpdatedValue 表示要使用写入到数据源对象的值(在写入该值后)。您还可以为 ValidationStep 选择值,从而可以使用用户的原始输入、应用了类型和值转换后的值或是“已提交的”值(这意味着实现 IEditableObject 接口以针对对象的属性做出事务性更改)。

ValidatesOnTargetUpdated 标志使得每次通过 Bindings 设置目标属性时都会对规则进行评估。这包括最初设置属性时的情况,因此在初始数据无效时,以及每当用户在属于 BindingGroup 的控件中更改值时,都将立刻获得验证错误指示。

挂接到 BindingGroup 的 ValidationRule 的工作方式与挂接到单个 Binding 的 ValidationRule 稍有不同。图 7 显示了挂接到上面代码示例中演示的 BindingGroup 的 ValidationRule。

图 7 BindingGroup 的 ValidationRule

public class CustomerObjectiveOrReasonValidationRule : 
  ValidationRule {

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

    BindingGroup bindingGroup = value as BindingGroup;
    if (bindingGroup == null) 
      return new ValidationResult(false, 
        "CustomerObjectiveOrReasonValidationRule should only be used with a BindingGroup");

    if (bindingGroup.Items.Count == 1) {
      object item = bindingGroup.Items[0];
      ActivityEditorViewModel viewModel = 
        item as ActivityEditorViewModel;
      if (viewModel != null && viewModel.Activity != null && 
        !viewModel.Activity.CustomerObjectiveOrReasonEntered())
        return new ValidationResult(false, 
          "You must enter one of Customer, Objective, or Reason to a valid entry");
    }
    return ValidationResult.ValidResult;
  }
}

在挂接到单个 Binding 的 ValidationRule 中,传入的值是来自一个数据源属性的单个值,该数据源属性设置为该 Binding 的 Path。对于 BindingGroup,传递给 ValidationRule 的值是 BindingGroup 本身。该值包含一个 Items 集合,该集合由包含元素的 DataContext(本例中为 Grid)进行填充。

对于示例应用程序,我使用 MVVM 模式,因此,视图的 DataContext 是 ViewModel 本身。Items 集合仅包含对 ViewModel 的单个引用。我可以从 ViewModel 来访问它上面的 Activity 属性。本例中 Activity 类所具有的验证方法可确定是否输入了至少一个 Customer、Objective 或 Reason,因此,我不必在 ValidationRule 中重复该逻辑。

与前面介绍的其他 ValidationRule 一样,如果您对传入的数据值满意,则可返回一个 ValidationResult.ValidResult。如果不满意,则可使用设置为 false 的有效标志和指示问题的字符串消息来构造新的 ValidationResult,该消息随后可以用于显示。

不过,设置 ValidatesOnTargetUpdated 标志并不足以让 ValidationRules 自动触发。BindingGroup 是围绕为整个控件组显式触发验证(通常是通过在窗体上按“提交”或“保存”按钮这类操作)的概念而设计的。在某些方案中,用户在认为编辑过程完成之前不希望被验证错误指示所打扰,因此 BindingGroup 在设计上考虑了此方法。

在示例应用程序中,我希望每当用户更改窗体中的内容时,将立即向其提供验证错误反馈。若要使用 BindingGroup 实现此目的,必须在属于该组的各个输入控件上挂接适当的更改事件,并使这些事件的事件处理程序触发 BindingGroup 的评估。在示例应用程序中,这意味着在 TextBox 上的两个 ComboBoxes 和 TextBox.TextChanged 事件上挂接 ComboBox.SelectionChanged 事件。所有这些事件都可以指向代码隐藏中的单个处理方法:

private void OnCommitBindingGroup(
  object sender, EventArgs e) {

  CrossCoupledPropsGrid.BindingGroup.CommitEdit();
}

请注意,对于验证显示,将在 BindingGroup 所在的 FrameworkElement(如示例应用程序中的 Grid)上显示默认红色边框,如图 4 所示。也可以使用 Validation.ValidationAdornerSite 和 Validation.ValidationAdornerSiteFor 附加属性来更改显示验证指示的位置。默认情况下,各个控件也会为其各自的验证错误显示红色边框。在示例应用程序中,我通过 Style 将 ErrorTemplate 设置为 null 以将这些边框关闭。

对于 .NET Framework 3.5 SP1 中的 BindingGroup,您可能会在初始窗体加载时遇到与正常显示验证错误有关的问题,即使在 ValidationRule 上设置了 ValidatesOnTargetUpdated 属性。我针对此问题找到的一个解决方法是“稍微修改”BindingGroup 中的绑定属性之一。在示例应用程序中,您可以在视图的 Loaded 事件中,在 TextBox 中最初呈现的任何文本结尾处添加和删除一个空格,如下所示:

string originalText = m_ProductTextBox.Text;
m_ProductTextBox.Text += " ";
m_ProductTextBox.Text = originalText;

这会使 BindingGroup ValidationRule 在所包含的 Binding 属性之一发生更改时触发,从而调用每个 Binding 的验证。此行为在 .NET Framework 4.0 中得到了修复,因此无需该解决方法便可获得验证错误的初始显示 — 只需在验证规则上将 ValidatesOnTargetUpdated 属性设置为 true。

posted @ 2013-04-10 23:21  风痕天下  阅读(383)  评论(0)    收藏  举报