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