ASP.NET MVC基於標注特性的Model驗證:將ValidationAttribute應用到參數上


ASP.NET MVC默認采用基於標准特性的Model驗證機制,但是只有應用在Model類型及其屬性上的ValidationAttribute才有效。如果我們能夠將ValidationAttribute特性直接應用到參數上,我們不但可以實現簡單類型(比如int、double等)數據的Model驗證,還能夠實現“一個Model類型,多種驗證規則”,本篇文章將為你提供相關的解決方案(源代碼從這里下載)。[本文已經同步到《How ASP.NET MVC Works?》中]

目錄
一、ValidationAttribute本身是可以應用到參數上的
二、為什么需要基於參數的Model驗證?
三、如何得到應用在參數上的ValidationAttribute?
四、自定義ModelValidatorProvider
五、自定義ModelBinder
六、實例演示

一、ValidationAttribute本身是可以應用到參數上的

如果你夠細心應該會發現我們常用的驗證特性都可以直接應用到方法的參數上。以如下所示的RangeAttribute的定義為例,應用在該類型上的AttributeUsageAttribute的定義表明可以標注該特性的目標元素包括參數、字段和屬性。

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

但是對於ASP.NET MVC的Model驗證來說,應用在Action方法參數上的驗證特性起不到任何作用,原因很簡單:用於進行Model驗證的ModelValidator對象是通過基於參數類型的Model元數據來創建的,根本不會去解析應用在參數本身上的驗證特性

二、為什么需要基於參數的Model驗證?

但是在我看到,直接針對Action參數的Model驗證具有很高的實用意義:

  • 有些情況下我們不能對作為Model的數據類型進行修改(比如像int、double和字符串這樣的原生類型);
  • 相同的Model類型在不同的Action方法調用中需要采用不同的驗證規則。

如果我們可以直接將驗證特性應用到參數上面,這兩個問題在一定程度上都可以得到解決。

三、如何得到應用在參數上的ValidationAttribute?

到目前為止,我們對ASP.NET MVC的可擴展的Model驗證系統已經有了一個全面的了解,現在我們通過對它進行相應的擴展使直接應用到參數上的驗證特性能夠生效。我們需要自定義一個ModelValidatorProvider將提供基於應用到參數上的驗證特性的ModelValidator,但在這之前需要解決的另一個問題是如何將應用於參數的特性提供給我們自定義的ModelValidatorProvider。在這里我們將當前ControllerContext作為這些特性的載體。

Action方法的執行通過ActionInvoker來實現,默認的ControllerActionInvoker和AsyncControllerActionInvoker都定義了一個受保護的虛方法GetParameterValue根據用於描述參數的ParameterDescriptor對象和當前的Controller上下文來綁定對應的參數值。那么我們就可以通過繼承ControllerActionInvoker/AsyncControllerActionInvoker以重寫該方法的方式將ParameterDescriptor保存當前的Controller上下文中。

為此我們定義了一個具有如下定義的兩個自定義的ActionInvoker。ParameterValidationActionInvoker和ParameterValidationAsyncActionInvoker分別繼承自ControllerActionInvoker和AsyncControllerActionInvoker。在重寫的GetParameterValue方法中,我們在調用基類的同名方法之前將作為參數的ParameterDescriptor對象保存到當前Controller上下文中,具體來說是放到了表示當前路由數據的RouteDataDictionary對象的DataTokens集合中。在方法調用之后我們將它從Controller上下文中移除。

   1: public class ParameterValidationActionInvoker : ControllerActionInvoker
   2: {
   3:     protected override object GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
   4:     {
   5:         try
   6:         {
   7:             controllerContext.RouteData.DataTokens.Add("ParameterDescriptor",parameterDescriptor);
   8:             return base.GetParameterValue(controllerContext, parameterDescriptor);
   9:         }
  10:         finally
  11:         {
  12:             controllerContext.RouteData.DataTokens.Remove("ParameterDescriptor");
  13:         }
  14:     }
  15: }
  16:  
  17: public class ParameterValidationAsyncActionInvoker : AsyncControllerActionInvoker
  18: {
  19:     protected override object GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
  20:     {
  21:         try
  22:         {
  23:             controllerContext.RouteData.DataTokens.Add("ParameterDescriptor", parameterDescriptor);
  24:             return base.GetParameterValue(controllerContext, parameterDescriptor);
  25:         }
  26:         finally
  27:         {
  28:             controllerContext.RouteData.DataTokens.Remove("ParameterDescriptor");
  29:         }
  30:     }
  31: }

