閱讀476 返回首頁    go 汽車大全


通過擴展改善ASP.NET MVC的驗證機製[實現篇]

在《使用篇》中我們談到擴展的驗證編程方式,並且演示了本解決方案的三大特性:、和

目錄:
一、為驗證創建一個上下文:ValidatorContext
二、通過自定義ActionInvoker在進行操作執行之前初始化上下文
三、為Validator創建基類:ValidatorBaseAttribute
四、通過自定義ModelValidatorProvider在驗證之前將不匹配Validator移除
五、RequiredValidatorAttribute的定義

“基於某個規則的驗證”是本解決方案一個最大的賣點。為了保持以驗證規則名稱為核心的上下文信息,我定義了如下一個ValidatorContext(我們本打算將其命名為ValidationContext,無奈這個類型已經存在)。ValidatorContext的屬性RuleName和Culture表示當前的驗證規則和語言文化(默認值為當前線程的CurrentUICulture),而字典類型的屬性Properties用戶存放一些額外信息。當前ValidationContext的獲取與設置通過靜態Current完成。

   1: public class ValidatorContext
   2: {
   3:     [ThreadStatic]
   4:     private static ValidatorContext current;
   5:  
   6:     public string RuleName { get; private set; }
   7:     public CultureInfo Culture { get; private set; }
   8:     public IDictionary<string, object> Properties { get; private set; }
   9:  
  10:     public ValidatorContext(string ruleName, CultureInfo culture=null)
  11:     {
  12:         this.RuleName = ruleName;
  13:         this.Properties = new Dictionary<string, object>();
  14:         this.Culture = culture??CultureInfo.CurrentUICulture;
  15:     }
  16:  
  17:     public static ValidatorContext Current
  18:     {
  19:         get { return current; }
  20:         set { current = value; }
  21:     }
  22: }

我們為ValidatorContext定義了如下一個匹配的ValidatorContextScope對象用於設置ValidatorContext的作用範圍。

   1: public class ValidatorContextScope : IDisposable
   2: {
   3:     private ValidatorContext current = ValidatorContext.Current;
   4:     public ValidatorContextScope(string ruleName, CultureInfo culture = null)
   5:     {
   6:         ValidatorContext.Current = new ValidatorContext(ruleName, culture);
   7:     }
   8:     public void Dispose()
   9:     {
  10:         if (null == current)
  11:         {
  12:             foreach (object property in ValidatorContext.Current.Properties.Values)
  13:             {
  14:                 IDisposable disposable = property as IDisposable;
  15:                 if (null != disposable)
  16:                 {
  17:                     disposable.Dispose();
  18:                 }
  19:             }
  20:         }
  21:         ValidatorContext.Current = current;
  22:     }
  23: }

通過《使用篇》中我們知道當前的驗證規則名稱是通過ValidationRuleAttribute來設置的,該特性不僅僅可以應用在Action方法上,也可以應用在Controller類型上。當然Action方法上的ValidationRuleAttribute具有更高的優先級。如下麵的代碼片斷所示,ValidationRuleAttribute就是一個包含Name屬性的普通Attribute而已。

   1: [AttributeUsage( AttributeTargets.Class| AttributeTargets.Method)]
   2: public class ValidationRuleAttribute:Attribute
   3: {
   4:     public string Name { get; private set; }
   5:     public ValidationRuleAttribute(string name)
   6:     {
   7:         this.Name = name;
   8:     }
   9: }

