18、MVC框架模型验证全解析

MVC框架模型验证全解析

1. 模型验证概述

在Web应用开发中,用户输入的数据往往不能直接使用,需要进行验证以确保其符合业务规则和数据模型的要求。模型验证就是确保接收到的数据适合绑定到模型,并在不符合要求时向用户提供有用信息以帮助他们纠正问题的过程。这一过程包括两部分:一是检查数据以维护领域模型的完整性,二是帮助用户纠正问题以提升用户体验。

MVC框架为模型验证提供了广泛的支持,下面将介绍不同的验证技术。

2. 创建示例项目

在开始验证技术的学习之前,我们需要创建一个简单的MVC应用程序。这里创建了一个名为 Appointment 的视图模型,代码如下:

using System;
using System.ComponentModel.DataAnnotations;
namespace MvcApp.Models {
    public class Appointment {
        public string ClientName { get; set; }
        [DataType(DataType.Date)]
        public DateTime Date {get; set;}
        public bool TermsAccepted { get; set; }
    }
}

同时,创建了用于渲染 Appointment 类编辑器的视图 MakeBooking.cshtml

@model MvcApp.Models.Appointment
@{
    ViewBag.Title = "Make A Booking";
}
<h4>Book an Appointment</h4>
@using (Html.BeginForm()) {
    <p>Your name: @Html.EditorFor(m => m.ClientName)</p>
    <p>Appointment Date: @Html.EditorFor(m => m.Date)</p>
    <p>@Html.EditorFor(m => m.TermsAccepted) I accept the terms & conditions</p>

    <input type="submit" value="Make Booking" />
}

还有一个控制器类 AppointmentController ,包含操作 Appointment 对象的动作方法:

public class AppointmentController : Controller {
    private IAppointmentRepository repository;
    public AppointmentController(IAppointmentRepository repo) {
        repository = repo;
    }
    public ViewResult MakeBooking() {
        return View(new Appointment { Date = DateTime.Now });
    }

    [HttpPost]
    public ViewResult MakeBooking(Appointment appt) {
        repository.SaveAppointment(appt);
        return View("Completed", appt);
    }
}

当前应用程序会接受用户提交的任何数据,但为了维护应用程序和领域模型的完整性,在接受用户提交的 Appointment 对象之前,需要满足以下三个条件:
1. 用户必须提供姓名。
2. 用户必须提供未来的日期(格式为 mm/dd/yyyy )。
3. 用户必须勾选接受条款和条件的复选框。

模型验证就是强制执行这些要求的过程。

3. 显式验证模型

最直接的验证模型的方法是在动作方法中进行验证。示例代码如下:

[HttpPost]
public ViewResult MakeBooking(Appointment appt) {
    if (string.IsNullOrEmpty(appt.ClientName)) {
        ModelState.AddModelError("ClientName", "Please enter your name");
    }
    if (ModelState.IsValidField("Date") && DateTime.Now > appt.Date) {
        ModelState.AddModelError("Date", "Please enter a date in the future");
    }
    if (!appt.TermsAccepted) {
        ModelState.AddModelError("TermsAccepted", "You must accept the terms");
    }
    if (ModelState.IsValid) {
        repository.SaveAppointment(appt);
        return View("Completed", appt);
    } else {
        return View();
    }
}

在上述代码中,我们检查模型绑定器分配给参数对象属性的值,并将发现的任何错误注册到 ModelState 属性中。验证完成后,通过 ModelState.IsValid 属性判断是否有错误,如果没有错误则保存预约并渲染 Completed 视图,否则重新渲染当前视图让用户纠正输入值。

模板化视图助手会检查验证错误,如果某个属性有错误,会为输入元素添加 input-validation-error 的CSS类,示例样式如下:

.input-validation-error {
    border: 1px solid #ff0000;
    background-color: #ffeeee;
}

这会使有错误的元素显示红色边框和粉色背景。

3.1 复选框样式问题

一些浏览器(如Chrome和Firefox)会忽略应用于复选框的样式,导致视觉反馈不一致。解决方法是在 ~/Views/Shared/EditorTemplates/Boolean.cshtml 中创建自定义模板,并将复选框包装在 div 元素中。示例模板如下:

@model bool?

@if (ViewData.ModelMetadata.IsNullableValueType) {
    @Html.DropDownListFor(m => m, new SelectList(new [] {"Not Set", "True", "False"}, Model))
} else {
    ModelState state = ViewData.ModelState[ViewData.ModelMetadata.PropertyName];
    bool value = Model ?? false;

    if (state != null && state.Errors.Count > 0) {
        <div class="input-validation-error" style="float:left">
            @Html.CheckBox("", value)
        </div>
    } else {
        @Html.CheckBox("", value)        
    }
}

4. 显示验证消息

模板化助手方法应用于输入元素的CSS样式可以指示字段有问题,但不能告诉用户具体问题是什么。MVC框架提供了一些方便的HTML助手方法来解决这个问题,例如 Html.ValidationSummary 助手方法。示例代码如下:

