對於Model驗證,理想的設計應該是場景驅動的,而不是Model(類型)驅動的,也就是對於同一個Model對象,在不同的使用場景中可能具有不同的驗證規則。舉個簡單的例子,對於一個表示應聘者的數據對象來說,針對應聘的崗位不同,肯定對應聘者的年齡、性別、專業技能等方面有不同的要求。但是ASP.NET MVC的Model驗證確是Model驅動的,因為驗證規則以驗證特性的形式應用到Model類型及其屬性上。這樣的驗證方式實際上限制了Model類型在基於不同驗證規則的使用場景中的重用。通過上一篇文章《將ValidationAttribute應用到參數上》的擴展我們將驗證特性直接應用在參數上變成了可能,這從一定程度上解決了這個問題,但是只能解決部分問題,因為應用到參數的驗證特性只能用於針對參數類型級別的驗證,而不能用於針對參數類型屬性級別的驗證(源代碼從這里下載)。[本文已經同步到《How ASP.NET MVC Works?》中]
目錄
一、同一個Model在采用不同的驗證規則
二、新的基類ValidatorAttribute
三、指定當前采用的驗證規則:ValidationRuleAttribute
四、新的Controller基類:RuleBasedController
五、自定義ModelValidatorProvider:RuleBasedValidatorProvider
一、同一個Model在采用不同的驗證規則
現在我們通過利用對ASP.NET MVC的擴展來實現一種基於不同驗證規則的Model驗證。為了讓讀者對這種認證方式有一個感官的認識,我們來看看這個擴展最終實現怎樣的驗證效果。在通過Visual Studio的ASP.NET MVC 項目模板創建的空Web應用中,我們定義了如下一個Person類型作為Model。
1: public class Person
2: {
3: [DisplayName("姓名")]
4: public string Name { get; set; }
5:
6: [DisplayName("性別")]
7: public string Gender { get; set; }
8:
9: [DisplayName("年齡")]
10: [RangeValidator(10, 20, RuleName = "Rule1", ErrorMessage = "{0}必須在{1}和{2}之間!")]
11: [RangeValidator(20, 30, RuleName = "Rule2", ErrorMessage = "{0}必須在{1}和{2}之間!")]
12: [RangeValidator(30, 40, RuleName = "Rule3", ErrorMessage = "{0}必須在{1}和{2}之間!")]
13: public int Age { get; set; }
14: }
在表示年齡的Age屬性上應用了三個RangeValidatorAttribute(不是RangeAttribute),它們對應針對年齡的三種不同的驗證規則,RuleName屬性表示規則名稱。三種驗證規則(Rule1、Rule2和Rule3)分別要求年齡分別在10到20、20到30和30到40歲之間。
然后我們定義了具有如下定義HomeController,它具有三組Action方法(Index、Rule1和Rule2)。方法Rule1、Rule2和HomeController類上應用了一個ValidationRuleAttribute特性用於指定了當前采用的驗證規則。用於指定驗證規則的ValidationRuleAttribute特性可以同時應用於Controller類型和Action方法上,應用於后者的ValidationRuleAttribute特性具有更高的優先級。針對HomeController的定義,Action方法Index、Rule1和Rule2分別采用的驗證規則為Rule3、Rule1和Rule2。
1: [ValidationRule("Rule3")]
2: public class HomeController : RuleBasedController
3: {
4: public ActionResult Index()
5: {
6: return View("person", new Person());
7: }
8: [HttpPost]
9: public ActionResult Index(Person person)
10: {
11: return View("person", person);
12: }
13:
14: [ValidationRule("Rule1")]
15: public ActionResult Rule1()
16: {
17: return View("person", new Person());
18: }
19: [HttpPost]
20: [ValidationRule("Rule1")]
21: public ActionResult Rule1(Person person)
22: {
23: return View("person", person);
24: }
25:
26: [ValidationRule("Rule2")]
27: public ActionResult Rule2()
28: {
29: return View("person", new Person());
30: }
31: [HttpPost]
32: [ValidationRule("Rule2")]
33: public ActionResult Rule2(Person person)
34: {
35: return View("person", person);
36: }
37: }
定義在HomeController中的6個方法均將創建的/接收的Person對象呈現到名為Person的View中,該View的定義如下所示。這是一個將Person類型作為Model的強類型View,在該View中我們將作為Model的Person對象以編輯模式呈現在一個表單中,並在表單中提供一個提交按鈕。
1: @model Person
2: @using (Html.BeginForm())
3: {
4: @Html.EditorForModel()
5: <input type="submit" value="保存" />
6: }
現在運行我們的程序,並通過在瀏覽器中指定相應的地址分別訪問定義在HomeController的三個Action(Index、Rule1和Rule2),一個用於編輯個人信息的表單會呈現出來。然后我們根據三個Action方法采用的驗證規則輸入不合法的年齡,然后點擊“保存”按鈕,我們會看到輸入的年齡按照對應的規則被驗證了,具體的驗證效果如下圖所示。
二、新的基類ValidatorAttribute
我們現在就來具體談談上面這個例子所展示的基於不同規則的Model驗證是如何實現的。首先我們需要重建一套新的驗證特性體系,只為我們能夠指定具體的驗證規則。為此我們定義了一個抽象的ValidatorAttribute類型,如下面的代碼片斷所示,ValidatorAttribute直接繼承自ValidationAttribute,屬性RuleName表示采用的驗證規則名稱。我們重寫了TypeId屬性,因為我們需要在相同的屬性或者類型上應用多個同類的ValidatorAttribute。
1: [AttributeUsage( AttributeTargets.Class| AttributeTargets.Property,AllowMultiple = true)]
2: public abstract class ValidatorAttribute: ValidationAttribute
3: {
4: private object typeId;
5: public string RuleName { get; set; }
6: public override object TypeId
7: {
8: get{return typeId ?? (typeId = new object());}
9: }
10: }
上面演示實例采用的RangeValidatorAttribute定義如下,我們可以看到它僅僅是對RangeAttribute的封裝。RangeValidatorAttribute具有與RangeAttribute一致的構造函數定義,並直接使用被封裝的RangeAttribute實施驗證。除了能夠通過RuleName指定具體采用的驗證規則之外,其他的使用方式與RangeAttribute完全一致。
1: [AttributeUsage( AttributeTargets.Property, AllowMultiple = true)]
2: public class RangeValidatorAttribute:ValidatorAttribute
3: {
4: private RangeAttribute rangeAttribute;
5: public RangeValidatorAttribute(int minimum, int maximum)
6: {
7: rangeAttribute = new RangeAttribute(minimum, maximum);
8: }
9: public RangeValidatorAttribute(double minimum, double maximum)
10: {
11: rangeAttribute = new RangeAttribute(minimum, maximum);
12: }
13: public RangeValidatorAttribute(Type type, string minimum, string maximum)
14: {
15: rangeAttribute = new RangeAttribute(type, minimum, maximum);
16: }
17: public override bool IsValid(object value)
18: {
19: return rangeAttribute.IsValid(value);
20: }
21:
22: public override string FormatErrorMessage(string name)
23: {
24: return string.Format(CultureInfo.CurrentCulture, base.ErrorMessageString, new object[] { name, rangeAttribute.Minimum, rangeAttribute.Maximum });
25: }
26: }
三、指定當前采用的驗證規則:ValidationRuleAttribute
ValidatorAttribte的RuleName屬性僅僅指定了驗證特性采用的驗證規則名稱,當前應在采用的驗證規則通過應用在Action方法或者Controller類型上的ValidationRuleAttribute特性還指定。如下所示的就是ValidationRuleAttribute的定義,它僅僅包含一個表示當前采用的驗證規則名稱的RuleName屬性的特性而已。
1: [AttributeUsage( AttributeTargets.Class| AttributeTargets.Method)]
2: public class ValidationRuleAttribute: Attribute
3: {
4: public string RuleName { get; private set; }
5: public ValidationRuleAttribute(string ruleName)
6: {
7: this.RuleName = ruleName;
8: }
9: }
四、新的Controller基類:RuleBasedController
對於這個用於實現針對不同驗證規則的擴展來說,其核心是如何將通過ValidationRuleAttribute特性設置的驗證規則應用到ModelValidator的提供機制中,使之篩選出與當前驗證規則匹配的驗證特性,在這里我們依然使用Controller上下文來保存這個這個驗證規則名稱。細心的讀者應該留意到了上面演示實例中創建的HomeController不是繼承自Controller,而是繼承自RuleBasedController,這個自定義的Controller基類定義如下。
1: public class RuleBasedController: Controller
2: {
3: private static Dictionary<Type, ControllerDescriptor> controllerDescriptors = new Dictionary<Type, ControllerDescriptor>();
4: public ControllerDescriptor ControllerDescriptor
5: {
6: get
7: {
8: ControllerDescriptor controllerDescriptor;
9: if (controllerDescriptors.TryGetValue(this.GetType(), out controllerDescriptor))
10: {
11: return controllerDescriptor;
12: }
13: lock (controllerDescriptors)
14: {
15: if (!controllerDescriptors.TryGetValue(this.GetType(), out controllerDescriptor))
16: {
17: controllerDescriptor = new ReflectedControllerDescriptor(this.GetType());
18: controllerDescriptors.Add(this.GetType(), controllerDescriptor);
19: }
20: return controllerDescriptor;
21: }
22: }
23: }
24: protected override IAsyncResult BeginExecuteCore(AsyncCallback callback, object state)
25: {
26: SetValidationRule();
27: return base.BeginExecuteCore(callback, state);
28: }
29: protected override void ExecuteCore()
30: {
31: SetValidationRule();
32: base.ExecuteCore();
33: }
34: private void SetValidationRule()
35: {
36: string actionName = this.ControllerContext.RouteData.GetRequiredString("action");
37: ActionDescriptor actionDescriptor = this.ControllerDescriptor.FindAction(this.ControllerContext, actionName);
38: if (null != actionDescriptor)
39: {
40: ValidationRuleAttribute validationRuleAttribute = actionDescriptor.GetCustomAttributes(true).OfType<ValidationRuleAttribute>().FirstOrDefault() ??
41: this.ControllerDescriptor.GetCustomAttributes(true).OfType<ValidationRuleAttribute>().FirstOrDefault() ??
42: new ValidationRuleAttribute(string.Empty);
43: this.ControllerContext.RouteData.DataTokens.Add("ValidationRuleName", validationRuleAttribute.RuleName);
44: }
45: }
46: }
在繼承自Controller的RuleBasedController中,ExecuteCore和BeginExecuteCore方法被重寫,在調用基類的同名方法之前,方法SetValidationRule方法被調用將應用在當前Action方法或者Controller類型上的ValidationRuleAttribute特性指定的驗證規則名稱保存到當前Controller上下文中。由於對Action方法和Controller類上特性的解析需要使用到用於描述Controller的ControllerDescriptor對象,處於性能考慮,我們對該對象進行了全局緩存。
五、自定義ModelValidatorProvider:RuleBasedValidatorProvider
對於應用在同一個屬性或者類型上的多個基於不同驗證規則的ValidatorAttribute,對應的驗證規則名稱並沒有應用到具體的驗證邏輯中。以上面定義的RangeValidatorAttribute為例,具體的驗證邏輯通過被封裝的RangeAttribute來實現,如果我們不做任何的處理,所有的基於不同規則的RangeValidatorAttribute都還參與到最終的Model驗證過程中。我們必須作的是在根據驗證特性創建ModelValidator的時候只選擇那些與當前驗證規則一直的ValidatorAttribute,這樣的操作實現在具有如下定義的RuleBasedValidatorProvider中。
1: public class RuleBasedValidatorProvider : DataAnnotationsModelValidatorProvider
2: {
3: protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
4: {
5: object validationRuleName = string.Empty;
6: context.RouteData.DataTokens.TryGetValue("ValidationRuleName", out validationRuleName);
7: string ruleName = validationRuleName.ToString();
8: attributes = this.FilterAttributes(attributes, ruleName);
9: return base.GetValidators(metadata, context, attributes);
10: }
11:
12: private IEnumerable<Attribute> FilterAttributes(IEnumerable<Attribute> attributes, string validationRule)
13: {
14: var validatorAttributes = attributes.OfType<ValidatorAttribute>();
15: var nonValidatorAttributes = attributes.Except(validatorAttributes);
16: List<ValidatorAttribute> validValidatorAttributes = new List<ValidatorAttribute>();
17:
18: if (string.IsNullOrEmpty(validationRule))
19: {
20: validValidatorAttributes.AddRange(validatorAttributes.Where(v => string.IsNullOrEmpty(v.RuleName)));
21: }
22: else
23: {
24: var groups = from validator in validatorAttributesgroup validator by validator.GetType();
25: foreach (var group in groups)
26: {
27: ValidatorAttribute validatorAttribute = group.Where(v => string.Compare(v.RuleName, validationRule, true) == 0).FirstOrDefault();
28: if (null != validatorAttribute)
29: {
30: validValidatorAttributes.Add(validatorAttribute);
31: }
32: else
33: {
34: validatorAttribute = group.Where(v => string.IsNullOrEmpty(v.RuleName)).FirstOrDefault();
35: if (null != validatorAttribute)
36: {
37: validValidatorAttributes.Add(validatorAttribute);
38: }
39: }
40: }
41: }
42: return nonValidatorAttributes.Union(validValidatorAttributes);
43: }
44: }
如上面的代碼所示,RuleBasedValidatorProvider繼承自DataAnnotationsModelValidatorProvider,基於當前驗證規則(從當前的Controller上下文中提取)對ValidatorAttribute的篩選,以及ModelValidator的創建通過重寫的GetValidators方法實現。具體的篩選機制是:如果當前的驗證規則存在,則選擇與之具有相同規則名稱的第一個ValidatorAttribute,如果這樣的ValidatorAttribute找不到,則選擇第一個沒有指定驗證規則的ValidatorAttribute;如果當前的驗證規則沒有指定,那么也選擇第一個沒有指定驗證規則的ValidatorAttribute。
在讓我們的Controller繼承自RuleBasedController之后,我們需要在Global.asax中通過如下的方式對自定義的RuleBasedValidatorProvider進行注冊,然后我們的應用就能按照我們期望的方式根據你指定的驗證規則實施Model驗證了。
1: public class MvcApplication : System.Web.HttpApplication
2: {
3: //其他成員
4: protected void Application_Start()
5: {
6: //其他操作
7: DataAnnotationsModelValidatorProvider validator = ModelValidatorProviders.Providers.OfType<DataAnnotationsModelValidatorProvider>().FirstOrDefault();
8: if(null != validator)
9: {
10: ModelValidatorProviders.Providers.Remove(validator);
11: }
12: ModelValidatorProviders.Providers.Add(new RuleBasedValidatorProvider());
13: }
14: }