登峰造极的 ASP.NET

ASP.NET MVC 2 中的模型验证和元数据

K. Scott Allen

下载代码示例

ASP.NET MVC 2 版本新增的一项功能可以验证服务器和客户端的用户输入。您只需为该框架提供一些有关要验证数据的信息,该框架将会为您处理艰巨的工作和详细信息。

对于我们这些使用 ASP.NET MVC 1.0 编写自定义验证代码和自定义模型绑定器来执行简单模型验证的人来说,此功能可谓天赐福音。在本文中,我将探讨 ASP.NET MVC 2 中内置的验证支持。

不过,在讨论这些新功能之前,我将回顾一下旧的方法。多年来,ASP.NET WebForms 中的验证功能一直让我非常满意。我想,回顾一下这些功能对于理解验证框架的作用非常有帮助。

控制验证

您如果用过 ASP.NET WebForms,应当会知道将验证逻辑添加到 WebForm 中相对简单。您使用控件表示验证规则。例如,如果要确保用户在 TextBox 控件中输入特定文本,只需添加一个指向 TextBox 的 RequiredFieldValidator 控件,如下所示:

<form id="form1" runat="server">
  <asp:TextBox runat="server" ID="_userName" />
  <asp:RequiredFieldValidator runat="server" ControlToValidate="_userName"
                               ErrorMessage="Please enter a username" />
  <asp:Button runat="server" ID="_submit" Text="Submit" />
</form>

RequiredFieldValidator 将封装客户端和服务器端的逻辑以确保用户提供的是用户名。若要提供客户端验证,该控件会将 JavaScript 发送到客户端浏览器,此脚本可确保将表单传回服务器之前用户操作满足所有验证规则。

想想这些 WebForm 验证控件提供了什么,它们的功能的确异常强大!

  • 可以通过声明方式为一个位置的页面表示验证规则。
  • 如果用户操作不满足这些验证规则,客户端验证将阻止往返服务器。
  • 服务器验证将防止恶意用户避开客户端脚本。
  • 服务器和客户端验证逻辑保持同步,而不会成为维护问题。

但是在 ASP.NET MVC 中,您无法使用这些验证控件并继续忠实 MVC 设计模式的精神。幸运的是,此框架的第 2 版中的一些功能甚至更好。

控件与模型

可以将 WebForm 控件(如 TextBox)视为一个简单的用户数据容器。可以用初始值填充此控件,并向用户显示该值,也可以在回发后通过检查该控件来检索用户输入或编辑的任何值。使用 MVC 设计模式时,M(模型)与数据容器扮演着相同的角色。可以用需要提供给用户的信息来填充模型,它会将更新的值回送到您的应用程序。因此,模型是表示验证规则和约束的一个理想场所。

下面是一个现成的示例。如果创建一个新的 ASP.NET MVC 2 应用程序,您将在新项目中找到一个控制器 AccountController。该控制器负责处理新用户的注册请求,以及登录和密码更改请求。其中每个操作都使用一个专用的模型对象。可以在 Models 文件夹中的 AccountModels.cs 文件中找到这些模型。例如,不带验证规则的 RegisterModel 类如下所示:

public class RegisterModel
{
  public string UserName { get; set; }
  public string Email { get; set; }
  public string Password { get; set; }
  public string ConfirmPassword { get; set; }
}

AccountController 的 Register 操作将此 RegisterModel 类的实例用作参数:

[HttpPost]
public ActionResult Register(RegisterModel model)
{
    // ...
}

如果模型有效,Register 操作会将此模型信息转发到可以创建新用户的服务。

RegisterModel 模型是视图特定模型(视图模型)的一个极好示例。此模型未设计用于处理特定的数据库表、Web 服务调用或业务对象。它设计用于处理特定视图(Register.aspx 视图,图 1 中显示了该视图的一部分)。模型上的每个属性都映射到该视图中的一个输入控件。我建议您使用视图模型,因为它们简化了 MVC 开发中的许多方案(包括验证)。


图 1 注册信息

模型和元数据

用户在 Register 视图中输入帐户信息时,MVC 框架将确保用户提供 UserName 和 Email。该框架还将确保 Password 和 ConfirmPassword 字符串匹配,并且该密码至少为 6 个字符长。它是如何做到这一切的呢?通过检查附加到 RegisterModel 类的元数据并对其进行操作。图 2 显示了 RegisterModel 类,同时显示了其验证属性。

图 2 带有验证属性的 RegisterModel 类

[PropertiesMustMatch("Password", "ConfirmPassword", 
  ErrorMessage = "The password and confirmation password do not match.")]