@model MvcApp.Models.Appointment
@{
    ViewBag.Title = "Make A Booking";
}
<h4>Book an Appointment</h4>
@using (Html.BeginForm()) {
    @Html.ValidationSummary()

    <p>Your name: @Html.EditorFor(m => m.ClientName)</p>
    <p>Appointment Date: @Html.EditorFor(m => m.Date)</p>
    <p>@Html.EditorFor(m => m.TermsAccepted) I accept the terms & conditions</p>

    <input type="submit" value="Make Booking" />
}

Html.ValidationSummary 助手会将注册的验证错误摘要添加到显示给用户的页面上。如果没有错误,该助手不会生成任何HTML。

ValidationSummary 方法有多个重载版本,常用的如下表所示:
| 重载方法 | 描述 |
| — | — |
| Html.ValidationSummary() | 生成所有验证错误的摘要。 |
| Html.ValidationSummary(bool) | 如果 bool 参数为 true ,则仅显示模型级错误;如果为 false ,则显示所有错误。 |
| Html.ValidationSummary(string) | 在所有验证错误摘要之前显示一条消息(包含在 string 参数中)。 |
| Html.ValidationSummary(bool, string) | 在验证错误之前显示一条消息。如果 bool 参数为 true ,则仅显示模型级错误。 |

模型级错误可用于处理多个属性值之间交互产生的问题。例如,名为Joe的客户不能在周一预约,示例代码如下:

public ViewResult MakeBooking(Appointment appt) {
    if (string.IsNullOrEmpty(appt.ClientName)) {
        ModelState.AddModelError("ClientName", "Please enter your name");
    }
    if (ModelState.IsValidField("Date") && DateTime.Now > appt.Date) {
        ModelState.AddModelError("Date", "Please enter a date in the future");
    }
    if (!appt.TermsAccepted) {
        ModelState.AddModelError("TermsAccepted", "You must accept the terms");
    }
    if (ModelState.IsValidField("ClientName") && ModelState.IsValidField("Date")
        && appt.ClientName == "Joe" && appt.Date.DayOfWeek == DayOfWeek.Monday) {
            ModelState.AddModelError("", "Joe cannot book appointments on Mondays");
    }

    if (ModelState.IsValid) {
        repository.SaveAppointment(appt);
        return View("Completed", appt);
    } else {
        return View();
    }
}

通过将空字符串作为第一个参数传递给 ModelState.AddModelError 方法来注册模型级错误。

4.1 显示属性级验证消息

为了避免验证摘要中重复显示属性特定的消息,我们可以在字段旁边显示属性级错误。示例代码如下:

@model MvcApp.Models.Appointment
@{
    ViewBag.Title = "Make A Booking";
}
<h4>Book an Appointment</h4>
@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)

    <p>
        Your name: @Html.EditorFor(m => m.ClientName)
        @Html.ValidationMessageFor(m => m.ClientName)
    </p>
    <p>
        Appointment Date: @Html.EditorFor(m => m.Date)
        @Html.ValidationMessageFor(m => m.Date)
    </p>
    <p>
        @Html.EditorFor(m => m.TermsAccepted) I accept the terms & conditions
        @Html.ValidationMessageFor(m => m.TermsAccepted)
    </p>

    <input type="submit" value="Make Booking" />
}

Html.ValidationMessageFor 助手用于显示单个模型属性的验证错误。

5. 使用替代验证技术

在MVC框架中,除了在动作方法中进行模型验证外,还有其他验证方法。下面将介绍在模型绑定器中进行验证的方法。

5.1 在模型绑定器中执行验证

默认的模型绑定器在绑定过程中会执行一些基本的验证。例如,如果清空 Date 字段并提交表单,模型绑定器会因为无法从空字段创建 DateTime 对象而添加错误消息。

内置的 DefaultModelBinder 类提供了一些可重写的方法来添加验证逻辑,如下表所示:
| 方法 | 描述 | 默认实现 |
| — | — | — |
| OnModelUpdated | 当绑定器尝试为模型对象的所有属性赋值时调用 | 应用模型元数据定义的验证规则,并将任何错误注册到 ModelState 中。 |
| SetProperty | 当绑定器想要将值应用于特定属性时调用 | 如果属性不能为 null 且没有提供值,则注册 <name>字段是必需的 错误;如果有值但无法解析,则注册 值<value>对<name>无效 错误。 |

我们可以重写这些方法将验证逻辑推送到绑定器中,示例代码如下:

using System;
using System.ComponentModel;
using System.Web.Mvc;
using MvcApp.Models;
namespace MvcApp.Infrastructure {
    public class ValidatingModelBinder : DefaultModelBinder {
        protected override void SetProperty(ControllerContext controllerContext, 
            ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, 
            object value) {
            // 确保调用基实现
            base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
            // 执行属性级验证
            switch (propertyDescriptor.Name) {
                case "ClientName":
                    if (string.IsNullOrEmpty((string)value)) {
                        bindingContext.ModelState.AddModelError("ClientName", 
                            "Please enter your name");
                    }
                    break;
                case "Date":
                    if (bindingContext.ModelState.IsValidField("Date") && 
                        DateTime.Now > ((DateTime)value)) {
                        bindingContext.ModelState.AddModelError("Date", 
                            "Please enter a date in the future");
                    }
                    break;
                case "TermsAccepted":
                    if (!((bool)value)) {
                        bindingContext.ModelState.AddModelError("TermsAccepted", 
                            "You must accept the terms");
                    }
                    break;
            }
        }
        protected override void OnModelUpdated(ControllerContext controllerContext,
            ModelBindingContext bindingContext) {
            // 确保调用基实现
            base.OnModelUpdated(controllerContext, bindingContext);
            // 获取模型
            Appointment model = bindingContext.Model as Appointment;
            // 应用模型级验证
            if (model != null &&
                bindingContext.ModelState.IsValidField("ClientName") &&
                bindingContext.ModelState.IsValidField("Date") &&
                model.ClientName == "Joe" &&
                model.Date.DayOfWeek == DayOfWeek.Monday) {
                bindingContext.ModelState.AddModelError("",
                    "Joe cannot book appointments on Mondays");
            }
        }
    }
}