很顯然,以當前驗證規則驗證規則為核心的ValidatorContext需要在Action操作之前設置(嚴格地說應該在進行Model綁定之前),而在Action操作完成後清除。很自然地,我們可以通過自定義ActionInvoker來完成,為此我定義了如下一個直接繼承自ControllerActionInvoker的ExtendedControllerActionInvoker類。

   1: public class ExtendedControllerActionInvoker : ControllerActionInvoker
   2: {
   3:     public ExtendedControllerActionInvoker()
   4:     { 
   5:         this.CurrentCultureAccessor= (context=>
   6:             {
   7:                 string culture = context.RouteData.GetRequiredString("culture");
   8:                 if(string.IsNullOrEmpty(culture))
   9:                 {
  10:                     return null;
  11:                 }
  12:                 else
  13:                 {
  14:                     return new CultureInfo(culture);
  15:                 }
  16:             });
  17:     }
  18:     public virtual Func<ControllerContext, CultureInfo> CurrentCultureAccessor { get; set; }
  19:     public override bool InvokeAction(ControllerContext controllerContext, string actionName)
  20:     {
  21:         CultureInfo originalCulture = CultureInfo.CurrentCulture;
  22:         CultureInfo originalUICulture = CultureInfo.CurrentUICulture;
  23:         try
  24:         {
  25:             CultureInfo culture = this.CurrentCultureAccessor(controllerContext);
  26:             if (null != culture)
  27:             {
  28:                 Thread.CurrentThread.CurrentCulture = culture;
  29:                 Thread.CurrentThread.CurrentUICulture = culture;
  30:             }
  31:             var controllerDescriptor = this.GetControllerDescriptor(controllerContext);
  32:             var actionDescriptor = this.FindAction(controllerContext, controllerDescriptor, actionName);
  33:             ValidationRuleAttribute attribute = actionDescriptor.GetCustomAttributes(true).OfType<ValidationRuleAttribute>().FirstOrDefault() as ValidationRuleAttribute;
  34:             if (null == attribute)
  35:             {
  36:                 attribute = controllerDescriptor.GetCustomAttributes(true).OfType<ValidationRuleAttribute>().FirstOrDefault() as ValidationRuleAttribute;
  37:             }
  38:             string ruleName = (null == attribute) ? string.Empty : attribute.Name;
  39:             using (ValidatorContextScope contextScope = new ValidatorContextScope(ruleName))
  40:             {
  41:                 return base.InvokeAction(controllerContext, actionName);
  42:             }
  43:         }
  44:         catch
  45:         {
  46:             throw;
  47:         }
  48:         finally
  49:         {
  50:             Thread.CurrentThread.CurrentCulture = originalCulture;
  51:             Thread.CurrentThread.CurrentUICulture = originalUICulture;
  52:         }
  53:     }
  54: }

如上麵的代碼片斷所示,在重寫的InvokeAction方法中我們通過ControllerDescriptor/ActionDescriptor得到應用在Controller類型/Action方法上的ValidationRuleAttribute特性,並或者到設置的驗證規則名稱。然後我們創建ValidatorContextScope對象,而針對基類InvokeAction方法的執行就在該ValidatorContextScope中執行的。初次之外,我們還對當前線程的Culture進行了相應地設置,默認的Culture 信息來源於當前RouteData。

為了更方便地使用ExtendedControllerActionInvoker,我們定義了一個抽象的Controller基類:BaseController。BaseController是Controller的子類,在構造函數中我們將ActionInvoker屬性設置成我們自定義的ExtendedControllerActionInvoker對象。

   1: public abstract class BaseController: Controller
   2: {
   3:     public BaseController()
   4:     {
   5:         this.ActionInvoker = new ExtendedControllerActionInvoker();
   6:     }
   7: }

