通过扩展改善ASP.NET MVC的验证机制[使用篇]

ASP.NET MVC提供一种基于元数据的验证方式是我们可以将相应的验证特性应用到作为Model实体的类型或者属性/字段上,但是这依然具有很多的不足。在这篇文章中,我结合EntLib的VAB(Validation Application Block)的一些思想通过扩展为ASP.NET MVC提供一种更为完善的验证机制。[源代码从这里下载]

目录:
一、扩展旨在解决怎样的验证问题
二、一个简单的消息维护组件
三、多语言的支持
四、基于某个验证规则的验证
五、验证规则的一致性

一、扩展旨在解决怎样的验证问题

这个基于验证的扩展可以实现如下几个ASP.NET MVC无法实现验证问题:

  • 消息提供机制的分离:目前我们可以通过“硬编码”和“资源文件”两种验证错误消息的提供机制,但是如果能够提供一种独立的机制来提供验证的错误消息无疑是一种更好的选择。原因很简单,验证消息是呈现给最终的用户的,应该是可以单独进行维护的,当我们发现某个验证消息不够友好,应该以一种对现有应用毫无影响的方式进行修改。此外,消息的定义最好是基于“模板”,模板中定义相应的占位符,这样可以省去很多冗余消息的定义。比如对于某个区间的验证消息就可以定义成“{0}必须在{1}与{2}之间”;
  • 多语言的支持:和ASP.NET MVC基于资源文件(所有的ValidationAttribute可以通过指定属性Name和ResourceType使我们可以在资源文件中定义相应的消息)不同,消息模板对多语言的支持可以通过独立的消息维护组件/框架来解决,但是我们需要解决用于替换占位符的参数的多语言支持;
  • 多验证规则的支持:对于同一个实体对象,在不同的场景中具有不同的验证规则。比如说我们做一个招聘网站,针对不同工作岗位对应聘者的性别、年龄、学历、身高和体重等属性的要求都是不一样的,所以我们应该针对基于工作岗位的验证场景定义不同的验证规则,并针对某个具体的验证规则对实体对象实施验证。

二、一个简单的消息维护组件

为了演示消息提供机制的分离,我们定义了一个简单的消息维护组件MessageManager。如下面的代码所示,抽象类MessageManager具有唯一的FormatMessage方法用于获取一个经过格式化好的最终消息文本,参数category、id和args分别代表对应消息条目的类型、ID和作为替换占位符的参数。

   1: public abstract  class MessageManager
   2: {
   3:     public abstract string FormatMessage(string category, string id, params object[] args);
   4: }

我们定义了如下一个默认的DefaultMessageManager,它维护了一组代表消息条目的MessageEntry列表,而MessageEntry是支持多语言的。在重写的FormatMessage方法中,直接通过类型和ID在列表中找到相应的MessageEntry,并传输占位符参数根据当前线程的CurrentUICulture对消息文本进行格式。从如下的代码可以看出,我们仅仅定义了一个表示“必需字段”的消息,在en-US和zh-CN这两种语言文化下的文本分别是“{0} is mandatory!”和“请输入{0}!”。该MessageEntry得类型和ID分别是“Validation”和“MandatoryField”。

   1: public class DefaultMessageManager : MessageManager
   2: {
   3:     public DefaultMessageManager()
   4:     {
   5:         var messages = new List<MessageEntry>();
   6:         var messageEntry = new MessageEntry("Validation", "MandatoryField");
   7:         messageEntry.AddMessageText("{0} is mandatory!", new CultureInfo("en-US"));
   8:         messageEntry.AddMessageText("请输入{0}!", new CultureInfo("zh-CN"));
   9:         messages.Add(messageEntry);
  10:         this.Messages = messages;
  11:     }
  12:  
  13:     public IEnumerable<MessageEntry> Messages { get; private set; } 
  14:     public override string FormatMessage(string category, string id, params object[] args)
  15:     {
  16:         MessageEntry messageEntry = (from message in this.Messages
  17:                                      where message.Category == category && message.Id == id
  18:                                      select message).FirstOrDefault();
  19:         if (null == messageEntry)
  20:         {
  21:             throw new Exception("...");
  22:         }
  23:  
  24:         return messageEntry.Format(args);
  25:     }
  26: }

我们并没有列出MessageEntry的定义,有兴趣的朋友可以下载本例的源代码。最终我们定义了如下静态工厂MessageManagerFactory来创建相应的MessageManager,简单起见,我们直接创建上述的DefaultMessageManager。

   1: public static class MessageManagerFactory
   2: {
   3:     public static MessageManager GetMessageManager()
   4:     {
   5:         return new DefaultMessageManager();
   6:     }
   7: }