在上述代码中, SetProperty 方法用于执行属性级验证, OnModelUpdated 方法用于执行模型级验证。

需要注意的是,在使用绑定器进行模型验证时,必须调用 SetProperty OnModelUpdated 方法的基实现,否则会失去一些关键功能的支持。

我们还需要在 Global.asax Application_Start 方法中注册验证绑定器,示例代码如下:

protected void Application_Start() {
    AreaRegistration.RegisterAllAreas();
    ModelBinders.Binders.Add(typeof(Appointment), new ValidatingModelBinder());
    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
}

由于将 Appointment 模型类的验证逻辑移到了绑定器中,动作方法可以简化为:

[HttpPost]
public ViewResult MakeBooking(Appointment appt) {
    if (ModelState.IsValid) {
        repository.SaveAppointment(appt);
        return View("Completed", appt);
    } else {
        return View();
    }
}

5.2 使用元数据指定验证规则

MVC框架支持使用元数据来表达模型验证规则,这样验证规则可以在绑定过程应用于模型类的任何地方,而不仅仅局限于单个动作方法。验证属性由内置的 DefaultModelBinder 类检测和执行。示例代码如下:

using System;
using System.ComponentModel.DataAnnotations;
using MvcApp.Infrastructure;
namespace MvcApp.Models {
    public class Appointment {
        [Required]
        public string ClientName { get; set; }
        [DataType(DataType.Date)]
        [Required(ErrorMessage="Please enter a date")]
        public DateTime Date { get; set; }
        [Range(typeof(bool), "true", "true", ErrorMessage="You must accept the terms")]
        public bool TermsAccepted { get; set; }
    }
}

上述代码中使用了 Required Range 两个验证属性。 Required 属性指定如果用户没有为某个属性提交值,则为验证错误; Range 属性指定只接受值的一个子集。

内置的验证属性如下表所示:
| 属性 | 示例 | 描述 |
| — | — | — |
| Compare | [Compare("MyOtherProperty")] | 两个属性必须具有相同的值,适用于要求用户两次提供相同信息的情况,如电子邮件地址或密码。 |
| Range | [Range(10, 20)] | 数值(或任何实现 IComparable 的属性类型)必须在指定的最小值和最大值之间。可以只提供一个边界,如 [Range(int.MinValue, 50)] 。 |
| RegularExpression | [RegularExpression("pattern")] | 字符串值必须匹配指定的正则表达式模式,模式必须匹配用户提供的整个值。默认情况下区分大小写,可以使用 (?i) 修饰符使其不区分大小写。 |
| Required | [Required] | 值不能为空或仅由空格组成。如果要将空格视为有效,可以使用 [Required(AllowEmptyStrings = true)] 。 |
| StringLength | [StringLength(10)] | 字符串值的长度不能超过指定的最大值,也可以指定最小长度,如 [StringLength(10, MinimumLength=2)] 。 |

所有验证属性都允许通过设置 ErrorMessage 属性来指定自定义错误消息。

5.3 创建自定义属性验证属性

使用 Range 属性来重现 Required 属性的行为有些麻烦,我们可以通过从 ValidationAttribute 类派生并实现自己的验证逻辑来创建自定义属性验证属性。示例代码如下:

public class MustBeTrueAttribute : ValidationAttribute {
    public override bool IsValid(object value) {
        return value is bool && (bool)value;
    }
}

我们可以将自定义属性应用到模型类的属性上,示例如下:

[MustBeTrue(ErrorMessage="You must accept the terms")]
public bool TermsAccepted { get; set; }

此外,我们还可以从内置的验证属性派生以扩展其功能。例如,创建一个 FutureDateAttribute 属性来确保日期值是未来的日期,示例代码如下:

public class FutureDateAttribute : RequiredAttribute {
    public override bool IsValid(object value) {
        return base.IsValid(value) && 
            value is DateTime &&  
            ((DateTime)value) > DateTime.Now;
    }
}

应用该属性的示例如下:

[DataType(DataType.Date)]
[FutureDate(ErrorMessage="Please enter a date in the future")]
public DateTime Date { get; set; }

5.4 创建模型验证属性

前面介绍的验证属性都是应用于单个模型属性的,只能引发属性级验证错误。我们也可以使用元数据来验证整个模型。示例代码如下:

public class AppointmentValidatorAttribute : ValidationAttribute {
    public AppointmentValidatorAttribute() {
        ErrorMessage = "Joe cannot book appointments on Mondays";
    }
    public override bool IsValid(object value) {
        Appointment app = value as Appointment;
        if (app == null || string.IsNullOrEmpty(app.ClientName) || app.Date == null) {
            // 没有要验证的正确类型的模型,或者没有所需的ClientName和Date属性的值
            return true;
        } else {
            return !(app.ClientName == "Joe" && app.Date.DayOfWeek == DayOfWeek.Monday);
        } 
    }
}

