通過前面的介紹我們知道ModelValidatorProviders的靜态隻讀Providers維護着一個全局的ModelValidatorProvider清單,最終用于Model驗證的ModelValidator都是通過這些ModelValidatorProvider來提供的。對于該清單預設包含的三種ModelValidatorProvider來說,DataAnnotationsModelValidatorProvider無疑是最重要的,ASP.NET
目錄 一、ValidationAttribute特性 二、驗證消息的定義 三、驗證的執行 四、預定義ValidationAttribute 五、應用ValidationAttribute特性的唯一性
與通過資料标注特性定義Model中繼資料類似,我們可以在作為Model的資料類型及其屬性上應用相應的标注特性來定義Model驗證規則。所有的驗證特性都直接或者間接繼承自抽象類型System.ComponentModel.DataAnnotations.ValidationAttribute。如下面的代碼片斷所示,ValidationAttribute具有一個字元串類型的ErrorMessage屬性用于指定驗證錯誤消息。出于對本地化或者對錯誤消息單獨維護的需要,我們可以采用資源檔案的方式來儲存錯誤消息,在這種情況下我們隻需要通過ErrorMessageResourceName和ErrorMessageResourceType這兩個屬性指定錯誤消息所在資源項的名稱和類型即可。
如果我們通過ErrorMessage屬性指定一個字元串作為驗證錯誤消息,又通過ErrorMessageResourceName/ErrorMessageResourceType屬性指定了錯誤消息資源項對應的名稱和類型,後者具有更高的優先級。ValidationAttribute具有一個受保護的隻讀屬性ErrorMessageString用于傳回最終的錯誤消息文本。
對于錯誤消息的定義,我們可以定義一個完整的消息,比如“年齡必需在18至25之間”。但是對于像資源檔案這種對錯誤消息進行獨立維護的情況,為了讓定義的資源文本能夠最大限度地被重用,我們傾向于定義一個包含占位符的文本模闆,比如“{DisplayName}必需在{LowerBound}和{UpperBound}之間”,這樣消息适用于所有基于數值範圍的驗證。對于後者,模闆中的占位符可以在虛方法FormatErrorMessage中進行替換。該方法中的參數name實際上代表的是對應的顯示名稱,即對應ModelMetadata的DisplayName屬性。
FormatErrorMessage方法在ValidationAttribute中的預設實作僅僅是簡單地調用String的靜态方法Format将參數name作為替換占位符的參數,具體的定義如下。是以在預設的情況下,我們在定義錯誤消息模闆的時候,隻允許包含唯一一個針對顯示名稱的占位符“{0}”。如果具有額外的占位符,或者不需要采用基于序号(“{0}”)的定義方法(比如采用類似于“{DisplayName}”這種基于文字的占位符更具可讀性),隻需要重寫FormatErrorMessage方法即可。
當我們通過繼承ValidationAttribute建立我們自己的驗證特性的時候,可以通過重寫公有方法IsValid或者受保護方法IsValid來實作我們自定義的驗證邏輯。我們之是以能夠通過重寫任一個IsValid方法是我們自定義驗證邏輯生效的原因在于這兩個方法在ValidationAttribute特殊的定義方法。按照這兩個方法在ValidationAttribute中的定義,它們之間存在互相調用的關系,而這種互相調用必然造成“死循環”,是以我們需要重寫至少其中一個方法比避免“死循環”的方法。這裡的“死循環”被加上的引号,是因為ValidationAttribute在内部作了處理,當這種情況出現的時候會抛出一個NotImplementedException異常。
為了驗證對虛方法IsValid重寫的必要性,我們來做一個簡單的執行個體示範。在一個控制台應用中我們分别編寫了如上兩段程式,其中通過繼承ValidationAttribute定義了一個ValidatorAttribute,但是沒有重寫任何一個IsValid方法。當我們在Debug模式下分别運作這兩段程式的時候,都會抛出如下圖所示的NotImplementedException異常,提示“此類尚未實作
IsValid(object value)。首選入口點是 GetValidationResult(),并且類應重寫
IsValid(object value, ValidationContext context)。”