三、多语言的支持

在本篇文章中我们不谈具体实现,只谈具体的使用方法。我们以登录场景为例,如下所示的LoginInfo类型表示包含代表用户名和密码的Model类型。应用在属性上的RequiredValidatorAttribute特性是我们自定义的ValidationAttribute,它实现了RequiredAttribute一样的验证功能。以应用在UserName属性上的RequiredValidatorAttribute为例([RequiredValidator("Validation", "MandatoryField", "用户名", Name = "RequiredValidator", Culture = "zh-CN")]),构造函数参数分别代表通过MessageManager维护的对应消息条目的类型(Validation)、ID(MandatoryField)以及占位符参数(用户名)。Culture属性则代表对应的语言文化,如果没有对该属性进行显式指定,则代表“语言文化中性”的验证器。

   1: public class LoginInfo
   2: {
   3:     [Display(ResourceType = typeof(Resources), Name = "UserName")]
   4:     [RequiredValidator("Validation", "MandatoryField", "User Name")]
   5:     [RequiredValidator("Validation", "MandatoryField", "用户名", Culture = "zh-CN")]
   6:     public string UserName { get; set; }
   7:  
   8:     [RequiredValidator("Validation", "MandatoryField", "Password")]
   9:     [RequiredValidator("Validation", "MandatoryField", "密码", Culture = "zh-CN")]
  10:     [DataType(DataType.Password)]
  11:     [Display(ResourceType = typeof(Resources), Name = "Password")]
  12:     public string Password { get; set; }
  13: }

在进行验证器的选择的过程中,总是会根据当前线程的CurrentUICulture选择相匹配的验证器。如果找不到完全匹配的验证器,则会选择语言文化中性验证器(这样的验证器只允许有一个)。对于本例来说,如果当前的语言文化为zh-CN,那么只有应用在UserName和Password属性上Culture属性为zh-CN的RequiredValidatorAttribute有效,而在其他的语言文化环境中则会选择没有对Culture属性进行显式设置的RequiredValidatorAttribute。我们来看看用于进行用户登录的AccountController的定义:

   1: public class AccountController : BaseController
   2:     {
   3:         public ActionResult SignIn()
   4:         {
   5:             return View(new LoginInfo());
   6:         }
   7:         [HttpPost]
   8:         public ActionResult SignIn(LoginInfo logInfo)
   9:         {
  10:             if (ModelState.IsValid)
  11:             {
  12:                 return this.View();
  13:             }
  14:             else
  15:             {
  16:                 return this.View();
  17:             }
  18:         }
  19:     }

下面是SignIn操作默认的View的所有内容:

   1: @using Artech.Mvc.Validation.Properties
   2: @using Artech.Mvc.Validation.Models
   3: @model LoginInfo
   4:  
   5: @{
   6:     ViewBag.Title = "SignIn";
   7: }
   8: @Html.ValidationSummary()
   9: @using(Html.BeginForm())
  10: {
  11:     @Html.EditorForModel()
  12:     <input type="submit" value="@Resources.SignIn"/>
  13: }

在我们的例子中语言的设置是通过URL来体现的,为了我们在Global.asax中进行了如下的路由映射,即controller之前的部分代表语言文化代码,默认为zh-CN。

   1: public class MvcApplication : System.Web.HttpApplication
   2: {
   3:     public static void RegisterGlobalFilters(GlobalFilterCollection filters)
   4:     {
   5:         filters.Add(new HandleErrorAttribute());
   6:     }
   7:  
   8:     public static void RegisterRoutes(RouteCollection routes)
   9:     {
  10:          //...
  11:          routes.MapRoute(
  12:             "Default", // Route name
  13:             "{culture}/{controller}/{action}/{id}", // URL with parameters
  14:             new {culture="zh-CN", controller = "Account", action = "SignIn", id = UrlParameter.Optional } // Parameter defaults
  15:         );
  16:  
  17:     }
  18:  
  19:     protected void Application_Start()
  20:     {   
  21:         //...
  22:         RegisterRoutes(RouteTable.Routes);
  23:     }
  24: }

运行我们的程序并在分别以en-US和zh-CN访问主页,在没有输入用户名和密码的情况下将会得到如下的验证消息。

image

四、基于某个验证规则的验证