将模型验证属性应用到模型类上的示例如下:

[AppointmentValidator]
public class Appointment {
    [Required]
    public string ClientName { get; set; }
    [DataType(DataType.Date)]
    [FutureDate(ErrorMessage="Please enter a date in the future")]
    public DateTime Date { get; set; }
    [MustBeTrue(ErrorMessage="You must accept the terms")]
    public bool TermsAccepted { get; set; }
}

需要注意的是,如果任何属性级属性注册了验证错误,模型验证属性将不会被使用,这可能会延长用户纠正输入的过程。

5.5 定义自验证模型

另一种验证技术是创建自验证模型,即将验证逻辑作为模型类的一部分。通过实现 IValidatableObject 接口来表示自验证模型类,示例代码如下:

public class Appointment : IValidatableObject {
    public string ClientName { get; set; }
    [DataType(DataType.Date)]
    public DateTime Date { get; set; }
    public bool TermsAccepted { get; set; }
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
        List<ValidationResult> errors = new List<ValidationResult>();
        if (string.IsNullOrEmpty(ClientName)) {
            errors.Add(new ValidationResult("Please enter your name"));
        }
        if (DateTime.Now > Date) {
            errors.Add(new ValidationResult("Please enter a date in the future"));
        }
        if (errors.Count == 0 && ClientName == "Joe" 
            && Date.DayOfWeek == DayOfWeek.Monday) {
            errors.Add(new ValidationResult("Joe cannot book appointments on Mondays"));
        }
        if (!TermsAccepted) {
            errors.Add(new ValidationResult("You must accept the terms"));
        }
        return errors;
    }
}

IValidatableObject 接口定义了一个 Validate 方法,该方法返回一个 ValidationResult 对象的枚举,每个对象代表一个验证错误。如果模型类实现了该接口, Validate 方法将在模型绑定器为模型属性赋值后被调用。

5.6 创建自定义验证提供程序

还可以通过从 ModelValidationProvider 类派生并重写 GetValidators 方法来创建自定义验证提供程序。示例代码如下:

public class CustomValidationProvider : ModelValidatorProvider {
    public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata,
        ControllerContext context) {
        if (metadata.ContainerType == typeof(Appointment)) {
            return new ModelValidator[] {
            new AppointmentPropertyValidator(metadata, context)
        };
        } else if (metadata.ModelType == typeof(Appointment)) {
            return new ModelValidator[] { 
            new AppointmentValidator(metadata, context) 
        };
        }
        return Enumerable.Empty<ModelValidator>();
    }
}

GetValidators 方法会为模型的每个属性调用一次,然后再为模型本身调用一次,返回一个 ModelValidator 对象的枚举。

我们需要在 Global.asax Application_Start 方法中注册自定义验证提供程序,示例代码如下:

protected void Application_Start() {
    AreaRegistration.RegisterAllAreas();
    ModelValidatorProviders.Providers.Add(new CustomValidationProvider());
    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
}

自定义验证提供程序将与内置提供程序一起使用,如果需要移除其他提供程序,可以在添加自定义提供程序之前使用 Clear 方法。

5.7 执行客户端验证

前面介绍的验证技术都是服务器端验证,即用户将数据提交到服务器,服务器验证数据并返回验证结果。而在Web应用中,用户通常期望立即获得验证反馈,这就是客户端验证,通常使用JavaScript实现。

MVC框架支持不显眼的客户端验证,验证规则通过添加到生成的HTML元素的属性来表达,由MVC框架包含的JavaScript库解释这些属性值,并配置 jQuery Validation 库进行实际的验证工作。

5.7.1 启用和禁用客户端验证

客户端验证由 Web.config 文件中的两个设置控制,示例如下:

<configuration>
  <appSettings>
    <add key="ClientValidationEnabled" value="true"/> 
    <add key="UnobtrusiveJavaScriptEnabled" value="true"/> 
  </appSettings>
...

也可以在 Global.asax 中以编程方式控制这些设置,示例代码如下:

protected void Application_Start() {
    AreaRegistration.RegisterAllAreas();
    HtmlHelper.ClientValidationEnabled = true;
    HtmlHelper.UnobtrusiveJavaScriptEnabled = true;
    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
}

还可以为单个视图启用或禁用客户端验证,示例代码如下:

@model MvcApp.Models.Appointment
@{
    ViewBag.Title = "Make A Booking";
    HtmlHelper.ClientValidationEnabled = false;
}
...

要使客户端验证生效,所有设置都必须为 true ,只要将其中一个设置为 false 即可禁用该功能。此外,还必须确保引用了三个特定的JavaScript库,示例代码如下:

<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" 
        type="text/javascript"></script>

    <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" 
        type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" 
        type="text/javascript"></script>
</head>
<body>
    @RenderBody()
</body>
</html>

需要注意的是,jQuery文件的引用顺序很重要,更改顺序可能会导致客户端验证无法执行。

5.7.2 使用CDN加载JavaScript库