public class RegisterModel
{
  [Required]        
  public string UserName { get; set; }

  [Required]
  public string Email { get; set; }

  [Required]
  [ValidatePasswordLength]
  public string Password { get; set; }

  [Required]
  public string ConfirmPassword { get; set; }
}

用户提交 Register 视图时,ASP.NET MVC 中的默认模型绑定器将尝试构建 RegisterModel 类的一个新实例,将其作为参数传递到 AccountController 的 Register 操作。模型绑定器检索当前请求中的信息来填充 RegisterModel 对象。例如,它可以自动查找一个名为 UserName 的 HTML 输入控件的 POST 值,并用该值填充 RegisterModel 的 UserName 属性。自版本 1.0 以来,该行为已出现在 ASP.NET MVC 中,因此如果您已经使用过该框架,则不会对其感到陌生。

版本 2 中的新增功能是如果没有元数据可用于 RegisterModel 对象,默认模型绑定器同时请求一个元数据提供程序。此过程最后生成一个 ModelMetaData 派生对象,其目的不仅是描述与该模型关联的验证规则,还描述与该模型在视图中的显示相关的信息。关于此模型元数据如何通过模板影响模型的显示,ASP.NET 团队成员 Brad Wilson 撰写了一系列有深度的文章。该系列中第一篇文章的网址为 bradwilson.typepad.com/blog/2009/10/aspnet-mvc-2-templates-part-1-introduction.html

模型绑定器拥有与该模型相关联的 ModelMetaData 对象后,就可以使用内部验证元数据来验证模型对象。默认情况下,ASP.NET MVC 使用数据注释属性(如 [Required])中的元数据。当然,ASP.NET MVC 是可插入、可扩展的,因此如果您希望为模型元数据设计其他源,可以实现自己的元数据提供程序。对于这一主题,Ben Scheirman 在一篇名为“自定义 ASP.NET MVC 2 — 元数据和验证”的文章中提供了一些非常棒的信息,文章网址为 dotnetslackers.com/articles/aspnet/customizing-asp-net-mvc-2-metadata-and-validation.aspx

数据注释

顺便提一下,您可以构建自己的验证属性(我们稍后将会讨论),但 [Required] 是 System.ComponentModel.DataAnnotations 程序集中存在的众多标准验证属性之一。图 3 显示了注释程序集中的验证属性的完整列表。

图 3 注释程序集中的验证属性

属性 说明
StringLength 指定数据字段中允许的字符串最大长度。
Required 指定数据字段值是必需的。
RegularExpression 指定数据字段值必须与指定的正则表达式匹配。
Range 指定数据字段值的数值范围限制。
DataType 指定与某个数据字段关联的附加类型的名称(一个 DataType 枚举值,如 EmailAddress、Url 或 Password)。

这些数据注释属性已迅速在 Microsoft .NET Framework 中普遍存在。您不仅可以在 ASP.NET MVC 应用程序中使用这些属性,而且 ASP.NET 动态数据、Silverlight 和 Silverlight RIA 服务也理解它们。

查看验证

通过就地验证元数据,当用户输入不正确的数据时,错误将自动显示在视图中。图 4 显示了用户在没有提供任何信息的情况下单击 Register 时 Register 视图的外观。


图 4 验证失败

图 4 中的显示内容是使用 ASP.NET MVC 2 中一些新的 HTML 帮助器(包括 ValidationMessageFor 帮助器)构建的。ValidationMessageFor 控制当特定数据字段验证失败时放置验证消息的位置。图 5 显示了 Register.aspx 的一段摘录,演示如何使用 ValidationMessageFor 和 ValidationSummary 帮助器。

图 5 如何使用新的 HTML 帮助器