對于定義在ValidationAttribute中的IsValid方法的預設實作來說,在驗證失敗的情況下會傳回一個具體的ValidationResult對象,如果指定的ValidationContext不為Null,那麼其MemberName屬性表示的成員名稱将會包含在該ValidationResult對象的MemberNames清單中。而ValidationContext的DisplayName屬性将會作為調用FormatErrorMessage的參數,該方法調用得到的完整的錯誤消息将會作為ValidationResult的ErrorMessage屬性。如果通過驗證,則直接傳回Null。
在System.ComponentModel.DataAnnotations命名空間下定義了一系列繼承自ValidationAttribute的驗證特性,這些驗證特性大都直接應用在自定義資料類型的某個屬性上根據相應的驗證規則對屬性值實施驗證。這些預定義驗證特性不是本篇文章論述的重點,是以我們在這裡隻是對它們作一個概括性的介紹:
RequiredAttribute:用于驗證必需資料字段。
RangeAttribute:用于驗證數值字段的值是否在指定的範圍之内。
StringLengthAttribute:用于驗證目标字段的字元串長度是否在指定的範圍之内。
MaxLengthAttribute/MinLengthAttribute:用于驗證字元/數組字典的長度是否小于/大于指定的上/下限。
RegularExpressionAttribute:用于驗證字元串字段的格式是否與指定的正規表達式相比對。
CompareAttribute:用于驗證目标字段的值是否與另一個字段值一緻,在使用者注冊場景中可以用于确認兩次輸入密碼的一緻性。
CustomValidationAttribute:指定一個用于驗證目标成員的驗證類型和驗證方法。
對于上面列出的這些預定義ValidationAttribute,它們都具有一個相同的特性,那就是在同一個目标元素中隻能應用一次,這可以通過應用在它們之前的AttributeUsageAttribute特性的定義看出來。以如下所示的RequiredAttribute為例,應用在該類型上的AttributeUsageAttrribute特性的AllowMultiple屬性被設定為False。
但是是否意味着如果我們在定義ValidationAttribute的時候将應用在它上面的AttributeUsageAttrribute特性的AllowMultiple設定為True就可以将它們多次應用到被驗證的屬性或者類型上了呢?我們不妨通過執行個體示範的方式來說明這個問題。
我們知道RangeAttribute可以幫助我們驗證目标字段值的範圍,但是有時候我們需要進行“條件性範圍驗證”。舉個例子,我們現在對于對某個員工的薪水進行驗證,但是不同級别的員工的薪水範圍是不同的,為此我們建立了一個名為RangeIfAttribute的驗證特性輔助我們針對不同級别的薪水範圍進行驗證。如下面的代碼片斷所示,我們将三個RangeIfAttribute特性應用到了表示薪水的Salary屬性上,分别針對三個級别(G7、G8和G9)的薪水範圍作了設定。
RangeIfAttribute特性的定義如下所示,它直接繼承自RangeAttribute。RangeIfAttribute實際上就是根據容器對象的另一個屬性值來決定是否對目标屬性值實施驗證,屬性Property和Value就分别代表這個這個屬性和與之比對的值。在重寫的IsValid方法中,我們通過反射擷取到了容器對象用于比對的屬性值,如果該值與Value屬性值相比對,則調用基類同名法方法對指定對象進行驗證,否則直接傳回ValidationResult.Success(Null)。而應用在RangeIfAttribute上的AttributeUsageAttribute特性的AllowMultiple被設定為True。
那麼這樣一個RangeIfAttribute特性真的能夠按照我們期望的方式進行驗證嗎?為此我們通過Visual
Studio的ASP.NET
MVC項目模闆建立了一個空的Web應用,我們将上面的Employee類型定義其中,然後建立一個具有如下定義的HomeController。在Action方法Index中,我們建立了一個DataAnnotationsModelValidatorProvider對象,通過它擷取針對Employee的Salary屬性的所有DataAnnotationsModelValidator并将其類型名稱呈現出來。
當我們運作該程式時,會在浏覽器上呈現如下所示的輸出結果。該輸出結果意味着隻有兩個DataAnnotationsModelValidator最終應用到Employee的Salary屬性,其中用于驗證必要性的RequiredAttributeAdapter是系統自動添加的(因為Salary屬性為非空值類型,被認為是必需的),另一個自然來源于應用在該屬性上的RangeIfAttribute特性。但是我們一共應用了三個RangeIfAttribute特性在Salary屬性上,為何隻有一個DataAnnotationsModelValidator被建立呢?
我們知道Attribute具有一個名為TypeId的object類型屬性,預設傳回代表自身類型的Type對象。Model驗證系統在根據ValidationAttribute特性建立相應的DataAnnotationsModelValidator對象的時候會根據該TypeId屬性值進行分組,同一組的ValidationAttribute隻會選擇第一個。這就意味着對于多個應用到相同目标元素的同類ValidationAttribute,有且隻有一個是有效的。那麼如何來解決這個問題呢?其實很簡單,既然Model驗證系統在根據Attribute的TypeId進行驗證特性的篩選,我們隻需要通過重寫TypeId屬性是每個ValidationAttribute具有不同的屬性值就可以了。為此我們按照如下的方式在RangeIfAttribute中重寫了TypeId屬性。
再次運作我們的程式将會在浏覽器中得到如下的輸出結果,針對三個RangeIfAttribute特性的三個DataAnnotationsModelValidator被建立出來了。關于通過重寫TypeId而允許多個ValidationAttribute同時應用到相同的目标屬性或者類型的方式不适合用戶端驗證,因為這會導緻多組相同的驗證規則被生成,而這是不允許的。(S608)
<a href="http://www.cnblogs.com/artech/archive/2012/06/06/data-annotations-model-validation-01.html">ASP.NET MVC基于标注特性的Model驗證:ValidationAttribute</a>
<a href="http://www.cnblogs.com/artech/archive/2012/06/07/data-annotations-model-validation-02.html">ASP.NET MVC基于标注特性的Model驗證:DataAnnotationsModelValidator</a>
<a href="http://www.cnblogs.com/artech/archive/2012/06/08/data-annotations-model-validation-03.html">ASP.NET MVC基于标注特性的Model驗證:DataAnnotationsModelValidatorProvider</a>
<a href="http://www.cnblogs.com/artech/archive/2012/06/11/data-annotations-model-validation-04.html">ASP.NET MVC基于标注特性的Model驗證:将ValidationAttribute應用到參數上</a>
<a href="http://www.cnblogs.com/artech/archive/2012/06/12/data-annotations-model-validation-05.html">ASP.NET MVC基于标注特性的Model驗證:一個Model,多種驗證規則</a>
作者:蔣金楠
微信公衆賬号:大内老A
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識别二維碼)關注個人公衆号(原來公衆帳号蔣金楠的自媒體将會停用)。
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。
<a href="http://www.cnblogs.com/artech/archive/2012/06/06/data-annotations-model-validation-01.html" target="_blank">原文連結</a>