天天看點

通過 WPF 強制執行複雜的業務資料規則通過 WPF 強制執行複雜的業務資料規則

通過 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 所示。

通過 WPF 強制執行複雜的業務資料規則通過 WPF 強制執行複雜的業務資料規則

圖 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 中顯示了對象模型。

通過 WPF 強制執行複雜的業務資料規則通過 WPF 強制執行複雜的業務資料規則

圖 3 示例應用程式的對象模型

使用者填寫的主要資料片段為 Activity 對象,包括 Title、ActivityDate、ActivityType(預定義活動類型的下拉選擇)和 Description。他們還需要将其活動與三種可能性之一相關聯。他們需要從配置設定給他們的客戶清單中選擇已為其執行活動的 Customer,或從公司目标清單中選擇與活動相關的公司 Objective;或者,如果沒有适用于此活動的 Customer 或 Objective,則可以手動輸入 Reason。

下面是應用程式需要強制執行的驗證規則:

  • Title 和 Description 為必填字段。
  • ActivityDate 不得早于目前日期之前七天,也不得晚于目前日期之後七天。
  • 如果選擇了 ActivityType Install,則 Inventory 字段為必填字段,應訓示技術人員貨車中所耗用的裝置。清單項目需要以逗号分隔的清單形式輸入,具有适用于輸入項目的預期型号結構。
  • 必須至少提供一個 Customer、Objective 或 Reason。

這些要求可能看似十分簡單,但是并不那麼易于滿足(尤其是最後兩個),因為它們訓示出屬性間的交叉耦合。圖 4 顯示了正在運作的應用程式,其中包含一些無效資料(通過紅色框來訓示)。

通過 WPF 強制執行複雜的業務資料規則通過 WPF 強制執行複雜的業務資料規則

圖 4 顯示了工具提示和無效資料的對話框

異常驗證

最簡單的驗證形式是在設定目标屬性過程中引發一個異常,該異常将被視為驗證錯誤。異常可能來自 Binding 設定目标屬性之前的類型轉換過程;可能來自屬性 setter 中的顯式異常引發;也可能來自從 setter 對業務對象的調用(此時将進一步在堆棧靠下面的位置引發異常)。

若要使用此機制,隻需在 Binding 對象上将 ValidatesOnExceptions 屬性設定為 true:

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

如果在嘗試設定源對象屬性(本例中為 Activity.Title)時引發異常,則将對控件設定驗證錯誤。預設驗證錯誤通過控件四周的紅色邊框來訓示,如圖 5 所示。

通過 WPF 強制執行複雜的業務資料規則通過 WPF 強制執行複雜的業務資料規則

圖 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 導緻錯誤時所訓示的唯一驗證錯誤。

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。

驗證錯誤顯示

如前所述,WPF 顯示驗證錯誤的預設方式是在控件周圍繪制紅色邊框。通常需要對此方法進行自定義,以通過其他方式來顯示錯誤。而且,預設情況下不會顯示與驗證錯誤關聯的錯誤消息。常見的要求是僅當存在驗證錯誤時才在工具提示中顯示錯誤消息。通過将 Styles 和一組與驗證關聯的附加屬性進行組合,可以相當輕松地自定義驗證錯誤顯示。

添加顯示錯誤文本的工具提示非常簡單。隻需定義一個應用于輸入控件的 Style,每當存在驗證錯誤時,它便将該控件上的 ToolTip 屬性設定為驗證錯誤文本。若要對此提供支援,需要使用兩個附加屬性:Validation.HasError 和 Validation.Errors。下面示範了一個針對 TextBox 類型并設定工具提示的 Style:

<Style TargetType="TextBox">
  <Style.Triggers>
    <Trigger Property="Validation.HasError" 
             Value="True">
      <Setter Property="ToolTip">
        <Setter.Value>
          <Binding 
            Path="(Validation.Errors).CurrentItem.ErrorContent"
            RelativeSource="{x:Static RelativeSource.Self}" />
        </Setter.Value>
      </Setter>
    </Trigger>
  </Style.Triggers>
</Style>      

您可以看到,Style 隻包含 Validation.HasError 附加屬性的屬性觸發器。當 Binding 更新其源對象屬性且驗證機制生成錯誤時,HasError 屬性會設定為 true。這種情況可能源自異常、ValidationRule 或 IDataErrorInfo 調用。該 Style 随後使用 Validation.Errors 附加屬性,該屬性會在存在驗證錯誤時包含一個錯誤字元串集合。可以使用該集合類型的 CurrentItem 屬性來僅擷取集合中的第一個字元串。也可以設計為将資料綁定到集合,并為面向清單的控件中的每一項顯示 ErrorContent 屬性。

若要将控件的預設驗證錯誤顯示更改為紅色邊框之外的内容,需要将 Validation.ErrorTemplate 附加屬性設定為要自定義的控件上的新模闆。在示例應用程式中,将在存在錯誤的每個控件右側顯示一個小的紅色漸變圓形,而不是顯示紅色邊框。為此,可定義用作 ErrorTemplate 的控件模闆。

<ControlTemplate x:Key="InputErrorTemplate">
  <DockPanel>
    <Ellipse DockPanel.Dock="Right" Margin="2,0" 
             ToolTip="Contains invalid data"
             Width="10" Height="10">
      <Ellipse.Fill>
        <LinearGradientBrush>
          <GradientStop Color="#11FF1111" Offset="0" />
          <GradientStop Color="#FFFF0000" Offset="1" />
        </LinearGradientBrush>
      </Ellipse.Fill>
    </Ellipse>
    <AdornedElementPlaceholder />
  </DockPanel>
</ControlTemplate>      

若要将該控件模闆挂接到某個控件,隻需設定該控件的 Validation.ErrorTemplate 屬性,您可以通過 Style 再次執行此操作:

<Style TargetType="TextBox">
  <Setter Property="Validation.ErrorTemplate" 
    Value="{StaticResource InputErrorTemplate}" />
  ...
</Style>      

總結

在本文中,我示範了如何使用 WPF 資料綁定的三種驗證機制來實作一些業務資料驗證方案。您已了解到如何使用異常、ValidationRule 和 IDataErrorInfo 接口來實作單個屬性驗證,以及如何使用其驗證規則取決于控件上其他屬性的目前值的屬性。您還了解到如何使用 BindingGroup 來一次性評估多個 Bindings,以及如何自定義 WPF 預設方式之外的錯誤顯示。

本文的示例應用程式具有滿足某個簡單應用程式中描述的業務規則的整套驗證,該應用程式使用 MVVM 将視圖挂接到為其提供支援的資料。         

Brian Noyes 是 IDesign (idesign.net) 的首席架構師、Microsoft 區域總監和 Microsoft MVP。Noyes 是一名作家,經常在 Microsoft 技術教育訓練、DevConnections、DevTeach 以及全世界的其他會議上發表演講。您可以通過他的部落格 (briannoyes.net) 與他聯系。

衷心感謝以下技術專家對本文的審閱: Sam Bent

轉載于:https://www.cnblogs.com/xiwang/articles/2584033.html

繼續閱讀