通過前面的介紹我們知道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)。”