ASP.NET MVC基於標注特性的Model驗證:ValidationAttribute


通過前面的介紹我們知道ModelValidatorProviders的靜態只讀Providers維護着一個全局的ModelValidatorProvider列表,最終用於Model驗證的ModelValidator都是通過這些ModelValidatorProvider來提供的。對於該列表默認包含的三種ModelValidatorProvider來說,DataAnnotationsModelValidatorProvider無疑是最重要的,ASP.NET MVC默認提供的基於數據標注特性的聲明式Model驗證就是通過DataAnnotationsModelValidatorProvider提供的DataAnnotationsModelValidator來實現的。[本文已經同步到《How ASP.NET MVC Works?》中]

目錄
一、ValidationAttribute特性
二、驗證消息的定義
三、驗證的執行
四、預定義ValidationAttribute
五、應用ValidationAttribute特性的唯一性

一、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)。”

image

受保護的IsValid方法除了包含一個表示被驗證對象的參數value,還具有具有如下定義的類型為ValidationContext的參數validationContext。顧名思義,ValidationContext旨在為當前的驗證維護相應的上下文信息,這些信息包括通過ObjectInstance和ObjectType屬性表示的驗證對象及其類型,通過MemberName和DisplayName屬性表示的成員名稱(一般指屬性名稱)和顯示名稱。

   1: public sealed class ValidationContext
   2: {    
   3:     //其他成員
   4:     public ValidationContext(object instance);    
   5:     public ValidationContext(object instance, IDictionary<object, object> items);
   6:   
   7:     public string DisplayName { get; set; }
   8:     public string MemberName {  get;  set; }
   9:     public object ObjectInstance {  get; }
  10:     public Type ObjectType { get; }
  11: }

作為該IsValid方法返回值表示驗證結果的對象是一個具有如下定義的類型為ValidationResult的對象。與作為ModelValidator驗證結果的ModelValidationResult類型類似,ValidationResult依然是錯誤消息和成員名稱的組合。不過ModelValidationResult對應某個單一的成員名稱,而ValidationResult包含一組相關成員名稱的列表。

   1: public class ValidationResult
   2: {    
   3:     //其他成員
   4:     public ValidationResult(string errorMessage);
   5:     public ValidationResult(string errorMessage, IEnumerable<string> memberNames);
   6:  
   7:     public string ErrorMessage {  get;  set; }
   8:     public IEnumerable<string> MemberNames {  get; }
   9: }

對於定義在ValidationAttribute中的IsValid方法的默認實現來說,在驗證失敗的情況下會返回一個具體的ValidationResult對象,如果指定的ValidationContext不為Null,那么其MemberName屬性表示的成員名稱將會包含在該ValidationResult對象的MemberNames列表中。而ValidationContext的DisplayName屬性將會作為調用FormatErrorMessage的參數,該方法調用得到的完整的錯誤消息將會作為ValidationResult的ErrorMessage屬性。如果通過驗證,則直接返回Null。

我們可以通過調用ValidationAttribute的公有方法GetValidationResult對指定的對象實施驗證並得到以ValidationResult對象形式返回的驗證結果,最終返回的實際上就是調用受保護方法IsValid的返回值。我們也可以調用Validate方法驗證某個指定的對象,該方法在驗證失敗的情況下會直接拋出一個ValidationException異常,而作為該異常的消息是通過調用FormatErrorMessage方法(將參數name表示的字符串作為參數)格式化后的錯誤消息。

四、預定義ValidationAttribute

