原文: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所定义的验证规则,如下图所示的错误消息会自动呈现出来。

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,多种验证规则

ASP.NET MVC基于标注特性的Model验证:将ValidationAttribute应用到参数上的更多相关文章

  1. ASP.NET MVC基于标注特性的Model验证:一个Model,多种验证规则

    原文:ASP.NET MVC基于标注特性的Model验证:一个Model,多种验证规则 对于Model验证,理想的设计应该是场景驱动的,而不是Model(类型)驱动的,也就是对于同一个Model对象, ...

  2. [ASP.NET MVC 小牛之路]16 - Model 验证

    上一篇博文 [ASP.NET MVC 小牛之路]15 - Model Binding 中讲了MVC在Model Binding过程中如何根据用户提交HTTP请求数据创建Model对象.在实际的项目中, ...

  3. 【ASP.NET MVC 学习笔记】- 17 Model验证

    本文参考:http://www.cnblogs.com/willick/p/3434483.html 1.Model验证用于在实际项目中对用户提交的表单的信息进行验证,MVC对其提供了很好的支持. 2 ...

  4. ASP.NET MVC 基于角色的权限控制系统的示例教程

    上一次在 .NET MVC 用户权限管理示例教程中讲解了ASP.NET MVC 通过AuthorizeAttribute类的OnAuthorization方法讲解了粗粒度控制权限的方法,接下来讲解基于 ...

  5. 微软下一代站点开发框架:ASP.NET MVC 6 新特性揭秘

     国内第一个<微软下一代站点开发框架:ASP.NET MVC 6 新特性揭秘 >课程 微软特邀讲师 徐雷!周六晚8点YY预定:id=28447" href="htt ...

  6. ASP.NET MVC 5 学习教程:添加验证

    原文 ASP.NET MVC 5 学习教程:添加验证 起飞网 ASP.NET MVC 5 学习教程目录: 添加控制器 添加视图 修改视图和布局页 控制器传递数据给视图 添加模型 创建连接字符串 通过控 ...

  7. 【译】ASP.NET MVC 5 教程 - 10:添加验证

    原文:[译]ASP.NET MVC 5 教程 - 10:添加验证 在本节中,我们将为Movie模型添加验证逻辑,并确认验证规则在用户试图使用程序创建和编辑电影时有效. DRY 原则 ASP.NET M ...

  8. [ASP.NET MVC]如何定制Numeric属性/字段验证消息

    原文:[ASP.NET MVC]如何定制Numeric属性/字段验证消息 对于一个Numeric属性/字段,ASP.NET MVC会自动进行数据类型的验证(客户端验证),以确保输入的是一个有效的数字, ...

  9. [转][ASP.NET MVC]如何定制Numeric属性/字段验证消息

    本文转自:http://www.cnblogs.com/artech/archive/2012/02/13/NumericPropertyValidation.html 对于一个Numeric属性/字 ...

随机推荐

  1. HDU 2008 数字统计

    号码值统计 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others) Total Subm ...

  2. HDU 1710-Binary Tree Traversals(二进制重建)

    Binary Tree Traversals Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/O ...

  3. UVA 239 - Tempus et mobilius. Time and motion(更换周期)

    UVA 239 - Tempus et mobilius. Time and motion 题目链接 题意:这题题意也是吊得飞起,看了老半天,大概是这样: 有一个放球的队列.和3个轨道(说白了就是栈) ...

  4. head first c&lt;11&gt;在根据网络编程

    博文可以在一个大的网络通信实现,但是,一个人只能起到,我们能够给每个clientfork()子进程,实现诸多的服务. 方法: client连到server以后,server启动一个新创建的套接字对话. ...

  5. BizTalk开发小技巧

    BizTalk开发小技巧 随笔分类 - Biztalk Biztalk 使用BizTalk实现RosettaNet B2B So Easy 摘要: 使用BizTalk实现RosettaNet B2B ...

  6. Unity3D合并着色器

    unity 3d倒每次模型更多的是一种着色器.我可以拥有这些车型共享的地图想分享一个着色器.所以每次删除,然后附加,很麻烦.如何才能合并这些着色器? 采纳TexturePacking对 1.遍历gam ...

  7. 在Mac OS上配置Android开发环境

    1)安装配置NDK 1.1 下载NDK并解压缩 下载路径 https://developer.android.com/tools/sdk/ndk/index.html 在terminal运行: chm ...

  8. Codeforces Round #254 (Div. 2):A. DZY Loves Chessboard

    A. DZY Loves Chessboard time limit per test 1 second memory limit per test 256 megabytes input stand ...

  9. JavaEE(11) - 消息驱动EJB

    1. MDB作为异步消费者的本质 2. MDB的运行机制 3. 使用@MessageDriven修饰MDB(需要messageListenerInterface) 4. 实现MessageListen ...

  10. 设计模式--模板方法 And State模式

    1.模板方法   钩子: 在抽象基类已经有默认的定义,子类选择是否覆盖它 在模板方法模式中,  抽象基类中使用 钩子函数(子类视情况是否覆盖)  来达到控制模板方法中  流程控制的 目的 设计原则: ...