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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章