在System.ComponentModel.DataAnnotations命名空間下定義了一系列繼承自ValidationAttribute的驗證特性,這些驗證特性大都直接應用在自定義數據類型的某個屬性上根據相應的驗證規則對屬性值實施驗證。這些預定義驗證特性不是本篇文章論述的重點,所以我們在這里只是對它們作一個概括性的介紹:

  • RequiredAttribute:用於驗證必需數據字段。
  • RangeAttribute:用於驗證數值字段的值是否在指定的范圍之內。
  • StringLengthAttribute:用於驗證目標字段的字符串長度是否在指定的范圍之內。
  • MaxLengthAttribute/MinLengthAttribute:用於驗證字符/數組字典的長度是否小於/大於指定的上/下限。
  • RegularExpressionAttribute:用於驗證字符串字段的格式是否與指定的正則表達式相匹配。
  • CompareAttribute:用於驗證目標字段的值是否與另一個字段值一致,在用戶注冊場景中可以用於確認兩次輸入密碼的一致性。
  • CustomValidationAttribute:指定一個用於驗證目標成員的驗證類型和驗證方法。

五、應用ValidationAttribute特性的唯一性

對於上面列出的這些預定義ValidationAttribute,它們都具有一個相同的特性,那就是在同一個目標元素中只能應用一次,這可以通過應用在它們之前的AttributeUsageAttribute特性的定義看出來。以如下所示的RequiredAttribute為例,應用在該類型上的AttributeUsageAttrribute特性的AllowMultiple屬性被設置為False

   1: [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple=false)]
   2: public class RequiredAttribute : ValidationAttribute
   3: {
   4:    //省略成員
   5: }

但是是否意味着如果我們在定義ValidationAttribute的時候將應用在它上面的AttributeUsageAttrribute特性的AllowMultiple設置為True就可以將它們多次應用到被驗證的屬性或者類型上了呢?我們不妨通過實例演示的方式來說明這個問題。

我們知道RangeAttribute可以幫助我們驗證目標字段值的范圍,但是有時候我們需要進行“條件性范圍驗證”。舉個例子,我們現在對於對某個員工的薪水進行驗證,但是不同級別的員工的薪水范圍是不同的,為此我們創建了一個名為RangeIfAttribute的驗證特性輔助我們針對不同級別的薪水范圍進行驗證。如下面的代碼片斷所示,我們將三個RangeIfAttribute特性應用到了表示薪水的Salary屬性上,分別針對三個級別(G7、G8和G9)的薪水范圍作了設定。

   1: public class Employee
   2: {
   3:     public string Name { get; set; }
   4:     public string Grade { get; set; }
   5:  
   6:     [RangeIf("Grade", "G7", 2000, 3000)]
   7:     [RangeIf("Grade", "G8", 3000, 4000)]
   8:     [RangeIf("Grade", "G9", 4000, 5000)]
   9:     public decimal Salary { get; set; }
  10: }