<% using (Html.BeginForm()) { %>
    <%= Html.ValidationSummary(true, "Account creation was unsuccessful. " +
    "Please correct the errors and try again.") %>
    <div>
        <fieldset>
            <legend>Account Information</legend>
            
            <div class="editor-label">
                <%= Html.LabelFor(m => m.UserName) %>
            </div>
            <div class="editor-field">
                <%= Html.TextBoxFor(m => m.UserName) %>
                <%= Html.ValidationMessageFor(m => m.UserName) %>
            </div>

自定义验证

并非 RegisterModel 类上的所有验证属性都是 Microsoft 数据注释程序集中的属性。[PropertiesMustMatch] 和 [ValidatePasswordLength] 都是自定义属性,您可以发现它们已在承载 RegisterModel 类的同一 AccountModel.cs 文件中定义。如果只是希望提供自定义验证规则,则无需担心自定义元数据提供程序或元数据类。您只需从抽象类 ValidationAttribute 派生类,并为 IsValid 方法提供一个实现。ValidatePasswordLength 属性的实现如图 6 所示。

图 6 ValidatePasswordLength 属性的实现

[AttributeUsage(AttributeTargets.Field | 
                AttributeTargets.Property, 
                AllowMultiple = false, 
                Inherited = true)]
public sealed class ValidatePasswordLengthAttribute 
    : ValidationAttribute
{
    private const string _defaultErrorMessage = 
        "’{0}’ must be at least {1} characters long.";

    private readonly int _minCharacters = 
        Membership.Provider.MinRequiredPasswordLength;

    public ValidatePasswordLengthAttribute()
        : base(_defaultErrorMessage)
    {
    }

    public override string FormatErrorMessage(string name)
    {
        return String.Format(CultureInfo.CurrentUICulture, 
            ErrorMessageString,
            name, _minCharacters);
    }

    public override bool IsValid(object value)
    {
        string valueAsString = value as string;
        return (valueAsString != null && 
            valueAsString.Length >= _minCharacters);
    }
}

另一个属性 PropertiesMustMatch 是可以在类级别上应用以执行跨属性验证的验证属性的一个极好示例。

客户端验证

到目前为止我们探讨的 RegisterModel 验证全都发生在服务器上。幸好在客户端上启用验证也很容易。我会尽可能地使用客户端验证,因为在从我的服务器卸载某些工作时,它可以为客户提供快速反馈。不过,服务器端逻辑需要就地保留,以防某个人的浏览器中没有启用脚本(或企图故意向服务器发送错误数据)。

启用客户端验证的过程分两个步骤。第 1 步是确保视图包含适当的验证脚本。所需的全部脚本都驻留在新 MVC 应用程序的 Scripts 文件夹中。MicrosoftAjax.js 脚本是 Microsoft AJAX 库的核心,并且是您需要包含的第一个脚本。第二个脚本为 MicrosoftMvcValidation.js。我通常将 ContentPlaceHolder 添加到 MVC 应用程序的母版页中以承载脚本,如下所示:

<head runat="server">
    <title><asp:ContentPlaceHolder ID="TitleContent" runat=
"server" /></title>
    <link href="../../Content/Site.css" rel="stylesheet" type=
"text/css" />

    <asp:ContentPlaceHolder ID="Scripts" runat="server">
       
    </asp:ContentPlaceHolder>
    
</head>

然后,视图可以使用 Content 控件包含所需的脚本。以下代码可确保验证脚本存在:

<asp:Content ContentPlaceHolderID="Scripts" runat="server">
    <script src="../../Scripts/MicrosoftAjax.js" 
            type="text/javascript"></script>
    <script src="../../Scripts/MicrosoftMvcValidation.js" 
            type="text/javascript"></script>
</asp:Content>

使用客户端验证的第 2 步是在需要验证支持的视图内调用 EnableClientValidation HTML 帮助器方法。使用 BeginForm HTML 帮助器之前,请确保调用此方法,如下所示:

<%
       Html.EnableClientValidation(); 
       using (Html.BeginForm())
        {
     %>
     
     <!-- the rest of the form ... -->
     
     <% } %>

请注意,客户端验证逻辑仅适用于内置验证属性。对于 Register 视图,这意味着客户端验证将确保所需的字段存在,但不知如何验证密码长度或确认两个密码字段是否匹配。幸好,很容易添加插入到 ASP.NET MVC JavaScript 验证框架的自定义 JavaScript 验证逻辑。Phil Haack 在博客文章“ASP.NET MVC 2 自定义验证”中做了详细介绍,文章网址为 haacked.com/archive/2009/11/19/aspnetmvc2-custom-validation.aspx

做个总结,您可以看到,对常见验证方案的内置支持是 ASP.NET MVC 2 的一项非常强大的新增功能。不仅可通过模型对象上的属性轻松添加验证规则,而且验证功能自身也非常灵活,并且易于扩展。在您的下一个 ASP.NET MVC 应用程序中,就请开始使用这些功能来节省时间并减少代码行数吧。

K. Scott Allen 是 Pluralsight 技术团队的成员,也是 OdeToCode 的创始人。您可以通过 scott@OdeToCode.com 与 Allen 联系,通过 odetocode.com/blogs/scott 访问其博客,或通过 Twitter 了解他(网址为 twitter.com/OdeToCode)。

衷心感谢以下技术专家对本文的审阅:Brad Wilson