除了从应用程序的 ~/Scripts 文件夹引用jQuery库文件外,还可以从Microsoft Ajax内容分发网络(CDN)加载这些文件。使用CDN的好处包括减少用户浏览器加载应用程序的时间,以及减少服务器容量和带宽的需求。

要使用CDN,需要将脚本元素的 src 属性更改为引用以下URL:

<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.5.1.min.js"
    type="text/javascript"></script>
<script src="http://ajax.aspnetcdn.com/ajax/jquery.validate/1.7/jquery.validate.min.js"
    type="text/javascript"></script>
<script src="http://ajax.aspnetcdn.com/ajax/mvc/3.0/jquery.validate.unobtrusive.min.js"
    type="text/javascript"></script>

CDN服务适用于面向Internet的应用程序,但对于内部网项目,从应用程序服务器获取JavaScript库可能更快更便宜。

5.7.3 使用客户端验证

启用客户端验证并确保引用了jQuery库后,就可以开始执行客户端验证。最简单的方法是应用之前用于服务器端验证的元数据属性,如 Required Range StringLength 。示例代码如下:

public class Appointment {
    [Required]
    [StringLength(10, MinimumLength=3)]
    public string ClientName { get; set; }
    [DataType(DataType.Date)]
    [Required(ErrorMessage="Please enter a date")]
    public DateTime Date { get; set; }
    public bool TermsAccepted { get; set; }
}

同时更新 MakeBooking 视图以使用验证摘要和验证消息,示例代码如下:

@model MvcApp.Models.Appointment
@{
    ViewBag.Title = "Make A Booking";
}
<h4>Book an Appointment</h4>
@using (Html.BeginForm()) {
    @Html.ValidationSummary()

    <p>Your name: @Html.EditorFor(m => m.ClientName) 
        @Html.ValidationMessageFor(m => m.ClientName)</p> 
    <p>Appointment Date: @Html.EditorFor(m => m.Date) 
        @Html.ValidationMessageFor(m => m.Date)</p> 
    <p>@Html.EditorFor(m => m.TermsAccepted) I accept the terms & conditions 
        @Html.ValidationMessageFor(m => m.TermsAccepted)</p>

    <input type="submit" value="Make Booking" />

}

当点击提交按钮时,添加到 Appointment 类的验证规则会在浏览器中使用JavaScript应用。客户端验证的错误消息与服务器端验证产生的错误消息看起来相同,但无需向服务器发送请求。客户端验证在用户提交表单时首先执行,之后每次用户按键或更改HTML表单的焦点时都会再次执行。

5.7.4 理解客户端验证的工作原理

MVC框架的客户端验证功能的好处之一是无需编写任何JavaScript,验证规则通过HTML属性表达。例如,当客户端验证禁用时, Html.EditorFor 助手为 ClientName 属性渲染的HTML如下:

<input class="text-box single-line" id="ClientName" name="ClientName" 
    type="text" value="" />

启用验证后,会添加以下属性:

<input class="text-box single-line" data-val="true" data-val-length="The field ClientName must be a string 
with a minimum length of 3 and a maximum length of 10." data-val-length-max="10" data-val-length-
min="3" data-val-required="The ClientName field is required." 
id="ClientName" name="ClientName" type="text" value="" />

data-val 属性用于标识需要验证的字段,单个验证规则使用 data-val-<name> 形式的属性指定,属性值为规则关联的错误消息。一些规则需要额外的属性,如长度规则需要 data-val-length-min data-val-length-max 属性来指定允许的最小和最大字符串长度。

5.7.5 MVC客户端验证与jQuery验证

MVC客户端验证功能基于 jQuery Validation 库构建,如果愿意,也可以直接使用 jQuery Validation 库而忽略MVC功能。 jQuery Validation 库非常灵活且功能丰富,但需要一定的JavaScript知识。示例代码如下:

$(document).ready(function () {
    $('form').validate({
        errorLabelContainer: '#validtionSummary',
        wrapper: 'li',
        rules: {
            ClientName: {
                required: true, 
            }
        },
        messages: {
            ClientName: "Please enter your name"
        }
    });
}); 

MVC客户端验证功能隐藏了JavaScript,并且对客户端和服务器端验证都有效。在MVC应用中可以选择使用任何一种方法,但在单个视图中混合使用时需要谨慎。

5.7.6 自定义客户端验证

内置的客户端验证规则虽然有用但并不全面,我们可以通过编写一些JavaScript来创建自己的规则。

5.7.6.1 显式创建验证HTML属性

最直接的利用额外验证规则的方法是在视图中手动生成所需的属性,示例代码如下:

@model MvcApp.Models.Appointment
@{
    ViewBag.Title = "Make A Booking";
}
<h4>Book an Appointment</h4>
@using (Html.BeginForm()) {

    @Html.ValidationSummary()

    <p>Your name: 
        @Html.TextBoxFor(m => m.ClientName, new { data_val = "true", 
            data_val_email = "Enter a valid email address",
            data_val_required = "Please enter your name"})
        @Html.ValidationMessageFor(m => m.ClientName)</p> 
    <p>Appointment Date: @Html.EditorFor(m => m.Date) 
        @Html.ValidationMessageFor(m => m.Date)</p> 
    <p>@Html.EditorFor(m => m.TermsAccepted) I accept the terms & conditions 
        @Html.ValidationMessageFor(m => m.TermsAccepted)</p>

    <input type="submit" value="Make Booking" />   
}