四、自定義ModelValidatorProvider

ParameterValidationActionInvoker/ParameterValidationAsyncActionInvoker存放到當前Controller上下文中的ParameterDescriptor被我們自定義的ModelValidatorProvider提取出來用於創建相應的ModelValidator。如下面的代碼片斷所示,我們自定義的ParameterValidationModelValidatorProvider直接繼承自DataAnnotationsModelValidatorProvider,在重寫的GetValidators方法中我們將ParameterDescriptor從Controller上下文中提取出來,然后得到應用在參數上的所有的特性並與當前的特性列表進行合並,最后將合並的特性列表作為參數調用積累的GetValidators方法。

   1: public class ParameterValidationModelValidatorProvider : DataAnnotationsModelValidatorProvider
   2: {
   3:     protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
   4:     {    
   5:         object descriptor;
   6:         if (metadata.ContainerType == null && context.RouteData.DataTokens.TryGetValue("ParameterDescriptor", out descriptor))
   7:         {
   8:             ParameterDescriptor parameterDescriptor = (ParameterDescriptor)descriptor;
   9:             DisplayAttribute displayAttribute = parameterDescriptor.GetCustomAttributes(true).OfType<DisplayAttribute>().FirstOrDefault()
  10:                 ?? new DisplayAttribute { Name = parameterDescriptor.ParameterName };
  11:             metadata.DisplayName = displayAttribute.Name;
  12:             var addedAttributes = parameterDescriptor.GetCustomAttributes(true).OfType<Attribute>();
  13:             return base.GetValidators(metadata, context, attributes.Union(addedAttributes));
  14:         }
  15:         else
  16:         {
  17:             return base.GetValidators(metadata, context, attributes);
  18:         }
  19:     }
  20: }

值得一提的是,應用在參數上的特性是針對最外層的容器類型,而不是針對容器類型的屬性的。比如所以我們在類型為Contact的參數上應用一個驗證特性,該特性應該與應用在Contact類型上的特性具有相同的效果,但是與Address屬性無關。所以ParameterDescriptor的提取以及特性的合並僅僅在當前Model元數據的ContainerType為Null的情況下才會進行。除此之外,我們還利用應用到參數的DisplayAttribute特性對Model元數據的DisplayName屬性進行了相應的設置。

五、自定義ModelBinder

在默認的情況下,只有在針對復雜類型的Model綁定過程中才會進行Model驗證。雖然我們通過ParameterValidationModelValidatorProvider能夠根據應用在Action方法參數上的驗證特性生成相應的ModelValidator,但是如果驗證特性是應用在一個簡單類型的參數上,生成出來的ModelValidator也是不會被使用的。為了使Model驗證發生在針對簡單類型的Model綁定過程中,我們不得不創建一個自定義的ModelBinder。為此我們定義了一個具有如下定義的ParameterValidationModelBinder,它直接繼承自DefaultModelBinder,而針對簡單類型的Model驗證定義在重寫的BindModel方法中。

   1: public class ParameterValidationModelBinder : DefaultModelBinder
   2: {
   3:     public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
   4:     {
   5:         object model = bindingContext.ModelMetadata.Model = base.BindModel(controllerContext, bindingContext);
   6:         ModelMetadata metadata = bindingContext.ModelMetadata;
   7:         if (metadata.IsComplexType || null == model)
   8:         {
   9:             return model;
  10:         }
  11:  
  12:         Dictionary<string, bool> dictionary = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
  13:         foreach (ModelValidationResult result in ModelValidator.GetModelValidator(metadata, controllerContext).Validate(null))
  14:         {
  15:             string key = bindingContext.ModelName;
  16:             if (!dictionary.ContainsKey(key))
  17:             {
  18:                 dictionary[key] = bindingContext.ModelState.IsValidField(key);
  19:             }
  20:             if (dictionary[key])
  21:             {
  22:                 bindingContext.ModelState.AddModelError(key, result.Message);
  23:             }
  24:         }
  25:         return model;
  26:     }
  27: }

