【IT168 技术文档】
一、 简介
在上一篇文章中,我描述了如何在一个MVC应用程序中借助于定制校验属性来从服务器端对表单域执行校验。通过使用属性(例如RequiredValidator和RegularExpressionValidator属性)来修饰你的模型,你可以强制实施指定的校验规则并显示定制的校验错误消息。
为了执行更复杂的校验,我曾推荐使用一个CustomValidator校验器,你可以把它应用于实体类的校验。这个CustomValidator类是通过指向另一个类来最终执行定制的校验规则的。
但是,如果进行更细致的分析,你会发现上述这个方法在应用于大多数场所下实现定制校验时还是比较复杂的。因此,大多数开发人员可能更喜欢使用属性的方法进行校验,但是,是否还存在更容易的方法进行定制校验呢?
在本篇中,我将采用与上面稍微不同的方法来处理表单校验的问题。具体地说,我将把Scott Guthrie所描述的命令式方法和上面的属性方法结合到一起。换句话说,我将混合使用命令式和声明性方法来完成表单域的校验。我称这个方法为实现表单校验的混合方法。
二、 使用命令式方法进行表单校验
让我们首先开始讨论Scott Guthrie所描述的方法。当使用这个方法进行表单校验时,你必须创建下列类:
? IRuleEntity—这是为了进行校验每一个实体都必须实现的一个接口。
? RuleViolation—这是一个描述校验错误的类。
? RuleViolationException—这是当一个实体校验失败时引发的一个异常类。
? Validation—这是类中包括了一个方法,用于把校验规则复制到ModelState中。
首先,上面这个IRuleEntity接口是相当简单。它仅包含了两个方法,名字为Validate()和GetRuleViolations()。列表1中提供了这个接口的实现代码。
列表1–IRuleEntity.cs
namespace MvcValidation
{
public interface IRuleEntity
{
List<RuleViolation> GetRuleViolations();
void Validate();
}
}
该RuleViolation类描述了一个特定的校验错误。在此,使用了一个RuleViolations的集合来描述所有的校验错误(当提交一个表单时可能发生)。列表1中提供了该RuleViolation类的实现代码。
列表2–RuleViolation.cs
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace MvcValidation
{
public class RuleViolation
{
public string PropertyName { get; private set; }
public object PropertyValue { get; private set; }
public string ErrorMessage { get; private set; }
public RuleViolation(string propertyName, object propertyValue, string errorMessage)
{
this.PropertyName = propertyName;
this.PropertyValue = propertyValue;
this.ErrorMessage = errorMessage;
}
}
}
即使存在一条规则被破坏的情况,也会引发一个RuleViolationException异常。列表3中提供了这个特别的异常类的实现代码。
列表3–RuleViolationException.cs
using System.Collections.Generic;
namespace MvcValidation
{
public class RuleViolationException : Exception
{
public RuleViolationException(string message, List<RuleViolation> validationIssues)
: base(message)
{
this.ValidationIssues = validationIssues;
}
public List<RuleViolation> ValidationIssues { get; private set; }
}
}
最后请注意,该Validation类是一个工具类,它暴露了一个名字为UpdateModelStateWithViolations()的方法。这个方法负责把所有的校验规则复制到一个控制器类的ModelState数据中。列表3中提供了这个校验类的实现代码。
列表4–Validation.cs
namespace MvcValidation
{
public class Validation
{
public static void UpdateModelStateWithViolations(RuleViolationException ruleViolationException, ModelStateDictionary modelState)
{
foreach (var issue in ruleViolationException.ValidationIssues)
{
var value = issue.PropertyValue ?? string.Empty;
modelState.AddModelError(issue.PropertyName, value.ToString(), issue.ErrorMessage);
}
}
}
}
让我们来看一下上面所有这些类是如何协同工作的。首先注意到,列表5中的Message类描述了一个论坛消息。注意,这个类通过实现Validate()和GetRuleViolations()方法实现了IRuleEntity接口。
列表5–Message.cs
using MvcValidation;
using System.Collections.Generic;
using System.Data.Linq;
using MvcValidation.Attributes;
using System.Web.Mvc;
namespace MvcForums.Models.Entities
{
public class Message :IRuleEntity
{
private DateTime _entryDate = DateTime.Now;
public Message()
{ }
public Message(int id, int? parentThreadId, int? parentMessageId, string author, string subject, string body)
{
this.Id = id;
this.ParentThreadId = parentThreadId;
this.ParentMessageId = parentMessageId;
this.Author = author;
this.Subject = subject;
this.Body = body;
}
public int Id { get; set; }
public int? ParentThreadId { get; set; }
public int? ParentMessageId { get; set; }
public string Author { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
public DateTime EntryDate
{
get { return _entryDate; }
set { _entryDate = value; }
}
#region IRuleEntity Members
public List<RuleViolation> GetRuleViolations()
{
var validationIssues = new List<RuleViolation>();
// Validate Subject
if (String.IsNullOrEmpty(this.Subject))
validationIssues.Add(new RuleViolation("subject", this.Subject, "You must enter a message subject."));
// Validate Body
if (String.IsNullOrEmpty(this.Body))
validationIssues.Add(new RuleViolation("body", this.Body, "You must enter a message body."));
return validationIssues;
}
public void Validate()
{
var validationIssues = GetRuleViolations();
if (validationIssues.Count > 0)
throw new RuleViolationException("Validation Issues", validationIssues);
}
#endregion
}
}
请注意到,这个Validate()方法调用了另一个GetRuleViolations()方法。如果存在任何校验错误,那么该Validate()方法即抛出一个RuleViolationException型异常。
在论坛应用程序中,该Validate()方法是在ForumRepository中被调用的。在把一个新消息添加到数据库中之前,在Message类上调用这个Validate()方法,如下所示:
{
messageToAdd.Validate();
_dataContext.Insert(messageToAdd);
return messageToAdd;
}
如果存在一个校验错误,那么将抛出一个RuleViolationException型异常,并且永不会把相应的消息插入到数据库中。
该ForumRepository应用于论坛控制器内。当你提交一个表单用以创建一个新的论坛消息时,ForumController.Create(FormCollection collecction)方法即被调用。列表6中提供了论坛控制器完整的实现代码。
列表6–ForumController.cs
using System.Web.Mvc;
using MvcForums.Models;
using Microsoft.Web.Mvc;
using MvcForums.Models.Entities;
using MvcValidation;
namespace MvcForums.Controllers
{
public class ForumController : Controller
{
private IForumRepository _repository;
public ForumController()
: this(new ForumRepository())
{ }
public ForumController(IForumRepository repository)
{
_repository = repository;
}
public ActionResult Index()
{
ViewData.Model = _repository.SelectThreads();
return View("Index");
}
[AcceptVerbs("GET")]
public ActionResult Create()
{
return View("Create");
}
[AcceptVerbs("Post")]
public ActionResult Create(FormCollection form)
{
var messageToCreate = new Message();
try
{
UpdateModel(messageToCreate, new[] { "Author", "ParentThreadId", "ParentMessageId", "Subject", "Body" });
_repository.AddMessage(messageToCreate);
}
catch (RuleViolationException rex)
{
Validation.UpdateModelStateWithViolations(rex, ViewData.ModelState);
return View("Create", messageToCreate);
}
catch (Exception ex)
{
ViewData.ModelState.AddModelError("message", null, "Could not save message.");
return View("Create", messageToCreate);
}
// Redirect
return RedirectToAction("Index");
}
public ActionResult Thread(int threadId)
{
ViewData.Model = _repository.SelectMessages(threadId);
return View("Thread");
}
}
}
注意,这里存在两个Create()方法。第一个Create()方法仅当发生一次HTTP GET操作时才被调用。这个行为返回的表单将用于创建一个新的消息
当把一个XHTML表单回寄到服务器时调用第二个Create()方法。当表单数据被回寄时,该Create()方法将调用控制器的UpdateModel()方法以便生成一个Product类(它把所有的表单值进行提交)。接下来,调用ForumRepository.Add()方法以便把新的Product类实例添加到数据库。
注意,上述两个语句都包含在一个Try..Catch块内。如果任意一个方法,无论是Controller.UpdateModel()还是ForumRepository.Add()方法,失败,那么,即执行这个Try…Catch语句的Catch子句。第一个Catch子句使用校验错误信息更新ViewData.ModelState属性(通过把校验错误从RuleViolationException复制到ModelState)。接下来,重新显示Create视图。
注意,还存在两个Catch语句。第二个Catch子句捕获一个泛型异常。当UpdateModel()方法遇到一个校验错误时(错误的类型值被赋给一个属性),调用这个Catch子句。当存在网络问题(例如你的数据库服务器或你的数据库服务器崩溃)时,将调用这个子句。注意,一个名字为message的错误键也同时被更新。
列表7中提供了Create视图的完整的实现代码。
列表7--Create.aspx
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Index</title>
<style type="text/css">
.input-validation-error
{
border: solid 2px red;
}
#message
{
padding: 5px;
color:red;
}
</style>
</head>
<body>
<div>
<form method="post" action="/Forum/Create">
<div id="message">
<%= Html.ValidationMessage("message") %>
</div>
<input name="author" type="hidden" value="Stephen" />
<label for="subject">Subject:</label>
<%= Html.TextBox("subject") %>
<%= Html.ValidationMessage("subject") %>
<br /><br />
<label for="body">Body:</label>
<%= Html.TextArea("body") %>
<%= Html.ValidationMessage("body") %>
<br /><br />
<input type="submit" value="Post" />
</form>
</div>
</body>
</html>
注意,上面的视图中包括了一个泛型错误消息(名字为message)。
上述这个实现校验的方法是一种命令式编程方法。该校验规则是通过GetRuleViolations()方法中命令式的编程代码实现的,如下所示:
{
var validationIssues = new List<RuleViolation>();
// Validate Subject
if (String.IsNullOrEmpty(this.Subject))
validationIssues.Add(new RuleViolation("subject", this.Subject, "You must enter a message subject."));
// Validate Body
if (String.IsNullOrEmpty(this.Body))
validationIssues.Add(new RuleViolation("body", this.Body, "You must enter a message body."));
return validationIssues;
}
使用命令式方法可以使你能够执行你所需要的任何类型的校验。例如,在把一条消息插入到数据库之前,你可以执行一个数据库查询以确保消息主题和正文在数据库中都是唯一的。
三、 使用声明性方法进行表单校验
一般的程序员都喜欢使用声明性方法实现表单校验。声明性方法能够使你校验表单数据而不用编写任何代码。
使用声明性方法的另一个优点是,声明性方法能够使你非常容易地实现客户端校验。我们的一个想法是,从我们的服务器端校验器生成客户端校验器。通过这种方式,我们可以校验一个表单而不必把表单回寄到服务器端。
列表8中修改过的Message类即使用一个声明性方法实现了有关表单域的校验。
列表8–Message.cs
using MvcValidation;
using System.Collections.Generic;
using System.Data.Linq;
using MvcValidation.Attributes;
using System.Web.Mvc;
namespace MvcForums.Models.Entities
{
public class Message :IRuleEntity
{
private DateTime _entryDate = DateTime.Now;
public Message()
{ }
public Message(int id, int? parentThreadId, int? parentMessageId, string author, string subject, string body)
{
this.Id = id;
this.ParentThreadId = parentThreadId;
this.ParentMessageId = parentMessageId;
this.Author = author;
this.Subject = subject;
this.Body = body;
}
public int Id { get; set; }
public int? ParentThreadId { get; set; }
public int? ParentMessageId { get; set; }
public string Author { get; set; }
[RequiredValidator("You must enter a message subject.")]
public string Subject { get; set; }
[RequiredValidator("You must enter a message body.")]
public string Body { get; set; }
public DateTime EntryDate
{
get { return _entryDate; }
set { _entryDate = value; }
}
#region IRuleEntity Members
public List<RuleViolation> GetRuleViolations()
{
var validationIssues = new List<RuleViolation>();
// Validate attributes
AttributeValidation.Validate(this, validationIssues);
return validationIssues;
}
public void Validate()
{
var validationIssues = GetRuleViolations();
if (validationIssues.Count > 0)
throw new RuleViolationException("Validation Issues", validationIssues);
}
#endregion
}
}
注意,在这个修改过的Message类中,Subject和Body属性都修饰以RequiredValidator属性。借助于这个RequiredValidator属性,这两个属性成为必需提供的属性。
此外还请注意,已经从该GetRuleViolations()方法中删除了原先的命令式代码。代之的是,AttributeValidation.Validate()方法被调用。这个方法针对每一个应用到Message类上的校验器属性都调用Validate()方法。如果至少一个校验器失败,那么该Message类校验即失败,并且抛出一个RuleViolationException型的异常。
很显然,你可以混合使用属性和非属性方法进行校验。对于普通的校验任务而言,你可以使用标准校验器,例如RequiredValidator,LengthValidator或RegularExpressionValidator。对于更复杂的类型校验,你可以编写命令式代码以执行校验。
四、 总结
在本篇中,我描述了一种把命令式和声明性相结合进行校验的方法。我相信,对于普通的校验任务方法最容易的方法应该是声明性方法。但是,对于更复杂的或专门的类型校验任务,你应该编写命令式校验代码。对于本系列文章中创建的论坛应用程序而言,我计划采取一种混合的方法实现最终的表单域的校验。