渲染视图时, ClientName 属性的HTML如下:

<input data-val="true" data-val-email="Enter a valid email address" 
    data-val-required="Please enter your name" id="ClientName" 
    name="ClientName" type="text" value="" />

常用的jQuery验证规则如下表所示:
| 验证规则 | 验证属性 | 描述 |
| — | — | — |
| Required | Required | 要求输入一个值,这是 Required 验证属性使用的规则。 |
| Length | StringLength | 值必须具有最小和/或最大字符数,最小长度由 data-val-length-min 属性指定,最大长度由 data-val-length-max 属性指定。 |
| Range | Range | 值必须在 data-val-required-min data-val-required-max 属性指定的边界之间,可以只提供一个属性来验证值的下限或上限。 |
| Regex | RegularExpression | 值必须匹配 data-val-regexp-pattern 属性指定的正则表达式。 |
| Equalto | Compare | 值必须与 data-val-equalto-other 属性指定的输入元素的值相同。 |
| Email | - | 值必须是有效的电子邮件地址。 |
| Url | - | 值必须是有效的URL。 |
| Date | - | 值必须是有效的日期。 |
| Number | - | 值必须是数字(可以包含小数位)。 |
| Digits | - | 值必须全是数字。 |
| Creditcard | - | 值必须是有效的信用卡号码。 |

需要注意的是,一些验证规则只能检查值的格式,无法确保值的真实性。

5.7.6.2 创建支持客户端验证的模型属性

在视图元素中添加HTML属性虽然简单直接,但验证仅在客户端应用。更简洁的方法是创建自定义验证属性,使其像内置属性一样工作,并触发客户端和服务器端验证。示例代码如下:

public class EmailAddressAttribute : ValidationAttribute {
    private static readonly Regex emailRegex = new Regex(".+@.+\\..+");

    public EmailAddressAttribute() {
        ErrorMessage = "Enter a valid email address";
    }
    public override bool IsValid(object value) {
        return !string.IsNullOrEmpty((string)value) &&
            emailRegex.IsMatch((string)value);
    }
}

要启用客户端验证,需要实现 IClientValidatable 接口,示例代码如下:

public interface IClientValidatable {
    IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata,
        ControllerContext context);
}

EmailAddressAttribute 类添加客户端支持的示例代码如下:

public class EmailAddressAttribute : ValidationAttribute, IClientValidatable {
    private static readonly Regex emailRegex = new Regex(".+@.+\\..+");

    public EmailAddressAttribute() {
        ErrorMessage = "Enter a valid email address";
    }
    public override bool IsValid(object value) {
        return !string.IsNullOrEmpty((string)value) &&
            emailRegex.IsMatch((string)value);
    }
    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
        ModelMetadata metadata, ControllerContext context) {
        return new List<ModelClientValidationRule> {
            new ModelClientValidationRule {
                ValidationType = "email",
                ErrorMessage  = this.ErrorMessage
            }, 
            new ModelClientValidationRule {
                ValidationType = "required",
                ErrorMessage = this.ErrorMessage
            }
        };
    }
}

将属性应用到模型类上的示例如下:

public class Appointment {
    [EmailAddress]
    public string ClientName { get; set; }
...

当渲染 ClientName 属性的编辑器时,视图引擎会检查元数据,找到 IClientValidatable 的实现,并生成相应的HTML属性。数据提交时, IsValid 方法会再次检查数据,该属性可用于客户端和服务器端验证,比显式生成HTML更简洁、安全和一致。

5.7.6.3 创建自定义客户端验证规则

内置的客户端验证规则有限,我们可以通过编写JavaScript创建自定义规则。例如,客户端验证功能对复选框的处理不太好,我们可以创建一个新的客户端验证规则,将jQuery的 required 规则应用于复选框,示例代码如下:

<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.5.1.min.js" type="text/javascript"></script>
    <script src="http://ajax.aspnetcdn.com/ajax/jquery.validate/1.7/jquery.validate.min.js" 
    type="text/javascript"></script>
    <script src="http://ajax.aspnetcdn.com/ajax/mvc/3.0/jquery.validate.unobtrusive.js" 
    type="text/javascript"></script>
    <script type="text/javascript">
        jQuery.validator.unobtrusive.adapters.add("checkboxtrue", function (options) {
            if (options.element.tagName.toUpperCase() == "INPUT" &&
                options.element.type.toUpperCase() == "CHECKBOX") {
                options.rules["required"] = true;
                if (options.message) {
                    options.messages["required"] = options.message;
                }
            }
        });
    </script>
</head>
<body>
    @RenderBody()
</body>
</html>

创建客户端验证规则后,我们可以创建一个引用它的属性,示例代码如下:

public class MustBeTrueAttribute : ValidationAttribute, IClientValidatable {
    public override bool IsValid(object value) {
        return value is bool && (bool)value;
    }
    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
        ModelMetadata metadata, ControllerContext context) {
        return new ModelClientValidationRule[] {
            new ModelClientValidationRule {
                ValidationType = "checkboxtrue",
                ErrorMessage = this.ErrorMessage
            }};
    }
}