到此為止,為了能夠將驗證特性應用於Action方法的參數,我們創建了自定義的ActionInvoker、ModelValidatorProvider和ModelBinder。為了驗證它們是否能夠最終實現我們期望的驗證效果,我們將它們應用到一個簡單的ASP.NET MVC應用中。

六、實例演示

在通過Visual Studio的ASP.NET MVC項目模板創建的空的Web應用中,我們創建了一個具有如下定義的HomeController。我們重寫了CreateActionInvoker方法,如果調用基類同名方法返回一個ControllerActionInvoker對象,那么我們返回一個ParameterValidationActionInvoker對象,否則返回一個ParameterValidationAsyncActionInvoker對象,這是與默認的同步/異步Action執行方式保持一致。

   1: public class HomeController : Controller
   2: {
   3:     protected override IActionInvoker CreateActionInvoker()
   4:     {
   5:         IActionInvoker actionInvoker = base.CreateActionInvoker();
   6:         if (actionInvoker is ControllerActionInvoker)
   7:         {
   8:             return new ParameterValidationActionInvoker();
   9:         }
  10:         else
  11:         {
  12:             return new ParameterValidationAsyncActionInvoker();
  13:         }
  14:     }
  15:  
  16:     public ActionResult Add(
  17:         [Range(10, 20, ErrorMessage="{0}必須在{1}和{2}之間!")]
  18:         [ModelBinder(typeof(ParameterValidationModelBinder))]
  19:         [Display(Name = "第一個操作數")]
  20:         double x,
  21:  
  22:         [Range(20, 30,ErrorMessage="{0}必須在{1}和{2}之間!")]
  23:         [ModelBinder(typeof(ParameterValidationModelBinder))]
  24:         [Display(Name = "第二個操作數")]
  25:         double y)
  26:     {
  27:         return View(x + y);
  28:     }
  29: }

Action方法Add表示一個用於進行加法運算的操作,表示操作數的兩個參數x和y分別應用了一個RangeAttribute特性將允許值得范圍設置為10到20和20到30,並設置了相應的錯誤消息。此外,兩個參數還通過應用ModelBinderAttribute特性使我們自定義的ParameterValidationModelBinder參與到這兩個參數Model綁定中。DisplayAttribute特性也應用到這兩個參數上對顯示名稱進行了相應的設置。作於執行加法運算后的結果通過默認的View呈現出來。下面的代碼片斷表示Action方法Add對應的View的定義,這是一個Model類型為double的強類型View。我們通過一個ValidationSummary來呈現驗證的錯誤消息,只有在驗證成功的情況下我們才真正顯示運算的結果。

   1: @model double
   2: @Html.ValidationSummary()
   3: @{
   4:    if(ViewData.ModelState.IsValid)
   5:    {
   6:         @:運算結果:@Model
   7:    }
   8: }

然后我們在Global.asax中對自定義的ParameterValidationModelValidatorProvider進行注冊。如下面的代碼片斷所示,在注冊ParameterValidationModelValidatorProvider之前需要將現有的DataAnnotationsModelValidatorProvider移除。

   1: public class MvcApplication : System.Web.HttpApplication
   2: {
   3:     //其他成員
   4:     protected void Application_Start()
   5:     {
   6:         //其他操作
   7:         DataAnnotationsModelValidatorProvider validatorProvider = ModelValidatorProviders.Providers
   8:            .OfType<DataAnnotationsModelValidatorProvider>().FirstOrDefault();
   9:         if (null != validatorProvider)
  10:         {
  11:             ModelValidatorProviders.Providers.Remove(validatorProvider);
  12:         }
  13:         ModelValidatorProviders.Providers.Add(new ParameterValidationModelValidatorProvider());
  14:     }
  15: }

我們運行該程序通過在瀏覽器中輸入相應的地址來訪問定義在HomeController中的Add操作,並以查詢字符串的形式指定該Action方法的兩個操作數(x=9,y=31)。由於提供的參數不服務應用在參數上的 RangeAttribute所定義的驗證規則,如下圖所示的錯誤消息會自動呈現出來。

image

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