原文:通过扩展改善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访问主页,在没有输入用户名和密码的情况下将会得到如下的验证消息。
四、基于某个验证规则的验证
现在我们来演示基于某个验证规则的验证方式。对于登录,我们都应该有这样的体会,在开发阶段为了测试的时候避免频繁地输入用户名和密码,我们会设置一个默认的密码。在这里我们可以通过定义验证规则来屏蔽对密码的验证。为此我们我们对应用在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: }
那么在程序运行的时候就不会对密码进行任何验证,这可以通过如下的截图可以看出来:
如果我们通过应用在AccountController上的RuleNameAttribute将验证规则设置为“Production”
1: [ValidationRule("Production")]
2: public class AccountController : BaseController
3: {
4: //...
5: }
那么针对密码的验证就会生效了:
五、验证规则的一致性
值得一提的是:我们扩展的验证体系依然也为客户端认证提供支持,但是在进行基于验证规则的验证是确有一个小小的机关。同样以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: }