MustBeTrue 属性应用到模型类的 bool 属性上,可确保用户在提交数据到服务器之前勾选复选框。

5.7.7 执行远程验证

远程验证是一种客户端验证技术,它调用服务器上的动作方法进行验证。常见的例子是检查用户名是否可用,用户提交数据后,客户端验证会执行,同时会向服务器发送Ajax请求来验证请求的用户名。如果用户名已被占用,会显示验证错误,让用户输入另一个值。

远程验证的好处包括:只有部分属性会进行远程验证,客户端验证的优势仍然适用于用户输入的其他数据值;请求相对轻量级,专注于验证,而不是处理整个模型对象,可减少请求对性能的影响;远程验证在后台执行,用户无需点击提交按钮并等待新视图渲染和返回,提供更响应式的用户体验。

但远程验证也有一定的妥协,它需要向应用程序服务器发送请求,验证速度不如普通的客户端验证快。

使用远程验证的第一步是创建一个可以验证模型属性的动作方法。示例代码如下:

public class AppointmentController : Controller {
    private IAppointmentRepository repository;
    public AppointmentController(IAppointmentRepository repo) {
        repository = repo;
    }
    public ViewResult MakeBooking() {
        return View(new Appointment { Date = DateTime.Now });
    }
    public JsonResult ValidateDate(string Date) {
        DateTime parsedDate;
        if (!DateTime.TryParse(Date, out parsedDate)) {
            return Json("Please enter a valid date (mm/dd/yyyy)",
                JsonRequestBehavior.AllowGet);
        } else if (DateTime.Now > parsedDate) {
            return Json("Please enter a date in the future", JsonRequestBehavior.AllowGet);
        } else {
            return Json(true, JsonRequestBehavior.AllowGet);
        }
    }
    [HttpPost]
    public ViewResult MakeBooking(Appointment appt) {
        if (ModelState.IsValid) {
            repository.SaveAppointment(appt);
            return View("Completed", appt);
        } else {
            return View();
        }
    }
}

支持远程验证的动作方法必须返回 JsonResult 类型,方法参数必须与要验证的字段名称匹配。使用 Json 方法表达验证结果,验证成功时传递 true ,验证失败时传递错误消息。同时,必须传递 JsonRequestBehavior.AllowGet 值作为参数,以覆盖MVC框架默认禁止产生JSON的GET请求的行为。

需要注意的是,验证动作方法会在用户首次提交表单时调用,之后每次编辑数据时也会调用,这可能会产生大量请求,在生产环境中指定服务器容量和带宽时需要考虑这一点。对于验证成本较高的属性,可能不适合使用远程验证。

综上所述,模型验证是Web应用开发中非常重要的一部分,通过合理选择和使用不同的验证技术,可以确保用户输入的数据符合业务规则和数据模型的要求,提升用户体验,维护应用程序和领域模型的完整性。

6. 模型验证技术总结与对比

6.1 不同验证技术对比

验证技术 优点 缺点 适用场景
显式验证模型 简单直接,易于理解和实现,可根据具体逻辑灵活定制验证规则 代码耦合度高,验证逻辑分散在动作方法中,不利于维护和复用 小型项目或验证逻辑简单的场景
在模型绑定器中执行验证 可将验证逻辑与模型绑定过程集成,减少动作方法中的代码量,提高代码的可维护性 实现相对复杂,需要对模型绑定器有一定了解 验证逻辑与模型绑定密切相关的场景
使用元数据指定验证规则 验证规则与模型类紧密关联,可在多个地方复用,提高代码的一致性和可维护性 只能进行一些基本的属性级验证,对于复杂的验证逻辑支持不足 验证规则相对固定,且主要针对属性级验证的场景
创建自定义属性验证属性 可根据具体需求灵活定制验证逻辑,提高代码的可扩展性 需要编写额外的代码,增加了开发成本 内置验证属性无法满足需求,需要自定义验证规则的场景
创建模型验证属性 可对整个模型进行验证,处理多个属性之间的复杂关系 若属性级验证失败,模型验证属性可能无法及时发挥作用,延长用户纠正输入的过程 需要对整个模型进行综合验证的场景
定义自验证模型 将验证逻辑封装在模型类中,提高代码的内聚性和可维护性,验证逻辑可在模型创建时自动执行 可能会使模型类变得复杂,增加理解和维护的难度 验证逻辑与模型紧密相关,且希望在模型创建时自动验证的场景
创建自定义验证提供程序 可实现高度定制化的验证逻辑,满足复杂的业务需求 实现难度较大,需要对验证框架有深入了解 内置验证提供程序无法满足需求,需要自定义验证逻辑的复杂场景
客户端验证 提供即时的验证反馈,减少服务器负载,提高用户体验 依赖客户端环境,安全性较低,无法替代服务器端验证 提高用户体验,减少不必要的服务器请求的场景
远程验证 可在客户端验证的基础上,结合服务器端的信息进行验证,提高验证的准确性 需要向服务器发送请求,增加了网络开销,可能影响性能 需要结合服务器端数据进行验证的场景

6.2 验证技术选择流程

