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