现在我们来演示基于某个验证规则的验证方式。对于登录,我们都应该有这样的体会,在开发阶段为了测试的时候避免频繁地输入用户名和密码,我们会设置一个默认的密码。在这里我们可以通过定义验证规则来屏蔽对密码的验证。为此我们我们对应用在LoginInfo的Password属性上的RequiredValidatorAttribute特性稍加改动,对其RuleName属性进行了显式设置(RuleName = "Production"),意味着只有当前验证规则为“Production”(产品阶段)的时候,基于它们的验证才会生效。

   1: public class LoginInfo
   2: {
   3:     //...
   4:     [RequiredValidator("Validation", "MandatoryField", "Password",RuleName = "Production")]
   5:     [RequiredValidator("Validation", "MandatoryField", "密码", Culture = "zh-CN", RuleName = "Production")]
   6:     [DataType(DataType.Password)]
   7:     [Display(ResourceType = typeof(Resources), Name = "Password")]
   8:     public string Password { get; set; }
   9: }

而对当前采用怎样地验证规则,则可以在Controller或者Action方法上应用我们自定义的RuleNameAttribute来设定。如下面的代码片断所示,我们在AccountController上直接应用了RuleNameAttribute特性并将当前的验证规则设置为“Dev”(开发阶段)。

   1: [ValidationRule("Dev")]
   2: public class AccountController : BaseController
   3: {
   4:     //...
   5: }

那么在程序运行的时候就不会对密码进行任何验证,这可以通过如下的截图可以看出来:

image

如果我们通过应用在AccountController上的RuleNameAttribute将验证规则设置为“Production”

   1: [ValidationRule("Production")]
   2: public class AccountController : BaseController
   3: {
   4:     //...
   5: }

那么针对密码的验证就会生效了:

image

五、验证规则的一致性

值得一提的是:我们扩展的验证体系依然也为客户端认证提供支持,但是在进行基于验证规则的验证是确有一个小小的机关。同样以AccountController的两个SignIn操作为例,进行客户端验证的规则是基于第一个SignIn操作(HttpGet)生成的,服务端验证则是基于第二个SignIn操作(HttpPost)的验证规则进行的,如果我们将RuleNameAttribute应用到两个SignIn操作上,比如确保它们的规则名称一致方能保证客户端验证和服务端认证的一致性。

   1: public class AccountController : BaseController
   2: {
   3:     [ValidationRule("Production")]
   4:     public ActionResult SignIn()
   5:     {
   6:         //...
   7:     }
   8:     [HttpPost]
   9:     [ValidationRule("Production")]
  10:     public ActionResult SignIn(LoginInfo logInfo)
  11:     {
  12:          //...
  13:     }
  14: }
posted @ 2012-01-12 13:42 Artech 阅读(1721) 评论(23) 编辑 收藏

 回复 引用 查看   
#1楼[楼主] 2012-01-12 13:44 Artech      
 回复 引用 查看   
#2楼 2012-01-12 13:46 开国伟人      
BS。每次都这么搞。
 回复 引用 查看   
#3楼[楼主] 2012-01-12 13:48 Artech      
引用开国伟人:BS。每次都这么搞。

嘿嘿:)
什么意思?

 回复 引用 查看   
#4楼 2012-01-12 13:48 付之一笑      
顶,友情支持!参考思路。
 回复 引用 查看   
#5楼[楼主] 2012-01-12 13:55 Artech      
引用付之一笑:顶,友情支持!参考思路。

基于某个规则的验证来源于EntLib的Validation AppBlock;
而实现错误消息提供机制的独立性是出于我们自身开发的需要(我们需要与自己的消息管理组件集成)。

 回复 引用 查看   
#6楼 2012-01-12 14:04 xuexplorer      
顶,顺便问一下:自定义验证失败提示怎么做?比如验证识别用一个图片进行提示
 回复 引用 查看   
#7楼[楼主] 2012-01-12 14:08 Artech      
引用xuexplorer:顶,顺便问一下:自定义验证失败提示怎么做?比如验证识别用一个图片进行提示

关于自定义验证失败,我习惯做法还是添加ModelError。

 回复 引用 查看   
#8楼 2012-01-12 14:13 幸运草      
老A的文章,要顶一下。
 回复 引用 查看   
#9楼[楼主] 2012-01-12 14:14 Artech      
引用幸运草:老A的文章,要顶一下。

3X:)

 回复 引用 查看   
#10楼 2012-01-12 14:38 幸运草      
给老A 推荐一个验证的工具http://fluentvalidation.codeplex.com/ 非侵入式的。比较好用
 回复 引用 查看   
#11楼 2012-01-12 14:38 roloxa      
学习 学习
 回复 引用 查看   
#12楼[楼主] 2012-01-12 14:43 Artech      
引用幸运草:给老A 推荐一个验证的工具http://fluentvalidation.codeplex.com/ 非侵入式的。比较好用

看起来不错哟,看看能不能直接借来用用,谢谢!!

 回复 引用 查看   
