通過前面的介紹我們知道ModelValidatorProviders的靜態唯讀Providers維護著一個全域的ModelValidatorProvider列表,最終用於Model驗證的ModelValidator都是通過這些ModelValidatorProvider來提供的。對於該列表預設包含的三種ModelValidatorProvider來說,DataAnnotationsModelValidatorProvider無疑是最重要的,ASP.NET MVC預設提供的基於資料標註特性的聲明式Model驗證就是通過DataAnnotationsModelValidatorProvider提供的DataAnnotationsModelValidator來實現的。
一、ValidationAttribute特性
與通過資料標註特性定義Model中繼資料類似,我們可以在作為Model的資料類型及其屬性上應用相應的標註特性來定義Model驗證規則。所有的驗證特性都直接或者間接繼承自抽象類別型System.ComponentModel.DataAnnotations.ValidationAttribute。如下面的代碼片斷所示,ValidationAttribute具有一個字串類型的ErrorMessage屬性用於指定驗證錯誤訊息。出於對本地化或者對錯誤訊息單獨維護的需要,我們可以採用資源檔的方式來儲存錯誤訊息,在這種情況下我們只需要通過ErrorMessageResourceName和ErrorMessageResourceType這兩個屬性指定錯誤訊息所在資源項的名稱和類型即可。
1: public abstract class ValidationAttribute : Attribute
2: {
3: public string ErrorMessage { get; set; }
4: public string ErrorMessageResourceName { get; set; }
5: public Type ErrorMessageResourceType { get; set; }
6: protected string ErrorMessageString {get;}
7:
8: public virtual string FormatErrorMessage(string name);
9:
10: public virtual bool IsValid(object value);
11: protected virtual ValidationResult IsValid(object value, ValidationContext validationContext)
12:
13: public void Validate(object value, string name);
14: public ValidationResult GetValidationResult(object value, ValidationContext validationContext);
15: }
二、驗證訊息的定義
如果我們通過ErrorMessage屬性指定一個字串作為驗證錯誤訊息,又通過ErrorMessageResourceName/ErrorMessageResourceType屬性指定了錯誤訊息資源項對應的名稱和類型,後者具有更高的優先順序。ValidationAttribute具有一個受保護的唯讀屬性ErrorMessageString用於返回最終的錯誤訊息文本。
對於錯誤訊息的定義,我們可以定義一個完整的訊息,比如“年齡必需在18至25之間”。但是對於像資源檔這種對錯誤訊息進行獨立維護的情況,為了讓定義的資源文本能夠最大限度地被重用,我們傾向於定義一個包含預留位置的文本模板,比如“{DisplayName}必需在{LowerBound}和{UpperBound}之間”,這樣訊息適用於所有基於數值範圍的驗證。對於後者,模板中的預留位置可以在虛方法FormatErrorMessage中進行替換。該方法中的參數name實際上代表的是對應的顯示名稱,即對應ModelMetadata的DisplayName屬性。
FormatErrorMessage方法在ValidationAttribute中的預設實現僅僅是簡單地調用String的靜態方法Format將參數name作為替換預留位置的參數,具體的定義如下。所以在預設的情況下,我們在定義錯誤訊息模板的時候,只允許包含唯一一個針對顯示名稱的預留位置“{0}”。如果具有額外的預留位置,或者不需要採用基於序號(“{0}”)的定義方法(比如採用類似於“{DisplayName}”這種基於文字的預留位置更具可讀性),只需要重寫FormatErrorMessage方法即可。
1: public abstract class ValidationAttribute : Attribute
2: {
3: //其他成員
4: public virtual string FormatErrorMessage(string name)
5: {
6: return string.Format(CultureInfo.CurrentCulture, ErrorMessageString, new object[] { name });
7: }
8: }
三、驗證的執行
當我們通過繼承ValidationAttribute建立我們自己的驗證特性的時候,可以通過重寫公有方法IsValid或者受保護方法IsValid來實現我們自訂的驗證邏輯。我們之所以能夠通過重寫任一個IsValid方法是我們自訂驗證邏輯生效的原因在於這兩個方法在ValidationAttribute特殊的定義方法。按照這兩個方法在ValidationAttribute中的定義,它們之間存在相互調用的關係,而這種相互調用必然造成“死迴圈”,所以我們需要重寫至少其中一個方法比避免“死迴圈”的方法。這裡的“死迴圈”被加上的引號,是因為ValidationAttribute在內部作了處理,當這種情況出現的時候會拋出一個NotImplementedException異常。
1: //調用公有IsValid方法
2: public class ValidatorAttribute : ValidationAttribute
3: {
4: static void Main()
5: {
6: ValidatorAttribute validator = new ValidatorAttribute();
7: validator.IsValid(new object());
8: }
9: }
10:
11: //調用受保護IsValid方法
12: public class ValidatorAttribute : ValidationAttribute
13: {
14: static void Main()
15: {
16: ValidatorAttribute validator = new ValidatorAttribute();
17: validator.IsValid(new object(),null);
18: }
19: }
為了驗證對虛方法IsValid重寫的必要性,我們來做一個簡單的執行個體示範。在一個控制台應用中我們分別編寫了如上兩段程式,其中通過繼承ValidationAttribute定義了一個ValidatorAttribute,但是沒有重寫任何一個IsValid方法。當我們在Debug模式下分別運行這兩段程式的時候,都會拋出如下圖所示的NotImplementedException異常,提示“此類尚未實現 IsValid(object value)。首選進入點是 GetValidationResult(),並且類應重寫 IsValid(object value, ValidationContext context)。”