通过扩展改善ASP.NET MVC的验证机制[实现篇]
在《使用篇》中我們談到擴展的驗證編程方式,并且演示了本解決方案的三大特性:消息提供機制的分離、多語言的支持和多驗證規則的支持,我們現在來看看這樣的驗證解決方案最終是如何實現的。
目錄:
一、為驗證創建一個上下文:ValidatorContext
二、通過自定義ActionInvoker在進行操作執行之前初始化上下文
三、為Validator創建基類:ValidatorBaseAttribute
四、通過自定義ModelValidatorProvider在驗證之前將不匹配Validator移除
五、RequiredValidatorAttribute的定義
一、為驗證創建一個上下文:ValidatorContext
“基于某個規則的驗證”是本解決方案一個最大的賣點。為了保持以驗證規則名稱為核心的上下文信息,我定義了如下一個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: }二、通過自定義ActionInvoker在進行操作執行之前初始化上下文
通過《使用篇》中我們知道當前的驗證規則名稱是通過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: }三、為Validator創建基類:ValidatorBaseAttribute
接下來我們才來看看真正用于驗證的驗證特性如何定義。我們的驗證特性都直接或者間接地繼承自具有如下定義的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
四、通過自定義ModelValidatorProvider在驗證之前將不匹配Validator移除
應用在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的定義
最后我們來看看用于驗證必需字段的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: }轉載于:https://www.cnblogs.com/artech/archive/2012/01/13/aspnet-mvc-validation-02.html
總結
以上是生活随笔為你收集整理的通过扩展改善ASP.NET MVC的验证机制[实现篇]的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JSP页面去缓存
- 下一篇: python自带的解释器叫做_21条py