#13楼 2012-01-12 14:48 幸运草      
我不喜欢造轮子,但喜欢“借”能用就直接用了。
 回复 引用 查看   
#14楼[楼主] 2012-01-12 15:35 Artech      
引用幸运草:我不喜欢造轮子,但喜欢“借”能用就直接用了。

我喜欢对借来的轮子进行加工:)

 回复 引用 查看   
#15楼 2012-01-12 15:42 寂灭我有      
不错!
 回复 引用 查看   
#16楼 2012-01-12 17:29 keepsilence      
@Artech
如果把你的代码修改一下
@using(Html.BeginForm())
{
@Html.EditorFor(Model => Model.UserName)
@Html.EditorForModel();
<input type="submit" value="@Resources.SignIn"/>
}
可以看到
@Html.EditorFor(Model => Model.UserName)这样写并没有提示文字
请问是为什么了
thx

 回复 引用 查看   
#17楼[楼主] 2012-01-12 17:43 Artech      
引用keepsilence:
@Artech
如果把你的代码修改一下
@using(Html.BeginForm())
{
@Html.EditorFor(Model => Model.UserName)
@Html.EditorForModel();
<input type="submit" value="@Resources.SignIn"/>
}
可以看到
@Html.EditorFor(Model => Model.UserName)这样写并没有提示文字
请问是为什么了
thx

加上@Html.ValidationMessageFor(m => m.UserName)试试看!

 回复 引用 查看   
#18楼 2012-01-12 21:48 南京.王清培      
蒋大哥的文章一定要顶,书啥时候出版哦。我都急死了
 回复 引用 查看   
#19楼 2012-01-12 22:16 深海沉      
LZ的基于某个验证规则的验证不错,对于多语言,我还是建议将验证信息放在资源文件中。

幸运草的那个框架好像在enlib里也有类似的吧,或者就是在其它地方我见过。

 回复 引用 查看   
#20楼[楼主] 2012-01-13 09:20 Artech      
引用南京.王清培:蒋大哥的文章一定要顶,书啥时候出版哦。我都急死了

我的任务基本上已经完成,余下的工作大都是出版社的了,乐观估计在3月份吧。

 回复 引用 查看   
#21楼[楼主] 2012-01-13 09:31 Artech      
引用深海沉:
LZ的基于某个验证规则的验证不错,对于多语言,我还是建议将验证信息放在资源文件中。

幸运草的那个框架好像在enlib里也有类似的吧,或者就是在其它地方我见过。


对于本例对多语言的支持,可以说有好有坏,好的是:
1、将消息的提供机制抽象出来,使我们可以具有不同的存储方式,比如XML、Database和Resource文件;
2、由于MessageEntry定义的仅仅是一个带有{0}{1}这样站位符的模板,可以减少消息定义量。比如对于Required字段,如果我们不采用默认的消息,在Resource文件中就需要为所有字段定义一个Resource Item
但是不好的地方也来源于此:
我们需要对用于替换站位符指定的参数进行本地化,就需要额外添加一个ValidationAttribute并指定其Culture属性

不过没有关系,这里的Validator依然是ValidationAttribute的子类,ErrorMessage和ResourceType/ResourceName也都支持。

EntLib中基于验证就是VAB(Validation AppBlock),它与基于DataAnnotation有点类似,也支持将相应Validator Attribute应用在实体类或者其属性/字段上,如果采用与AOP集成,我们还可以将Validator Attribute应用到方法参数上。此外,它还支持配置方式的验证规则的定义。VAB是不错的验证框架,与幸运草的那个框架还是不同的。

 回复 引用 查看   
#22楼 2012-01-13 18:11 黑曜石      
看了你几篇关于MVC的验证文章
有个问题弱弱的问下
DataAnnotation这个东西加上自写扩展就可以验证,为什么还要自己写这么多~
我现在使用Entity Framework,然后利用partial class的特点,写对应的验证Model,后台用使用extjs全ajax化,所以验证方面自定义一个Attribute,有验证错误就直接返回JSON。
当然这种没有多语言支持这么牛比的功能,不过我也不需要

 回复 引用 查看   
#23楼[楼主] 2012-01-14 10:09 Artech      
引用黑曜石:
看了你几篇关于MVC的验证文章
有个问题弱弱的问下
DataAnnotation这个东西加上自写扩展就可以验证,为什么还要自己写这么多~
我现在使用Entity Framework,然后利用partial class的特点,写对应的验证Model,后台用使用extjs全ajax化,所以验证方面自定义一个Attribute,有验证错误就直接返回JSON。
当然这种没有多语言支持这么牛比的功能,不过我也不需要

我的验证用于非Ajax请求的Form Post场景。

发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 2320508 arbSRs9nLcs=