graph TD
    A[开始] --> B{验证逻辑简单吗?}
    B -- 是 --> C{主要是属性级验证吗?}
    C -- 是 --> D[使用元数据指定验证规则]
    C -- 否 --> E{需要对整个模型验证吗?}
    E -- 是 --> F[创建模型验证属性]
    E -- 否 --> G[显式验证模型]
    B -- 否 --> H{验证与模型绑定相关吗?}
    H -- 是 --> I[在模型绑定器中执行验证]
    H -- 否 --> J{需要自定义验证逻辑吗?}
    J -- 是 --> K{逻辑复杂吗?}
    K -- 是 --> L[创建自定义验证提供程序]
    K -- 否 --> M[创建自定义属性验证属性]
    J -- 否 --> N{需要即时反馈吗?}
    N -- 是 --> O{需要服务器数据吗?}
    O -- 是 --> P[远程验证]
    O -- 否 --> Q[客户端验证]
    N -- 否 --> R[定义自验证模型]

7. 模型验证的最佳实践

7.1 结合使用多种验证技术

在实际开发中,很少只使用一种验证技术,通常需要结合多种验证技术来满足不同的需求。例如,对于基本的属性级验证,可以使用元数据指定验证规则;对于复杂的业务逻辑验证,可以在动作方法中进行显式验证;为了提供即时的用户反馈,可以使用客户端验证;对于需要服务器端数据的验证,可以使用远程验证。

7.2 确保服务器端验证的完整性

虽然客户端验证可以提供即时的反馈,但不能替代服务器端验证。因为客户端验证可以被绕过,所以必须在服务器端进行完整的验证,以确保数据的安全性和完整性。

7.3 提高验证代码的可维护性

将验证逻辑封装在独立的类或方法中,避免在动作方法中编写大量的验证代码。使用自定义验证属性和验证提供程序来提高代码的复用性和可维护性。

7.4 优化客户端验证性能

合理使用客户端验证规则,避免过多的规则导致页面加载缓慢。可以使用CDN加载JavaScript库,减少网络传输时间。同时,注意jQuery文件的引用顺序,确保客户端验证正常工作。

7.5 考虑用户体验

在验证过程中,要考虑用户体验,提供清晰的错误提示信息,让用户能够轻松理解问题所在并进行纠正。例如,使用 Html.ValidationSummary Html.ValidationMessageFor 助手方法显示验证错误消息。

8. 常见问题及解决方案

8.1 复选框样式问题

  • 问题描述 :一些浏览器(如Chrome和Firefox)会忽略应用于复选框的样式,导致视觉反馈不一致。
  • 解决方案 :在 ~/Views/Shared/EditorTemplates/Boolean.cshtml 中创建自定义模板,并将复选框包装在 div 元素中。示例模板如下:
@model bool?

@if (ViewData.ModelMetadata.IsNullableValueType) {
    @Html.DropDownListFor(m => m, new SelectList(new [] {"Not Set", "True", "False"}, Model))
} else {
    ModelState state = ViewData.ModelState[ViewData.ModelMetadata.PropertyName];
    bool value = Model ?? false;

    if (state != null && state.Errors.Count > 0) {
        <div class="input-validation-error" style="float:left">
            @Html.CheckBox("", value)
        </div>
    } else {
        @Html.CheckBox("", value)        
    }
}

8.2 远程验证参数问题

  • 问题描述 :如果在远程验证的动作方法中使用模型绑定将参数转换为特定类型,当用户输入无效值时,验证方法可能不会被调用,导致错误信息无法显示。
  • 解决方案 :在动作方法中接受字符串参数,并显式地进行类型转换、解析或模型绑定。例如:
public JsonResult ValidateDate(string Date) {
    DateTime parsedDate;
    if (!DateTime.TryParse(Date, out parsedDate)) {
        return Json("Please enter a valid date (mm/dd/yyyy)",
            JsonRequestBehavior.AllowGet);
    } else if (DateTime.Now > parsedDate) {
        return Json("Please enter a date in the future", JsonRequestBehavior.AllowGet);
    } else {
        return Json(true, JsonRequestBehavior.AllowGet);
    }
}

8.3 客户端验证不生效问题

  • 问题描述 :启用客户端验证后,验证规则未生效。
  • 解决方案 :检查以下几点:
    1. 确保 Web.config 文件中的 ClientValidationEnabled UnobtrusiveJavaScriptEnabled 设置为 true
    2. 确保在 Global.asax 或视图中正确设置了客户端验证相关的属性。
    3. 确保引用了正确的JavaScript库,并且引用顺序正确。示例代码如下:
<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" 
        type="text/javascript"></script>

    <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" 
        type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" 
        type="text/javascript"></script>
</head>
<body>
    @RenderBody()
</body>
</html>

9. 总结

模型验证在Web应用开发中起着至关重要的作用,它不仅可以确保用户输入的数据符合业务规则和数据模型的要求,还可以提高用户体验,维护应用程序和领域模型的完整性。通过本文介绍的多种验证技术,如显式验证模型、在模型绑定器中执行验证、使用元数据指定验证规则等,我们可以根据具体的业务需求和场景选择合适的验证方法。

同时,为了提高开发效率和代码的可维护性,我们可以结合使用多种验证技术,遵循最佳实践,并注意解决常见问题。在实际开发中,要不断总结经验,灵活运用这些验证技术,以构建更加健壮、安全和用户友好的Web应用程序。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值