作者:Adam Freeman
翻译:陈广
日期:2018-10-22
在上一章中,我向您展示了 MVC 是如何通过模型绑定过程从 HTTP 请求创建模型对象的。在这一章中,我工作的基础是用户提供的数据是有效的。事实上,用户通常会输入无效且不能使用的数据,这就引出了本章的主题:模型验证。
模型验证是确保应用程序接收的数据适合绑定到模型的过程,如果情况并非如此,则向用户提供有助于解释问题的有用信息。
过程的第一部分是检查接收到的数据,它是保持域模型完整性的关键方法之一。拒绝域的 context 中没有意义的数据可以防止应用程序中出现奇怪和不需要的状态。第二部分,帮助用户纠正问题同样重要。如果没有与应用程序交互所需的信息和反馈,用户就会感到沮丧和困惑。在面向公共的应用程序中,这意味着用户将停止使用该应用程序。在企业应用程序中,这意味着用户的工作流程将受到阻碍。这两种结果都不可取,但幸运的是,MVC 为模型验证提供了广泛的支持。表27-1为模型验证简历。
表 27-1:模型验证简历
问题 | 回答 |
---|---|
它是什么? | 模型验证是确保请求中提供的数据在应用程序中有效使用的过程。 |
它有何用途? | 用户并不总是输入有效的数据,在应用程序中使用它会产生意想不到的错误。 |
如何使用它? | 控制器检查验证过程的结果,标签助手用于在显示给用户的视图中包含验证反馈。验证是在模型绑定过程中自动执行的,并且通常在控制器类中或通过使用验证特性来补充自定义验证。 |
是否有任何缺陷或限制? | 重要的是测试验证代码的有效性,以确保它可以防止应用程序接收到的所有值。 |
有没有其他选择? | 没有,模型验证紧密集成到了 ASP.NET Core MVC 中。 |
表 27-2:本章摘要
问题 | 解决方案 | 清单 |
---|---|---|
显式验证模型 | 使用ModelState 记录验证错误 |
9-10 |
生成验证错误摘要 | 对div 元素使用asp-validation-summary 属性 |
11 |
更改默认模型绑定消息 | 在模型绑定消息提供器中重新定义消息函数 | 12 |
生成属性级别验证错误 | 对span 元素使用asp-validation-for 属性 |
13 |
生成模型级别验证错误 | 使用ModelState 记录与特定属性无关的验证错误,并在div 元素中使用asp-validation-summary 属性的ModelOnly 值。 |
14,15 |
定义自验证模型 | 将数据验证特性应用于模型属性 | 16,17 |
创建自定义验证特性 | 实现IModelValidator 接口 |
18-19 |
执行客户端验证 | 使用 jQuery 验证和 jQuery unobtrusive 验证包 | 20,21 |
执行删除验证 | 定义一个 action 方法来执行验证,并将Remote 属性应用于模型属性。 |
22,23 |
在本章中,我使用【ASP.NET Core Web 应用程序(.NET core)】模板创建了一个名为 ModelValidation 的新的空项目。清单27-1显示了Startup
类,其中我添加了 MVC 框架,并启用了对开发有用的中间件组件。
清单 27-1:ModelValidation 文件夹下的 Startup.cs 文件的内容
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace ModelValidation
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseStatusCodePages();
app.UseDeveloperExceptionPage();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
}
}
我创建了 Models 文件夹,向其中添加了一个名为 Appointment.cs 的类文件,并用它定义了如清单27-2所示的类。
清单 27-2:Models 文件夹下的 Appointment.cs 文件的内容
using System;
using System.ComponentModel.DataAnnotations;
namespace ModelValidation.Models
{
public class Appointment
{
public string ClientName { get; set; }
[UIHint("Date")]
public DateTime Date { get; set; }
public bool TermsAccepted { get; set; }
}
}
Appointment
模型类定义了三个属性,我使用了UIHint
特性来指示Date
属性应该表示为没有时间组件的日期。
我创建了 Controllers 文件夹,添加了一个名为 HomeController.cs 的类文件,并使用它定义了清单27-3所示的控制器,它对Appointment
模型类进行操作。
清单 27-3:Controllers 文件夹下的 HomeController.cs 文件的内容
using System;
using Microsoft.AspNetCore.Mvc;
using ModelValidation.Models;
namespace ModelValidation.Controllers
{
public class HomeController : Controller {
public IActionResult Index() =>
View("MakeBooking", new Appointment { Date = DateTime.Now });
[HttpPost]
public ViewResult MakeBooking(Appointment appt) =>
View("Completed", appt);
}
}
Index
action 使用一个新的Appointment
对象作为视图模型来渲染 MakeBooking 视图。在本章中,MakeBooking
action 方法更有趣,因为这是执行模型验证的方法。
注意:示例应用程序非常简单,我没有定义存储库,也不需要添加任何代码来存储由模型绑定过程生成的
Appointment
对象。尽管如此,重要的是要牢记,使用验证模型的主要原因是防止坏的或无意义的数据被放置在存储库中并导致问题(无论是在试图存储数据时还是在之后试图处理数据时)。
对于本章中的一些示例,我需要一个简单的布局。我创建了 Views/Shared 文件夹,并向其添加了 _Layout.cshtml 文件,您可以在清单27-4中看到该文件的内容。
清单 27-4:Views/Shared 文件夹下的 _Layout.cshtml 文件的内容
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Model Validation</title>
<link asp-href-include="/lib/twitter-bootstrap/css/bootstrap.min.css" rel="stylesheet" />
@RenderSection("scripts", false)
</head>
<body class="m-1 p-1">
@RenderBody()
</body>
</html>
为了向 action 方法提供视图,我创建了 Views/Home 文件夹,并添加了一个名为 MakeBooking.cshtml 的文件,其标记如清单27-5所示。
清单 27-5:Views/Home 文件夹下的 MakeBooking.cshtml 文件的内容
@model Appointment
@{ Layout = "_Layout"; }
<div class="bg-primary m-1 p-1 text-white"><h2>Book an Appointment</h2></div>
<form class="m-1 p-1" asp-action="MakeBooking" method="post">
<div class="form-group">
<label asp-for="ClientName">Your name:</label>
<input asp-for="ClientName" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Date">Appointment Date:</label>
<input asp-for="Date" type="text" asp-format="{0:d}" class="form-control" />
</div>
<div class="radio form-group">
<input asp-for="TermsAccepted" />
<label asp-for="TermsAccepted" class="form-check-label">
I accept the terms & conditions
</label>
</div>
<button type="submit" class="btn btn-primary">Make Booking</button>
</form>
当 Index.cshtml 文件中包含的表单被回发到应用程序时,MakeBooking
action 方法将显示用户在 Views/Home 文件夹中使用 Completed.cshtml 视图创建的约会的详细信息,如清单27-6所示。
清单 27-6:Views/Home 文件夹下的 Completed.cshtml 文件的内容
@model Appointment
@{ Layout = "_Layout"; }
<div class="bg-success m-1 p-1 text-white"><h2>Your Appointment</h2></div>
<table class="table table-bordered">
<tr>
<th>Your name is:</th>
<td>@Model.ClientName</td>
</tr>
<tr>
<th>Your appontment date is:</th>
<td>@Model.Date.ToString("d")</td>
</tr>
</table>
<a class="btn btn-success" asp-action="Index">Make Another Appointment</a>
视图依赖于 Bootstrap CSS 包来对 HTML 元素进行样式化。要将 Bootstrap 添加到项目中,我在 MvcModels 项目中单击鼠标右键,在弹出菜单中选择【添加】➤【添加客户端库】,并将 twitter-bootstrap 添加至项目中。最终生成的 libman.json 配置文件代码清单27-6所示: 清单 27-6:ModelValidation 文件夹下的 libman.json 文件,添加包
{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": [
{
"library": "twitter-bootstrap@4.1.3",
"destination": "wwwroot/lib/twitter-bootstrap/"
}
]
}
最后的准备工作是在 Views 文件夹中创建 _ViewImports.cshtml 文件,该文件设置内置标签助手,以便在 Razor 视图中使用,并导入模型命名空间,如清单27-8所示。
清单 27-8:Views 文件夹下的 _ViewImports.cshtml 文件的内容
@using ModelValidation.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
正如您可能已经收集到的,本章的示例基于创建约会。通过启动应用程序并请求默认的 URL,您可以看到它是如何工作的。在表单中输入详细信息,然后单击【Make Booking】按钮,将数据发送到服务器,服务器执行模型绑定过程以创建Appointment
对象,然后使用 Completed.cshtml 视图渲染该对象的详细信息,如图27-1所示。
模型验证是应用程序对其从客户端接收的数据强制执行要求的过程。在没有验证的情况下,应用程序将尝试对它接收到的任何数据进行操作,这会导致异常和意外行为,它们会在存储库中填充了坏的、不完整的或恶意的数据时逐立即或长期出现问题。
目前,示例应用程序将接受用户提交的任何数据。为了维护应用程序和域模型的完整性,在我知道用户提供了一个可接受的约会对象之前,需要以下三件事情是正确的:
在接下来的部分中,我将演示如何使用模型验证来执行这些需求,方法是检查应用程序接收到的数据,并在应用程序不能使用用户提交的数据时向用户提供反馈。
验证模型的最直接的方法是在 action 方法中这样做。在清单27-9中,我在MakeBooking
action 方法中添加了由Appointment
类定义的每个属性的显式检查。
清单 27-9:Controllers 文件夹下的 HomeController.cs 文件,显式验证模型
using System;
using Microsoft.AspNetCore.Mvc;
using ModelValidation.Models;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace ModelValidation.Controllers
{
public class HomeController : Controller {
public IActionResult Index() =>
View("MakeBooking", new Appointment { Date = DateTime.Now });
[HttpPost]
public ViewResult MakeBooking(Appointment appt)
{
if (string.IsNullOrEmpty(appt.ClientName))
{
ModelState.AddModelError(nameof(appt.ClientName),
"Please enter your name");
}
if (ModelState.GetValidationState("Date")
== ModelValidationState.Valid && DateTime.Now > appt.Date)
{
ModelState.AddModelError(nameof(appt.Date),
"Please enter a date in the future");
}
if (!appt.TermsAccepted)
{
ModelState.AddModelError(nameof(appt.TermsAccepted),
"You must accept the terms");
}
if (ModelState.IsValid)
{
return View("Completed", appt);
}
else
{
return View();
}
}
}
}
我检查模型绑定器分配给参数对象属性的值,并使用从Controller
基类继承的ModelState
属性返回的ModelStateDictionary
对象注册发现的任何错误。
顾名思义,ModelStateDictionary
类是一个字典,用于跟踪模型对象状态的详细信息,重点是验证错误。表27-3描述了最重要的ModelStateDictionary
成员。
表 27-3:选择 ModelStateDictionary 成员
名称 | 描述 |
---|---|
AddModelError(property, message) | 此方法用于记录指定属性的模型验证错误。 |
GetValidationState(property) | 此方法用于确定特定属性是否存在模型验证错误,表示为ModelValidationState 枚举中的值。 |
IsValid | 如果所有模型属性都有效,则此属性返回true ,否则返回false 。 |
作为使用ModelStateDictionary
的一个例子,请考虑ClientName
属性是如何被验证的。
...
if (string.IsNullOrEmpty(appt.ClientName)) {
ModelState.AddModelError(nameof(appt.ClientName), "Please enter your name");
}
...
示例验证目标之一是确保用户为该属性提供一个值,因此我使用静态string.IsNullOrEmpty
方法测试模型绑定过程从请求中提取的属性值。如果ClientName
属性为null
或空字符串,那么我知道验证目标没有实现,使用ModelState.AddModelError
方法注册一个验证错误,指定属性的名称(ClientName
)和一条将显示给用户的消息,以解释问题的本质(Please enter your name
)。
模型绑定系统还使用ModelStateDictionary
来记录在为模型属性查找和赋值时出现的任何问题。GetValidationState
方法用于查看模型属性是否有任何错误记录,要么来自模型绑定过程,要么是因为在 action 方法的显式验证期间调用了AddModelError
方法。GetValidationState
方法从ModelValidationState
枚举返回一个值,该枚举定义表27-4中描述的值。
表 27-4:ModelValidationState 值
名称 | 描述 |
---|---|
Unvalidated | 此值意味着没有对模型属性执行验证,通常是因为请求中没有与属性名称相对应的值。 |
Valid | 此值意味着与该属性关联的请求值是有效的。 |
Invalid | 此值意味着与属性关联的请求值无效,不应使用。 |
Skipped | 此值意味着模型属性未被处理,这通常意味着存在太多验证错误,因此没有必要继续执行验证检查。 |
对于Date
属性,我检查模型绑定过程是否报告了将浏览器发送的值解析为DateTime
对象的问题,如下所示:
...
if (ModelState.GetValidationState("Date") == ModelValidationState.Valid
&& DateTime.Now > appt.Date)
{
ModelState.AddModelError(nameof(appt.Date), "Please enter a date in the future");
}
...
我对Date
属性的验证目标是确保用户提供一个有效的未来日期。我使用GetValidationState
方法查看模型绑定过程是否能够通过检查ModelValidationState.Valid
值将请求值解析为DateTime
对象。如果有一个有效的日期,那么我检查以确保它是未来的日期,如果不是的话,则使用AddModelError
方法来记录一个验证问题。
在验证了模型对象中的所有属性之后,我检查了ModelState.IsValid
属性,以查看是否存在错误。如果在检查期间调用了Model.State.AddModelError
方法,或者模型绑定器在创建Appointment
对象时遇到任何问题,则此方法将返回true
。
...
if (ModelState.IsValid)
{
return View("Completed", appt);
}
else
{
return View();
}
...
如果IsValid
属性返回true
,则Appointment
对象是有效的,在这种情况下,action 方法将渲染 Completed.cshtml 视图。如果IsValue
属性返回false
,则存在验证问题,通过调用View
方法来渲染默认视图来处理此问题。
通过调用View
方法来处理验证错误似乎很奇怪,但是 MVC 提供给视图的 context 数据包含模型验证错误的详细信息,该模型验证错误由用于转换input
元素的标签助手自动检测和使用。
要了解这是如何工作的,启动应用程序,不填写任何表单细节的情况下,单击【Make Booking】按钮。浏览器窗口中不会显示任何可见的更改,但是如果检查 MVC 从 POST 请求返回的 HTML,您将看到表单元素的class
属性发生了变化。以下是提交表单之前ClientName
元素的样子:
<input class="form-control" type="text" id="ClientName" name="ClientName" value="">
下面是在提交空表单时发送的 input 元素:
<input class="form-control input-validation-error" type="text" id="ClientName"
name="ClientName" value="">
标签助手将其值已验证失败的元素添加到input-validation-error
类,然后将其样式设置为向用户突出显示问题。
您可以通过在样式表中定义自定义 CSS 样式来实现这一点,但是如果您想要使用 CSS 库(如 Bootstrap)提供的内置验证样式,则需要做一些额外的工作。添加到表单元素中的类的名称不能更改,这意味着需要一些 JavaScript 代码在 MVC 使用的名称和 Bootstrap 提供的 CSS 错误类之间进行映射。
提示:使用这样的 JavaScript 代码可能会很尴尬,而且使用自定义的 CSS 样式也很诱人,即使在使用 Bootstrap 之类的 CSS 库时也是如此。但是,可以通过使用主题或自定义包以及定义自己的样式来覆盖 Bootstrap 中用于验证类的颜色,这意味着您必须确保对主题的任何更改都与您定义的任何自定义样式的相应更改相匹配。理想情况下,Microsoft 将使验证类名在 ASP.NET Core MVC 的未来版本中可配置,但在此之前,使用 JavaScript 应用 Bootstrap 样式比创建自定义样式表更健壮。
在清单27-10中,我在MakeBooking
视图中添加了 jQuery 代码,以查找input-validation-error
类中的元素,找到分配给form-group
类的最接近的父类,并将该元素添加到has-danger
类(Bootstrap 用来为表单元素设置错误颜色)。
清单 27-10:Views/Home 文件夹下的 MakeBooking.cshtml 文件,分配验证类
@model Appointment
@{ Layout = "_Layout"; }
@section scripts {
<script src="/lib/jquery/dist/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function () {
$("input.input-validation-error")
.closest(".form-group").addClass("has-danger");
});
</script>
}
<div class="bg-primary m-1 p-1 text-white"><h2>Book an Appointment</h2></div>
<form class="m-1 p-1" asp-action="MakeBooking" method="post">
<div class="form-group">
<label asp-for="ClientName">Your name:</label>
<input asp-for="ClientName" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Date">Appointment Date:</label>
<input asp-for="Date" type="text" asp-format="{0:d}" class="form-control" />
</div>
<div class="radio form-group">
<input asp-for="TermsAccepted" />
<label asp-for="TermsAccepted" class="form-check-label">
I accept the terms & conditions
</label>
</div>
<button type="submit" class="btn btn-primary">Make Booking</button>
</form>
jQuery 代码在浏览器解析完 HTML 文档中的所有元素后运行,其效果是突出显示分配给input-validaton-error
类的input
元素。您可以通过运行应用程序,在不填写任何字段的情况下提交表单而来看到效果,如图27-2所示。
当您提交表单而不输入任何数据时,所有三个属性的错误都会被高亮显示。在表单提交到模型浏览器可以解析的数据并通过MakeBooking
action 方法中的显式验证检查之前,不会向用户显示 Completed.cshtml 视图。在此之前,提交表单将导致 MakeBooking.cshtml 视图与当前验证错误一起渲染。
标签助手应用于input
元素的 CSS 类表明表单字段存在问题,但它们没有告诉用户问题所在。向用户提供更多信息需要使用不同的标签助手,它将问题摘要添加到视图中,如清单27-11所示。
清单 27-11:Views/Home 文件夹下的 MakeBooking.cshtml 文件,显示摘要
@model Appointment
@{ Layout = "_Layout"; }
@section scripts {
<script src="/lib/jquery/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function () {
$("input.input-validation-error")
.closest(".form-group").addClass("has-danger");
});
</script>
}
<div class="bg-primary m-1 p-1 text-white"><h2>Book an Appointment</h2></div>
<form class="m-1 p-1" asp-action="MakeBooking" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="ClientName">Your name:</label>
<input asp-for="ClientName" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Date">Appointment Date:</label>
<input asp-for="Date" type="text" asp-format="{0:d}" class="form-control" />
</div>
<div class="radio form-group">
<input asp-for="TermsAccepted" />
<label asp-for="TermsAccepted" class="form-check-label">
I accept the terms & conditions
</label>
</div>
<button type="submit" class="btn btn-primary">Make Booking</button>
</form>
ValidationSummaryTagHelper
类检测div
元素上的asp-validation-summary
属性,并通过添加消息进行响应,这些消息描述了 action 方法检测到的任何验证错误。asp-validation-summary
属性的值来自ValidationSummary
枚举,它定义了表27-5中所示的值,我将在稍后演示。
表 27-5:ValidationSummary 值
名称 | 描述 |
---|---|
All | 此值用于显示已记录的所有验证错误 |
ModelOnly | 此值仅用于显示整个模型的验证错误,不包括为单个属性记录的验证错误,如《显示模型级别消息》一节所述。 |
None | 此值用于禁用标签助手,使其不转换 HTML 元素。 |
如果运行应用程序并在不做任何更改的情况下提交表单,您可以看到标记助手生成的摘要。此示例的文本颜色是由text-danger
Bootstrap 类定义的,它确保文本与用于突出显示文本字段的颜色相匹配,如图27-3所示。
如果查看浏览器接收到的 HTML,您将看到验证消息已作为列表发送,如下所示:
<div class="text-danger validation-summary-errors" data-valmsg-summary="true">
<ul>
<li>Please enter your name</li>
<li>Please enter a date in the future</li>
<li>You must accept the terms</li>
</ul>
</div>
我在第26章中描述的模型绑定过程在试图提供调用 action 方法所需的数据值时执行它自己的验证。要查看这是如何工作的,请启动应用程序,清除约会日期字段的内容,并提交表单。您将看到所显示的验证错误消息之一已经更改,并且与传递给 action 方法中AddModelError
方法的任何字符串不匹配。
The value '' is invalid
当模型绑定过程找不到属性的值或找到值但无法解析时,此消息将被添加到ModelStateDictionary
中。在这种情况下,出现错误是因为表单数据中发送的空字符串无法解析为Appointment
类的Date
属性的DateTime
对象。
模型绑定器有一组用于验证错误的预定义消息。这些消息可以使用DefaultModelBindingMessageProvider
类定义的方法替换为自定义消息,如表27-6所述。
表 27-6:DefaultModelBindingMessageProvider 方法
名称 | 描述 |
---|---|
SetValueMustNotBeNullAccessor | 当模型属性的值为null 时,分配给该属性的函数用于生成验证错误消息。 |
SetMissingBindRequiredValueAccessor | 分配给此属性的函数用于在请求不包含所需属性的值时生成验证错误消息。 |
SetMissingKeyOrValueAccessor | 分配给此属性的函数用于在字典模型对象所需的数据包含空键或值时生成验证错误消息。 |
SetAttemptedValueIsInvalidAccessor | 分配给此属性的函数用于在模型绑定系统无法将数据值转换为所需的 C# 类型时生成验证错误消息。 |
SetUnknownValueIsInvalidAccessor | 分配给此属性的函数用于在模型绑定系统无法将数据值转换为所需的 C# 类型时生成验证错误消息。 |
SetValueMustBeANumberAccessor | 分配给此属性的函数用于在数据值不能解析为 C# 数字类型时生成验证错误消息。 |
SetValueIsInvalidAccessor | 分配给此属性的函数用于生成作为最后手段使用的回退验证错误消息。 |
表中描述的每个方法都接受调用的函数,以便将验证消息显示给用户。在Startup
类中使用这些方法配置应用程序,如清单27-12所示,在清单27-12中,我替换了值为空时显示的默认消息。
清单 27-12:ModelValidation 文件夹下的 Startup.cs 文件,替换绑定消息
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace ModelValidation
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddMvcOptions(opts =>
opts.ModelBindingMessageProvider
.SetValueMustNotBeNullAccessor(value => "Please enter a value")
);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseStatusCodePages();
app.UseDeveloperExceptionPage();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
}
}
您指定的函数接收用户提供的值,尽管这在处理空值时并不特别有用。要查看自定义消息,重新启动应用程序并在清除约会日期字段后提交表单,如图27-4所示。
尽管自定义错误消息比默认错误消息更有意义,但它仍然没有多大帮助,因为它没有向用户明确指出问题所在。对于这种错误,在包含问题数据的 HTML 元素旁边显示验证错误消息更为有用。这可以使用ValidationMessageTag
标签助手来完成,它查找具有asp-validation-for
属性的span
元素,该属性用于指定应该显示错误消息的模型属性。
在清单27-13中,我为表单中的每个input
元素添加了属性级验证消息元素。我还删除了scripts
section,因为单个验证消息将提供足够的高亮显示,以指示哪些元素存在验证错误。
清单 27-13:Views/Home 文件夹下的 MakeBooking.cshtml 文件,添加属性级别消息
@model Appointment
@{ Layout = "_Layout"; }
@section scripts {
<script src="/lib/jquery/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function () {
$("input.input-validation-error")
.closest(".form-group").addClass("has-danger");
});
</script>
}
<div class="bg-primary m-1 p-1 text-white"><h2>Book an Appointment</h2></div>
<form class="m-1 p-1" asp-action="MakeBooking" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="ClientName">Your name:</label>
<div><span asp-validation-for="ClientName" class="text-danger"></span></div>
<input asp-for="ClientName" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Date">Appointment Date:</label>
<div><span asp-validation-for="Date" class="text-danger"></span></div>
<input asp-for="Date" type="text" asp-format="{0:d}" class="form-control" />
</div>
<span asp-validation-for="TermsAccepted" class="text-danger"></span>
<div class="radio form-group">
<input asp-for="TermsAccepted" />
<label asp-for="TermsAccepted" class="form-check-label">
I accept the terms & conditions
</label>
</div>
<button type="submit" class="btn btn-primary">Make Booking</button>
</form>
由于span
元素是内联显示的,注意显示的验证消息,可以发现消息与哪个元素相关是显而易见的。您可以通过运行应用程序和提交表单而不输入任何数据来看到新验证消息的效果,如图27-5所示。
验证摘要消息似乎是多余的,因为它只是重复属性级别的消息,这些消息通常对用户更有帮助,因为它们出现在必须解决问题的表单元素旁边。但是摘要有一个有用的技巧,那就是能够显示应用于整个模型的消息,而不仅仅是单个属性。这意味着您可以报告由单个属性组合而产生的错误,例如,指定日期只有在与特定名称组合时才有效。
在清单27-14中,我添加了一个验证检查,以防止名为Joe
的客户端在星期一预约约会。
清单 27-14:Controllers 文件夹下的 HomeController.cs 文件,执行模型级别验证
using System;
using Microsoft.AspNetCore.Mvc;
using ModelValidation.Models;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace ModelValidation.Controllers
{
public class HomeController : Controller {
public IActionResult Index() =>
View("MakeBooking", new Appointment { Date = DateTime.Now });
[HttpPost]
public ViewResult MakeBooking(Appointment appt)
{
if (string.IsNullOrEmpty(appt.ClientName))
{
ModelState.AddModelError(nameof(appt.ClientName),
"Please enter your name");
}
if (ModelState.GetValidationState("Date")
== ModelValidationState.Valid && DateTime.Now > appt.Date)
{
ModelState.AddModelError(nameof(appt.Date),
"Please enter a date in the future");
}
if (!appt.TermsAccepted)
{
ModelState.AddModelError(nameof(appt.TermsAccepted),
"You must accept the terms");
}
if (ModelState.GetValidationState(nameof(appt.Date))
== ModelValidationState.Valid
&& ModelState.GetValidationState(nameof(appt.ClientName))
== ModelValidationState.Valid
&& appt.ClientName.Equals("Joe", StringComparison.OrdinalIgnoreCase)
&& appt.Date.DayOfWeek == DayOfWeek.Monday)
{
ModelState.AddModelError("",
"Joe cannot book appointments on Mondays");
}
if (ModelState.IsValid)
{
return View("Completed", appt);
}
else
{
return View();
}
}
}
}
这段代码看起来比实际要复杂得多,这是数据验证的本质。在检查指定日期是否为星期一和ClientName
属性是否为Joe
之前,通过检查模型状态以确保收到了有效的ClientName
和Date
值。如果 Joe 试图预订周一约会,那么我使用空字符串("")作为第一个参数调用AddModelError
方法,这表明错误适用于整个模型,而不仅仅是单个属性。
在清单27-15中,我将asp-validation-summary
属性的值更改为ModelOnly
,这排除了属性级别的错误,这意味着摘要将只显示适用于整个模型的错误。
清单 27-15:Views/Home 文件夹下的 MakeBooking.cshtml 文件,显示模型级别错误
@model Appointment
@{ Layout = "_Layout"; }
@section scripts {
<script src="/lib/jquery/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function () {
$("input.input-validation-error")
.closest(".form-group").addClass("has-danger");
});
</script>
}
<div class="bg-primary m-1 p-1 text-white"><h2>Book an Appointment</h2></div>
<form class="m-1 p-1" asp-action="MakeBooking" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="ClientName">Your name:</label>
<div><span asp-validation-for="ClientName" class="text-danger"></span></div>
<input asp-for="ClientName" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Date">Appointment Date:</label>
<div><span asp-validation-for="Date" class="text-danger"></span></div>
<input asp-for="Date" type="text" asp-format="{0:d}" class="form-control" />
</div>
<span asp-validation-for="TermsAccepted" class="text-danger"></span>
<div class="radio form-group">
<input asp-for="TermsAccepted" />
<label asp-for="TermsAccepted" class="form-check-label">
I accept the terms & conditions
</label>
</div>
<button type="submit" class="btn btn-primary">Make Booking</button>
</form>
您可以通过运行应用程序,在ClientName
字段中输入Joe
,并选择一个您知道为周一的日期,例如 01/18/2027。当提交表单时,您将看到如图27-6所示的响应。
将验证逻辑放入 action 方法中的一个问题是,在接收用户数据的每个 action 方法中都会复制验证逻辑。为了帮助减少重复,验证过程支持使用特性直接在模型类中表示模型验证规则,确保无论使用哪种 action 方法处理请求,都将应用相同的验证规则集。
在清单27-16中,我已将特性应用到Appointment
类,以执行在上一节中使用的相同的属性级验证规则集。
清单 27-16:Models 文件夹下的 Appointment.cs 文件,应用验证特性
using System;
using System.ComponentModel.DataAnnotations;
namespace ModelValidation.Models
{
public class Appointment
{
[Required]
[Display(Name = "name")]
public string ClientName { get; set; }
[UIHint("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
特性指定可接受值的子集。表27-7显示了 MVC 应用程序中可用的内置验证特性集。
表 27-7:内置验证特性
特性 | 示例 | 描述 |
---|---|---|
Compare | [Compare("OtherProperty")] | 此特性确保属性必须具有相同的值,当您要求用户两次提供相同的信息(例如电子邮件地址或密码)时,此值非常有用。 |
Range | [Range(10, 20)] | 此特性确保数值(或实现IComparable 的任何属性类型)不超过指定的最小值和最大值。若要仅在一侧指定边界,请使用MinValue 或MaxValue 常量 —— 例如,[Range(int.minvalue,50)] 。 |
RegularExpression | [RegularExpression("pattern")] | 此特性确保字符串值与指定的正则表达式模式匹配。请注意,模式必须匹配整个用户提供的值,而不仅仅是其中的一个子字符串。默认情况下,它大小写敏感,但可以通过应用(?i )修饰符(即[RegularExpression("(?i)mypattern")] )使其不区分大小写。 |
Required | [Required] | 此属性确保该值不为空或仅由空格组成的字符串。如果要将空白视为有效,请使用[Required(AllowEmptyStrings= true)] 。 |
StringLength | [StringLength(10)] | 此属性确保字符串值不超过指定的最大长度。您还可以指定最小长度:[StringLength(10,MinimumLength=2)] 。 |
所有验证特性都支持通过为ErrorMessage
属性设置一个值来指定自定义错误消息,如下所示:
...
[UIHint("Date")]
[Required(ErrorMessage = "Please enter a date")]
public DateTime Date { get; set; }
...
如果没有自定义错误消息,则将使用默认消息,但它们往往会显示模型类的详细信息,除非您还使用Display
特性(即我应用于ClientName
属性的组合),否则对用户没有任何意义。
...
[Required]
[Display(Name = "name")]
public string ClientName { get; set; }
...
Required
特性生成的默认消息反映了使用Display
属性指定的名称,因此不会向用户透露属性的名称。
要使这类验证一致地工作,需要注意一些地方。例如,考虑将此特性应用于TermsAccepted
属性:
...
[Range(typeof(bool), "true", "true", ErrorMessage="You must accept the terms")]
public bool TermsAccepted { get; set; }
...
我想确保用户选择接受条件的复选框。不能使用Required
特性,因为如果用户没有选择单选按钮,浏览器将为该属性发送一个false
值。为解决这个问题,我使用了Range
特性的一个功能,它允许我提供一个类型,并将上界和下界指定为字符串值。通过将两个边界设置为true
,我为使用复选框编辑的bool
属性创建了Required
特性的等效属性。需要进行一些实验,以确保验证特性和浏览器发送的数据一起工作。
在模型类上使用验证特意味着可以简化控制器中的 action 方法,如清单27-17所示。
清单 27-17:Controllers 文件夹下的 HomeController.cs 文件,移除属性级别验证
using System;
using Microsoft.AspNetCore.Mvc;
using ModelValidation.Models;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace ModelValidation.Controllers
{
public class HomeController : Controller {
public IActionResult Index() =>
View("MakeBooking", new Appointment { Date = DateTime.Now });
[HttpPost]
public ViewResult MakeBooking(Appointment appt)
{
if (ModelState.GetValidationState(nameof(appt.Date))
== ModelValidationState.Valid
&& ModelState.GetValidationState(nameof(appt.ClientName))
== ModelValidationState.Valid
&& appt.ClientName.Equals("Joe", StringComparison.OrdinalIgnoreCase)
&& appt.Date.DayOfWeek == DayOfWeek.Monday)
{
ModelState.AddModelError("",
"Joe cannot book appointments on Mondays");
}
if (ModelState.IsValid)
{
return View("Completed", appt);
}
else
{
return View();
}
}
}
}
验证特性是在调用 action 方法之前应用的,这意味着在执行模型级验证时,我仍然可以依赖于模型状态来确定单个属性是否有效。要查看 action 中的验证特性,启动应用程序并在不输入任何数据的情况下提交表单,如图27-7所示。
验证过程可以通过创建一个实现IModelValidator
接口的特性来扩展。为了演示,我创建了一个 Infrastructure 文件夹,并向它添加了一个名为 MustBeTrueAttribute.cs 的类文件,在其中我定义了清单27-18中所示的类。
清单 27-18:Infrastructure 文件夹下的 MustBeTrueAttribute.cs 文件的内容
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
namespace ModelValidation.Infrastructure
{
public class MustBeTrueAttribute : Attribute, IModelValidator
{
public bool IsRequired => true;
public string ErrorMessage { get; set; } = "This value must be true";
public IEnumerable<ModelValidationResult> Validate(
ModelValidationContext context)
{
bool? value = context.Model as bool?;
if (!value.HasValue || value.Value == false)
{
return new List<ModelValidationResult> {
new ModelValidationResult("", ErrorMessage)
};
}
else
{
return Enumerable.Empty<ModelValidationResult>();
}
}
}
}
IModelValidator
接口定义了一个IsRequired
属性,用于指示是否需要由该类进行验证(这有点误导,因为该属性返回的值只是用于排序验证属性,以便首先执行所需的属性)。Validate
方法用于执行验证,并通过ModelValidationContext
类的实例接收信息,表27-8描述了该类最有用的属性。
表 27-8:ModelValidationContext 类的有用属性
名称 | 描述 |
---|---|
Model | 此属性返回要验证的属性值,即示例中TermsAccepted 属性的值。 |
Container | 此属性返回包含该属性的对象,本例为Appointment 对象。 |
ActionContext | 此属性返回一个ActionContext 对象,它提供上下文数据并描述将处理请求的 action 方法。 |
ModelMetadata | 此属性返回一个ModelMetadata 对象,描述正在详细验证的模型类。 |
验证方法返回一系列ModelValidationResult
对象,每个对象描述一个验证错误。在示例属性中,如果模型值不正确,则创建一个ModelValidationResult
。ModelValidationResult
构造函数的第一个参数是与错误相关联的属性的名称,并在验证单个属性时指定为空字符串。第二个参数是将显示给用户的错误消息。在清单27-19中,我用自定义特性替换了Range
属性。
清单 27-19:Models 文件夹下的 Appointment.cs 文件,应用自定义验证特性
using System;
using System.ComponentModel.DataAnnotations;
using ModelValidation.Infrastructure;
namespace ModelValidation.Models
{
public class Appointment
{
[Required]
[Display(Name = "name")]
public string ClientName { get; set; }
[UIHint("Date")]
[Required(ErrorMessage = "Please enter a date")]
public DateTime Date { get; set; }
[MustBeTrue(ErrorMessage = "You must accept the terms")]
public bool TermsAccepted { get; set; }
}
}
使用自定义验证特性的结果与使用Range
特性相同,但在读取代码时,自定义特性的用途更为明显。
到目前为止,我演示的验证技术都是服务器端验证的示例。这意味着用户向服务器提交数据,服务器验证数据并发回验证结果(要么成功地处理数据,要么需要更正错误列表)。
在 Web 应用程序中,用户通常期望立即获得验证反馈 —— 无需向服务器提交任何内容。这被称为客户端验证,并使用 JavaScript 实现。用户输入的数据在发送到服务器之前是经过验证的,为用户提供了即时反馈和纠正任何问题的机会。
MVC 支持非突出的客户端验证。“非突出”一词意味着使用添加到视图生成的 HTML 元素的属性来表示验证规则。这些属性由 JavaScript 库解释,JavaScript 库是 MVC 的一部分,而 MVC 反过来配置 jQuery 验证库,jQuery 验证库执行实际的验证工作。在下面的部分中,我将向您展示内置验证支持是如何工作的,并演示如何扩展该功能以提供自定义客户端验证。
提示:客户端验证的重点是验证单个属性。实际上,使用 MVC 附带的内置支持来建立模型级别的客户端验证是很困难的。为此,大多数 MVC 应用程序使用客户端验证来解决属性级别的问题,并依赖服务器端验证来实现整个模型。
第一步是使用 LibMan 向应用程序添加新的 JavaScript 包,如清单27-20所示。
清单 27-20:ModelValidation 文件夹下的 libman.json 文件,添加包
{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": [
{
"library": "twitter-bootstrap@4.1.3",
"destination": "wwwroot/lib/twitter-bootstrap/"
},
{
"library": "jquery@3.3.1",
"destination": "wwwroot/lib/jquery/"
},
{
"library": "jquery-validate@1.17.0",
"destination": "wwwroot/lib/jquery-validate/"
},
{
"provider": "cdnjs",
"library": "jquery-validation-unobtrusive@3.2.11",
"destination": "wwwroot/lib/jquery-validation-unobtrusive/"
}
]
}
使用客户端验证意味着向视图中添加三个 JavaScript 文件:jQuery 库、jQuery 验证库和 Microsoft 非突出验证库,如清单27-21所示。
清单 27-21:Views/Home 文件夹下的 MakeBooking.cshtml 文件,添加 JavaScript 元素
@model Appointment
@{ Layout = "_Layout"; }
@section scripts {
<script src="/lib/jquery/jquery.min.js"></script>
<script src="/lib/jquery-validate/jquery.validate.min.js"></script>
<script src="/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js">
</script>
}
<div class="bg-primary m-1 p-1 text-white"><h2>Book an Appointment</h2></div>
<form class="m-1 p-1" asp-action="MakeBooking" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="ClientName">Your name:</label>
<div><span asp-validation-for="ClientName" class="text-danger"></span></div>
<input asp-for="ClientName" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Date">Appointment Date:</label>
<div><span asp-validation-for="Date" class="text-danger"></span></div>
<input asp-for="Date" type="text" asp-format="{0:d}" class="form-control" />
</div>
<span asp-validation-for="TermsAccepted" class="text-danger"></span>
<div class="radio form-group">
<input asp-for="TermsAccepted" />
<label asp-for="TermsAccepted" class="form-check-label">
I accept the terms & conditions
</label>
</div>
<button type="submit" class="btn btn-primary">Make Booking</button>
</form>
必须按照显示的顺序添加文件。当标签助手转换input
元素时,它们检查应用于模型类属性的验证特性,并向输出元素添加属性。如果运行应用程序并检查发送到浏览器的 HTML,您将看到如下元素:
<input class="form-control" type="text" data-val="true"
data-val-required="The name field is required." id="ClientName"
name="ClientName" value="" />
JavaScript 代码查找带有data-val
属性的元素,并在用户提交表单时在浏览器中执行本地验证,而不向服务器发送 HTTP 请求。您可以通过运行应用程序和提交表单,同时使用 F12 工具,注意到即使没有向服务器发送 HTTP 请求,也会显示验证错误消息。
避免与浏览器验证冲突
一些 HTML 5 浏览器支持基于应用于
input
元素的属性的简单客户端验证。一般的想法是,比方说,当用户试图提交表单而不提供值时,例如,应用了required
属性的input
元素将导致浏览器显示验证错误。如果您正在从模型中生成表单元素(正如我在本章中所做的那样),那么您将不会遇到浏览器验证方面的任何问题,因为 MVC 生成并使用数据属性来表示验证规则(例如,必须具有值的
input
元素被表示为data-val-required
属性,浏览器不识别该属性)。但是,如果无法完全控制应用程序中的标记,则可能会遇到问题,在传递其他地方生成的内容时经常会发生这种情况。结果是 jQuery 验证和浏览器验证都可以在表单上操作,这对用户来说是很混乱的。为了避免这个问题,您可以向
form
元素添加novalidate
属性。
MVC 客户端验证的一个好特性是,用于指定验证规则的相同特性应用于客户端和服务器。这意味着来自不支持 JavaScript 的浏览器的数据将受到支持 JavaScript 的浏览器相同的验证,而不需要任何额外的工作。但是,这确实意味着客户端验证不支持自定义验证特性,因为 JavaScript 代码无法在客户端实现自定义逻辑。换句话说,如果您想使用客户端验证,则需要保留表27-7中描述的内置特性。
MVC 客户端验证与 jQuery 验证
MVC 客户端验证功能构建在 jQuery 验证库之上。如果您愿意,可以直接使用验证库而忽略 MVC 功能。验证库是灵活和功能丰富的。如果只是了解如何定制 MVC 特性,以最大限度地利用可用的验证选项,这是非常值得探索的。我在我的专著《Pro jQuery 2.0》中深入介绍了 jQuery 验证库,这本书也是由 Apress 出版的。
本章中描述的最后一个验证特性是远程验证。这是一种客户端验证技术,它调用服务器上的 action 方法来执行验证。
远程验证的一个常见示例是在用户提交数据并执行客户端验证时,检查用户名在应用程序中是否可用,这些名称必须是唯一的。作为此过程的一部分,会向服务器发出 Ajax 请求,以验证所请求的用户名。如果用户名已被获取,则会显示一个验证错误,以便用户可以输入另一个值。
这看起来像是常规的服务器端验证,但是这种方法有一些好处。首先,只有一些属性将被远程验证;客户端验证的好处仍然适用于用户输入的所有其他数据值。第二,请求相对轻量级,并且侧重于验证,而不是处理整个模型对象。
第三个区别是远程验证是在后台执行的。用户不必单击提交按钮,然后等待渲染和返回新视图。它使用户体验更有响应性,特别是当浏览器和服务器之间存在缓慢的网络时。
也就是说,远程验证是一种妥协。它在客户端验证和服务器端验证之间取得了平衡,但它确实需要向应用程序服务器请求,而且验证速度也不如正常的客户端验证。
使用远程验证的第一步是创建一个可以验证模型属性之一的 action 方法。我将验证Appointment
模型的Date
属性,以确保所请求的约会是将来的。(这是我在本章开头使用的原始验证规则之一,但不可能使用标准客户端验证功能进行验证)。清单27-22显示了在 HOme 控制器中添加一个ValidateDate
action 方法。
清单 27-22:Controllers 文件夹下的 HomeController.cs 文件,添加一个 Action
using System;
using Microsoft.AspNetCore.Mvc;
using ModelValidation.Models;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace ModelValidation.Controllers
{
public class HomeController : Controller {
public IActionResult Index() =>
View("MakeBooking", new Appointment { Date = DateTime.Now });
[HttpPost]
public ViewResult MakeBooking(Appointment appt)
{
if (ModelState.GetValidationState(nameof(appt.Date))
== ModelValidationState.Valid
&& ModelState.GetValidationState(nameof(appt.ClientName))
== ModelValidationState.Valid
&& appt.ClientName.Equals("Joe", StringComparison.OrdinalIgnoreCase)
&& appt.Date.DayOfWeek == DayOfWeek.Monday)
{
ModelState.AddModelError("",
"Joe cannot book appointments on Mondays");
}
if (ModelState.IsValid)
{
return View("Completed", appt);
}
else
{
return View();
}
}
public JsonResult ValidateDate(string Date)
{
DateTime parsedDate;
if (!DateTime.TryParse(Date, out parsedDate))
{
return Json("Please enter a valid date (mm/dd/yyyy)");
}
else if (DateTime.Now > parsedDate)
{
return Json("Please enter a date in the future");
}
else
{
return Json(true);
}
}
}
}
支持远程验证的 Action 方法必须返回JsonResult
类型,这告诉 MVC 我正在处理 JSON 数据,如第20章所述。除了结果之外,验证 action 方法还必须定义与正在验证的数据字段同名的参数;这是示例的Date
。在 action 方法中,通过将值解析为DateTime
对象并检查它是否在将来执行验证。
提示:我本可以利用模型绑定的优势,使 action 方法的参数成为
DateTime
对象,但这样做意味着,如果用户输入了类似于apple
的无意义值,验证方法就不会被调用。这是因为模型绑定器无法从apple
创建DateTime
对象,并在尝试时抛出异常。远程验证功能没有表达异常的方法,因此它被悄然丢弃。这有一个不幸的后果,即没有突出显示数据字段,从而给用户感觉输入的值是有效的。通常情况下,远程验证的最佳方法是接受 action 方法中的字符串参数,并显式地执行任何类型转换、解析或模型绑定。
我使用Json
方法表示验证结果,该方法创建一个 JSON 格式的结果,客户端远程验证脚本可以解析和处理。如果值有效,则将true
作为参数传递给Json
方法,如下所示:
...
return Json(true);
...
如果出现问题,我将传递验证错误消息,用户应该将其视为参数,如下所示:
...
return Json("Please enter a date in the future");
...
为了使用远程验证方法,我将Remote
特性应用于模型类中的一个属性,如清单27-23所示。
清单 27-23:Models 文件夹下的 Appointment.cs 文件,使用 Remote 特性
using System;
using System.ComponentModel.DataAnnotations;
using ModelValidation.Infrastructure;
using Microsoft.AspNetCore.Mvc;
namespace ModelValidation.Models
{
public class Appointment
{
[Required]
[Display(Name = "name")]
public string ClientName { get; set; }
[UIHint("Date")]
[Required(ErrorMessage = "Please enter a date")]
[Remote("ValidateDate", "Home")]
public DateTime Date { get; set; }
[MustBeTrue(ErrorMessage = "You must accept the terms")]
public bool TermsAccepted { get; set; }
}
}
特性的参数是 action 的名称和控制器,用于生成 JavaScript 验证库将调用的 URL 来执行验证 —— 在本例中,是 Home 控制器上的ValidateDate
操作。
您可以通过启动应用程序、导航到 /Home URL 并输入过去的日期来查看远程验证是如何工作的。当您选择一个值并且焦点移动到另一个元素时,验证消息将出现,如图27-8所示。
在本章中,我研究了可用于执行模型验证的广泛技术,确保用户提供的数据与强加于数据模型的约束一致。模型验证是一个重要的主题,对应用程序进行正确的验证对于确保用户有良好和无挫折感的体验至关重要。在下一章中,我将解释如何使用 ASP.NET Core 身份来保护 MVC 应用程序。
;