RangeIfAttribute特性的定義如下所示,它直接繼承自RangeAttribute。RangeIfAttribute實際上就是根據容器對象的另一個屬性值來決定是否對目標屬性值實施驗證,屬性Property和Value就分別代表這個這個屬性和與之匹配的值。在重寫的IsValid方法中,我們通過反射獲取到了容器對象用於匹配的屬性值,如果該值與Value屬性值相匹配,則調用基類同名法方法對指定對象進行驗證,否則直接返回ValidationResult.Success(Null)。而應用在RangeIfAttribute上的AttributeUsageAttribute特性的AllowMultiple被設置為True。

   1: [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
   2: public class RangeIfAttribute: RangeAttribute
   3: {
   4:     public string Property { get; set; }
   5:     public string Value { get; set; }
   6:  
   7:     public RangeIfAttribute(string property, string value, double minimum, double maximum)
   8:         : base(minimum, maximum)
   9:     {
  10:         this.Property = property;
  11:         this.Value = value??"";
  12:     }
  13:  
  14:     protected override ValidationResult IsValid(object value, ValidationContext validationContext)
  15:     {
  16:        ...
  17:         PropertyInfo property = validationContext.ObjectType.GetProperty(this.Property);
  18:         object propertyValue = property.GetValue(validationContext.ObjectInstance, null);
  19:         propertyValue = propertyValue ?? "";
  20:         if (propertyValue.ToString()!= this.Value)
  21:         {
  22:             return ValidationResult.Success;
  23:         }
  24:         return base.IsValid(value, validationContext);
  25:     }  
  26: }

那么這樣一個RangeIfAttribute特性真的能夠按照我們期望的方式進行驗證嗎?為此我們通過Visual Studio的ASP.NET MVC項目模板創建了一個空的Web應用,我們將上面的Employee類型定義其中,然后創建一個具有如下定義的HomeController。在Action方法Index中,我們創建了一個DataAnnotationsModelValidatorProvider對象,通過它獲取針對Employee的Salary屬性的所有DataAnnotationsModelValidator並將其類型名稱呈現出來。

   1: public class HomeController : Controller
   2: {
   3:     public void Index()
   4:     {
   5:         DataAnnotationsModelValidatorProvider provider = 
   6:         new DataAnnotationsModelValidatorProvider();
   7:         ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(() => new Employee(), typeof(Employee));
   8:         metadata = metadata.Properties.FirstOrDefault(p => p.PropertyName == "Salary");
   9:         var validators = ModelValidatorProviders.Providers.GetValidators(metadata, ControllerContext);
  10:         foreach (var validator in validators.OfType<DataAnnotationsModelValidator>())
  11:         {
  12:             Response.Write(validator + "<br/>");
  13:         }
  14:     }        
  15: }

當我們運行該程序時,會在瀏覽器上呈現如下所示的輸出結果。該輸出結果意味着只有兩個DataAnnotationsModelValidator最終應用到Employee的Salary屬性,其中用於驗證必要性的RequiredAttributeAdapter是系統自動添加的(因為Salary屬性為非空值類型,被認為是必需的),另一個自然來源於應用在該屬性上的RangeIfAttribute特性。但是我們一共應用了三個RangeIfAttribute特性在Salary屬性上,為何只有一個DataAnnotationsModelValidator被創建呢

   1: System.Web.Mvc.DataAnnotationsModelValidator
   2: System.Web.Mvc.RequiredAttributeAdapter

我們知道Attribute具有一個名為TypeId的object類型屬性,默認返回代表自身類型的Type對象。Model驗證系統在根據ValidationAttribute特性創建相應的DataAnnotationsModelValidator對象的時候會根據該TypeId屬性值進行分組,同一組的ValidationAttribute只會選擇第一個。這就意味着對於多個應用到相同目標元素的同類ValidationAttribute,有且只有一個是有效的。那么如何來解決這個問題呢?其實很簡單,既然Model驗證系統在根據Attribute的TypeId進行驗證特性的篩選,我們只需要通過重寫TypeId屬性是每個ValidationAttribute具有不同的屬性值就可以了。為此我們按照如下的方式在RangeIfAttribute中重寫了TypeId屬性。

   1: [AttributeUsage( AttributeTargets.Field| AttributeTargets.Property, AllowMultiple = true)]
   2: public class RangeIfAttribute: RangeAttribute
   3: {
   4:     //其他成員
   5:     private object typeid;
   6:     public override object TypeId
   7:     {
   8:         get{ return typeid?? (typeid= new object());}
   9:     }
  10: }

再次運行我們的程序將會在瀏覽器中得到如下的輸出結果,針對三個RangeIfAttribute特性的三個DataAnnotationsModelValidator被創建出來了。關於通過重寫TypeId而允許多個ValidationAttribute同時應用到相同的目標屬性或者類型的方式不適合客戶端驗證,因為這會導致多組相同的驗證規則被生成,而這是不允許的。(S608)

   1: System.Web.Mvc.DataAnnotationsModelValidator
   2: System.Web.Mvc.DataAnnotationsModelValidator
   3: System.Web.Mvc.DataAnnotationsModelValidator
   4: System.Web.Mvc.RequiredAttributeAdapter

 

ASP.NET MVC基於標注特性的Model驗證:ValidationAttribute
ASP.NET MVC基於標注特性的Model驗證:DataAnnotationsModelValidator
ASP.NET MVC基於標注特性的Model驗證:DataAnnotationsModelValidatorProvider
ASP.NET MVC基於標注特性的Model驗證:將ValidationAttribute應用到參數上
ASP.NET MVC基於標注特性的Model驗證:一個Model,多種驗證規則


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM