不仅在客户端浏览器中需要执行验证逻辑,在服务器端也需要执行。客户端验证能即时给出一个错误反馈(阻止请求发送至服务器),是时下 Web 应用程序所期望的特性。服务器端验证,主要是因为来自网络的信息都是不可信任的。
当在 ASP.NET MVC 设计模式上下文中谈论验证时,主要关注的是验证模型的值。ASP.NET MVC 验证特性可以帮助我们验证模型值,且这样验证特性是可扩展的,所以我们可以采用任意想要的方式构建验证模式,默认方法是一种声明式验证,即数据注解特性。
注解是一种通用机制,不单单局限于验证这一用途,可以用来向框架注入元数据。
为验证注解订单
购买音乐的顾客会有一个典型的购物车结算环节,需要付款和填写收货信息。Order 类包含完成结算所需要需要的完整信息:
public partial class Order
{
public int OrderId { get; set; }
public System.DateTime OrderDate { get; set; }
public string Username { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; }
public string Phone { get; set; }
public string Email { get; set; }
public decimal Total { get; set; }
public List<OrderDetail> OrderDetails { get; set; }
}
应用程序使用 HTML 辅助方法 EditorForModel 来构建结算页面,下面是视图中的代码:
<fieldset>
<legend>Shipping Information</legend>
@Html.EditorForModel()
</fieldset>
EditForModel 辅助方法为对象的每个属性构建一个它认为合适的编辑器(这类型的编程做法适用于快速开发且对页面布局和美观度要求不高的内部小项目)。这个表单存在一些明显的问题。比如 OderId 和 OderDate 编辑器,这些值并不需要用户填写,应用程序会在服务器端设置。FirstName 属性对程序员有意义,而客户会认为难道少数如一个空格?(指正确写法是 First Name)
验证注解的使用
数据注解特性定义在命名空间 System.ComponentModel.DataAnnotations 中(并不全是,下面会讲到),它们提供了服务器端的验证功能,当在模型的属性上使用这些特性时,框架也支持客户端验证。
Required:当属性为 null 或者为空时,Required 特性会引发一个验证错误。客户的姓氏和名字都是必需的,所以可以在模型上添加此属性。
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
所有内置的验证特性既传递服务器端验证逻辑也传递客户端验证逻辑,即使客户端关闭了 JavaScript,服务器端也会引发验证。
StringLength:如果试图向数据库插入一个超过最大长度的字符串,就会引发异常,这就是 StringLength 的用武之地。
[Required]
[StringLength(160)]
public string FirstName { get; set; }
[Required]
[StringLength(160)]
public string LastName { get; set; }
[Required]
[StringLength(160, MinimumLength = 3)]
public string FirstName { get; set; }
RegularExpression:一些属性要求的验证并非是简单的非空或长度范围,例如 Email 属性需要一个有效可用的 Email 地址(至少看上去是一个有效的地址)。这里我们使用正则表达式来使验证输入的 Email 地址:
[RegularExpression(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,4}")]
public string Email { get; set; }
Range:该特性用来指定数值类型的最大值和最小值。
[Range(18,70)]
public int Age { get; set; }
System.Web.Mvc 下的验证特性
ASP.NET MVC 框架还会应用程序在命名空间 System.Web.Mvc 中额外添加了两个验证特性。
Remote:可以利用服务器端的回调函数执行客户端的验证逻辑。例如,系统中不允许两个用户具有相同的 UserName 值,但客户端验证是很难做到这一点的,使用 Remote 特性可以把 UserName 的值传到服务器(可以指定操作名和控制器名),然后在服务器端进行验证:
[Remote("CheckUserName","Account")]
public string Username { get; set; }
// Account 控制器中的验证方法
public JsonResult CheckUserName(string username)
{
var result = Membership.FindUsersByName(username).Count == 0;
return Json(result, JsonRequestBehavior.AllowGet);
}
Compare:它用来确保模型对象的两个属性拥有相同的值。例如购物时,要求用户输入两次 Email 地址以确保用户输入无误:
[RegularExpression(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,4}")]
public string Email { get; set; }
[System.Web.Mvc.Compare("Email")]
public string EmailConfirm { get; set; }
自定义错误提示消息及本地化
每个验证特性都允许传递一个带有自定义错误提示消息的参数。例如 Email 的原本错误提示消息是一个正则表达式,在客户看来就像是一串乱码,此时自定义错误消息就派上了用处:
[RegularExpression(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,4}",
ErrorMessage = "Email 格式不正确!")]
public string Email { get; set; }
[System.Web.Mvc.Compare("Email", ErrorMessage = "两次输入的密码不同!")]
public string EmailConfirm { get; set; }
自定义的错误提示消息在字符串中也带有一个格式项。内置特性使用友好的属性显示名称格式化错误提示消息字符串,注意,是属性显示名称(后面会讲到),若无,则直接使用属性名填充字符串占位符。
[Required(ErrorMessage="Your {0} is required.")]
[StringLength(160, MinimumLength = 3)]
public string LastName { get; set; }
注解的后台原理
ASP.NET MVC 的验证特性是由模型绑定器、模型元数据、模型验证器、模型状态组成的协调系统的一部分。默认情况下,ASP.NET MVC 框架在模型绑定时执行验证逻辑,当操作方法带有参数时,就会隐式的执行模型绑定,当然,也可以调用 UpdateModel 或 TryUpdateModel 方法显式执行模型绑定。
- 模型绑定器一旦使用新值完成对模型属性的更新,就会利用当前的模型元数据获得模型的所有验证器。
- ASP.NET MVC 运行时提供了一个验证器 DataAnnotationsModelValidator 来于数据注解一同工作,它会找到所有的验证特性并执行验证逻辑。
- 模型绑定器捕获所有失败的验证规则并把它们放入模型状态中。
模型绑定主要的副产品是模型状态 ModelState,模型状态不仅包含了用户想放入模型属性里的值,也包括与每个属性相关联的所有错误。假设用户在没有填写 LastName 值的情况下提交了表单,由于设置了 Required 验证注解特性,因此模型绑定之后,下面所有的表达式都将返回 false:
bool flag;
flag = ModelState.IsValid;
flag = ModelState.IsValidField("LastName");
flag = ModelState["LastName"].Errors.Count > 0;
也可以在模型状态中查看与失败验证相关的错误提示消息:
var errorMsg = ModelState["LastName"].Errors[0].ErrorMessage;
很少会编写代码来查看特定的错误消息。之前的 HTML 辅助方法中也介绍过,辅助方法可以利用模型状态(和模型状态中出现的错误)来改变模型在视图中的显示。例如,ValidationMessage 辅助方法可以通过查看模型状态来显示与特定部分视图数据相关的错误消息:
@Html.ValidationMessageFor(m => m.LastName)
控制器操作和验证错误
控制器决定了在模型验证失败和成功时的执行流程。当验证成功时,操作通常会执行必要的步骤来保存和更新客户的信息;当验证失败时,操作一般会重新渲染提交模型值的视图。这样就可以让用户看到所有的验证错误提示消息,并按照提示进行更正或补填遗漏的字段信息。
[HttpPost]
public ActionResult AddressAndPayment(Order newOrder)
{
if (ModelState.IsValid)
{
newOrder.Username = User.Identity.Name;
newOrder.OrderDate = DateTime.Now;
// Store DB and do something...
return RedirectToAction("Complete", new { id = newOrder.OrderId });
}
return View(newOrder);
}
注意,如果模型状态无效,操作就会重新渲染 AddressAndPayment 视图,给用户一个修正错误并重新提交表单的机会。
自定义验证逻辑
数据注解特性给验证带来了简易性和透明性,但也不可能满足程序中可能遇到的所有验证场合。
ASP.NET MVC 框架的扩展性意味着实现自定义验证逻辑有着很大的可行性。这里重点介绍两个核心应用方法:
- 将验证逻辑封装在自定义的数据注解中。
- 将验证逻辑封装在模型对象中。
封装在自定义数据注解中,则可以轻松实现在多个模型中重用逻辑,当然,这需要在特性内部编写代码以应对不同类型的模型,但一旦实现,新的注解就可以在多处重用。
将验证逻辑直接放入模型对象中,就意味着验证逻辑可以很容易的编码实现,因为这样只需关心一种模型对象的验证逻辑,但不利于重用。
自定义注解
假设要限制客户输入姓氏中单词的数量,并让这种验证在其他模型中重用,那就可以考虑将验证逻辑封装在一个可重用的特性中。所有的验证注解特性最终都派生自基类 ValidationAttribute,它是个抽象类,在命名空间 System.ComponentModel.DataAnnotations 中定义,程序员的验证逻辑也必须派生自该类。
using System.ComponentModel.DataAnnotations;
namespace MvcMusicStore.ExtendValidationAttribute
{
public class MaxWordsAttribute : ValidationAttribute
{
/// <summary>
/// 为了实现验证逻辑,至少需要重新基类中 IsValid 方法的其中一个版本。
/// 重写 IsValid 方法时可以利用的 ValidationContext 参数提供了很多可在 IsValid
/// 内部使用的信息,如模型类型、模型对象实例、用来验证属性的人性化显示名称以及其他一些信息
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
return ValidationResult.Success;
}
}
}
方法的第一个参数是要验证的对象的值,如果这个值有效,就可以返回一个成功的验证结果。在本例中,在判断它是否有效时,需要知道欲限制单词数量的上限,要获得这个上限,可通过创建构造函数要求顾客把最大单词数作为一个参数传递给它:
public class MaxWordsAttribute : ValidationAttribute
{
private readonly int _maxWords;
public MaxWordsAttribute(int maxWords)
{
this._maxWords = maxWords;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value != null)
{
var valueString = value.ToString();
if (valueString.Split(' ').Length > this._maxWords)
{
return new ValidationResult("Too many words!");
}
}
return ValidationResult.Success;
}
}
现在已经实现了验证的逻辑,但上述代码的问题在于那行返回硬编码错误消息的代码。使用数据注解的开发人员希望可以使用 ValidationAttribute 的 ErrorMessage 属性来自定义错误消息,同时还要与其它验证特性一样,提供一个默认的错误提示消息(在开发人员没有提供自定义的错误提示消息时使用),并且还要利用验证的属性名称生成错误提示消息:
public class MaxWordsAttribute : ValidationAttribute
{
private readonly int _maxWords;
public MaxWordsAttribute(int maxWords)
: base("{0} has too many words.")
// 设置默认值,如果特性不指明错误提示消息的话
// 如果特性显式的传递了错误消息,那么上面的字符串会被替换
// 注意,显式传递错误消息也是带有占位符的,这样下述语句
// FormatErrorMessage(validationContext.DisplayName)
// 才会把 DisplayName 传递至占位符
{
this._maxWords = maxWords;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value != null)
{
var valueString = value.ToString();
if (valueString.Split(' ').Length > this._maxWords)
{
// 基于发生错误的数据字段对错误消息应用格式设置
var errorMessage = FormatErrorMessage(validationContext.DisplayName);
return new ValidationResult(errorMessage);
}
}
return ValidationResult.Success;
}
}
代码作了两处改动,首先向基类构造函数传递了默认的错误提示消息,并带有一个占位符;调用继承自基类的 FormatErrorMessage 方法会自动使用 DisplayName 来格式化这个字符串!至此,该特性可以灵活(灵活即:可指定参数值和错误消息)重用。
// 因为在扩展时创建了新的文件夹存放自定义验证类便于管理
// 因此,使用时需要引用该命名空间
using MvcMusicStore.ExtendValidationAttribute;
[Required]
[StringLength(160)]
[MaxWords(10, ErrorMessage = "There are too many words in {0}")]
public string LastName { get; set; }
IValidatableObject
自验证(self - validating)模型指一个知道如何验证自身的模型对象,该模型对象可以通过实现 IValidatableObject 接口来实现对自身的验证。
例如,下面在 Order 模型中直接实现对 LastName 字段中单词个数的检查:
public partial class Order : IValidatableObject
{
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (LastName != null && LastName.Split(' ').Length > 10)
{
yield return new ValidationResult("The last name has too many words!", new[] { "LastName" });
}
// ......
}
// rest of Order implementation and properties
// ...
}
这种方式与特性版有明显的不同点:
- MVC 运行时为执行验证而调用的方法名称是 Validate 而不是 IsValid,更重要的是返回类型和参数也不同。
- Validate 返回类型是 IEnumerable<ValidationResult>,而不是单独的 ValidationResult 对象。因为从表面上看,内部的验证逻辑验证的是整个模型,因此可能返回多个验证错误。
- 没有 value 参数传递给 Validate 方法,因为该方法是一个模型实例方法,因此肯定可以看到当前模型对象自有的属性值。
上面的代码使用了 C# yield return 语法来构建枚举返回值,同时代码还需要显式的告知 ValidationResult 与其关联的字段名称,ValidationResult 构造函数最后一个参数是 String 数组,因为这样可以使验证的结果与多个属性相关联(一组属性都执行这一验证,返回相同的错误提示消息)。
显示和编辑注解
和验证特性一样,模型元数据提供器会收集下面的显示(和编辑)注解信息,以供 HTML 辅助方法和 ASP.NET MVC 运行时的其他组件使用,HTML 辅助方法可以使用任何可用的元数据来改变模型的显示和编辑 UI。
- Display:为模型属性设置友好的“显示名称”:[Display(Name = "First Name")];
- ScaffoldColumn:该特性可以隐藏 HTML 辅助方法如 EditorForModel 和 DisplayForModel 渲染的一些特性:[ScaffoldColumn(false)]
- DisplayFormat:通过命名参数来处理属性的各种格式化选项。下面代码可格式化为货币:
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:c}")]
public decimal Total { get; set; }
- ReadOnly:确保默认的模型绑定器不使用请求中的新值来更新属性。如在 TotalPrice(该值为计算得到,而不是用户请求来赋值) 上设置。
- DataType:这是一个枚举值,为运行时提供属性的特定用途信息。如 string 类型的属性可应用于很多场合:email、URL、密码等。
[DataType(DataType.Password)]
[Display(Name = "新密码")]
public string NewPassword { get; set; }
- UIHint:给 ASP.NET MVC 运行时提供了一个模版名称,以备调用模板辅助方法(DisplayFor、EditorFor)渲染输出时使用。
- HiddenInput:渲染一个隐藏的文本域,隐藏文本域有时非常好用。(但并非万无一失,恶意用户可篡改提交的表单值)