接下來我們才來看看真正用於驗證的驗證特性如何定義。我們的驗證特性都直接或者間接地繼承自具有如下定義的ValidatorBaseAttribute,而它使ValidationAttribute的子類。如下麵的代碼片斷所示,ValidatorBaseAttribute還實現了IClientValidatable接口,以提供對客戶端驗證的支持。屬性RuleName、MessageCategory、MessageId和Culture分別代表驗證規則名稱、錯誤消息的類別和ID號(通過這兩個屬性通過MessageManager這個獨立的組件獲取完整的錯誤消息)和基於的語言文化。

   1: [AttributeUsage(AttributeTargets.Class|AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
   2: public abstract class ValidatorBaseAttribute : ValidationAttribute, IClientValidatable
   3: {
   4:     
   5:     public string RuleName { get; set; }
   6:     public string MessageCategory { get; private set; }
   7:     public string MessageId { get; private set; }
   8:     public string Culture { get; set; }
   9:  
  10:     public ValidatorBaseAttribute(MessageManager messageManager, string messageCategory, string messageId, params object[] args)
  11:         : base(() => messageManager.FormatMessage(messageCategory, messageId, args))
  12:     {
  13:         this.MessageCategory = messageCategory;
  14:         this.MessageId = messageId;
  15:     }
  16:  
  17:     public ValidatorBaseAttribute(string messageCategory, string messageId, params object[] args)
  18:         : this(MessageManagerFactory.GetMessageManager(), messageCategory, messageId, args)
  19:     { }
  20:  
  21:     public virtual bool Match(ValidatorContext context, IEnumerable<ValidatorBaseAttribute> validators)
  22:     {
  23:         if (!string.IsNullOrEmpty(this.RuleName))
  24:         {
  25:             if (this.RuleName != context.RuleName)
  26:             {
  27:                 return false;
  28:             }
  29:         }
  30:  
  31:         if (!string.IsNullOrEmpty(this.Culture))
  32:         {                
  33:             if (string.Compare(this.Culture, context.Culture.Name, true) != 0)
  34:             {
  35:                 return false;
  36:             }
  37:         }
  38:  
  39:         if (string.IsNullOrEmpty(this.Culture))
  40:         {
  41:             if (validators.Any(validator => validator.GetType() == this.GetType() && string.Compare(validator.Culture, context.Culture.Name, true) == 0))
  42:             {
  43:                 return false;
  44:             }
  45:         }
  46:         return true;
  47:     }
  48:     public abstract IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context);
  49:     private object typeId;
  50:     public override object TypeId
  51:     {
  52:         get { return (null == typeId) ? (typeId = new object()) : typeId; }
  53:     }
  54: }

由於我們需要將多個相同類型的Validator特性應用到某個類型或者字段/屬性上,我們需要通過AttributeUsageAttribute將AllowMultiple屬性設置為True,此外需要重寫TypeId屬性。至於為什麼需需要這麼做,可以參考我的上一篇文章《在ASP.NET MVC中如何應用多個相同類型的ValidationAttribute?》。對於應用在同一個目標元素的多個相同類型的Validator特性,隻有與當前ValidatorContext相匹配的才能執行,我們通過Match方法來進行匹配性的判斷,具體的邏輯是這樣的:

  • 在顯式設置了RuleName屬性情況下,如果不等於當前驗證規則,直接返回False;
  • 在顯式設置了Culture屬性情況下,如果與當前語言文化不一致,直接返回False;
  • 在沒有設置Culture屬性(語言文化中性)情況下,如果存在另一個同類型的Validator與當前的語言文化一致,也返回False;
  • 其餘情況返回True

應用在Model類型或其屬性/字段上的ValidationAttribute最終通過對應的ModelValidatorProvider(DataAnnotationsModelValidatorProvider)用於創建ModelValidator(DataAnnotationsModelValidator)。我們必須在ModelValidator創建之前將不匹配的Validator特性移除,才能確保隻有與當前ValidatorContext相匹配的Validator特性參與驗證。為此我們通過繼承DataAnnotationsModelValidator自定義了如下一個ExtendedDataAnnotationsModelValidator。

   1: public class ExtendedDataAnnotationsModelValidatorProvider : DataAnnotationsModelValidatorProvider
   2: {
   3:     protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
   4:     {
   5:         var validators = attributes.OfType<ValidatorBaseAttribute>();
   6:         var allAttributes = attributes.Except(validators).ToList();
   7:         foreach (ValidatorBaseAttribute validator in validators)
   8:         {
   9:             if (validator.Match(ValidatorContext.Current, validators))
  10:             {
  11:                 allAttributes.Add(validator);
  12:             }
  13:         }
  14:         return base.GetValidators(metadata, context, allAttributes);
  15:     }
  16: }

如上麵的代碼片斷所示,在重寫的GetClientValidationRules方法中,輸入參數attributes表示所有的ValidationAttribute,在這裏我們根據調用ValidatorBaseAttribute的Match方法將不匹配的Validator特性移除,然後根據餘下的ValidationAttribute列表調用基類GetValidators方法創建ModelValidator列表。值得一提的是,關於System.Attribute的Equals/GetHashCode方法的問題就從這個方法中發現的(詳情參見《為什麼System.Attribute的GetHashCode方法需要如此設計?》)。自定義ExtendedDataAnnotationsModelValidator在Global.asax的Application_Start方法中通過如下的方式進行注冊。

   1: protected void Application_Start()
   2: {
   3:      //...
   4:     var provider = ModelValidatorProviders.Providers.OfType<DataAnnotationsModelValidatorProvider>().FirstOrDefault();
   5:     if (null != provider)
   6:     {
   7:         ModelValidatorProviders.Providers.Remove(provider);
   8:     }
   9:     ModelValidatorProviders.Providers.Add(new ExtendedDataAnnotationsModelValidatorProvider());
  10: }

最後我們來看看用於驗證必需字段的RequiredValidatorAttribute如何定義。IsValid用於服務端驗證,而GetClientValidationRules生成調用客戶端驗證規則。

   1: [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)]
   2: public class RequiredValidatorAttribute : ValidatorBaseAttribute
   3: {
   4:     public RequiredValidatorAttribute(string messageCategory, string messageId, params object[] args)
   5:         : base(messageCategory, messageId, args)
   6:     { }   
   7:  
   8:     public override bool IsValid(object value)
   9:     {
  10:         if (value == null)
  11:         {
  12:             return false;
  13:         }
  14:         string str = value as string;
  15:         if (str != null)
  16:         {
  17:             return (str.Trim().Length != 0);
  18:         }
  19:         return true;
  20:     }
  21:  
  22:     public override IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
  23:     {
  24:         return new ModelClientValidationRequiredRule[] { new ModelClientValidationRequiredRule(this.ErrorMessageString) };
  25:     }
  26: }

作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-26 14:04:28

  上一篇:go  在ASP.NET MVC中如何應用多個相同類型的ValidationAttribute?
  下一篇:go  一個通過JSONP跨域調用WCF REST服務的例子(以jQuery為例)