65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET MVC 中朴实的 AJAX 表单验证

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (21投票s)

2012年9月17日

CPOL

12分钟阅读

viewsIcon

129630

downloadIcon

3946

一份完整的循序渐进的教程,解释了如何在 ASP.NET MVC 中使用朴实的 AJAX 进行表单验证以及为何要这样做。

介绍 

我从 90 年代中期就开始从事 Web 开发。我最初使用 PERL 和 CGI,但有人向我展示了 Active Server Pages 后,我便全心投入。我喜欢 ASP 相较于 PERL 带来的生产力提升。我变得**高效而强大**。十年后,MVC 和 jQuery 同样令人振奋。约定变得标准化,我可以在框架内快速构建应用程序,更不用说确定哪个 HTML SELECT OPTION 被选中突然变得微不足道了。还能更好吗?嗯,是的。我最近在使用**jQuery 在 ASP.NET MVC 中的朴实扩展**时也有类似的“哇哦”时刻。在弄清楚如何实现它的过程中,我在网上没找到太多清晰的教程,我不确定它**是否显而易见**,所以我想在这里与你分享基础知识。

表单验证

在 Web 应用程序中不进行用户输入验证几乎是不可能的。通过 UI 来约束用户做“正确的事情™”是很好的,但不可避免地,用户会滥用你的表单,你需要确保这不会破坏你的软件。我大力提倡进行**服务器端验证**。虽然有时在客户端进行验证是有充分理由的,但大多数时候,我宁愿将验证放在唯一必须存在的地方:服务器上。

附注:我为什么说验证必须放在服务器端?考虑有人故意攻击你的应用程序。他们不会使用你用 JavaScript 编写的客户端验证。他们会使用某个实用程序进行直接的 HTTP 调用,绕过你在脚本中设置的验证。所以,如果你希望编写安全的应用程序,DRY 原则(Don't Repeat Yourself)和排除法表明**验证应该放在服务器端**。

在“过去”,开发人员编写 FORM 标签和 INPUT 按钮,将 HTTP 请求发送到服务器,当响应回来时,页面会**重新加载**并显示结果。后来出现了(奇怪的)XMLHttpRequest,如果你有耐心编写底层代码,你可以弄清楚如何提交表单(或响应任何 DOM 事件的任意数据),并在页面不重载的情况下接收响应。**AJAX** 和 **jQuery**(以及其他库)极大地改进了该编程模型,但仍然可能需要编写大量的响应处理代码,这通常比必要时更麻烦。

如果你正在进行服务器端验证,并利用 ASP.NET MVC 的部分视图、可测试的控制器、数据注释或自定义验证属性等,而你还没有使用朴实的 JS 方法,那么你将获得惊喜。与手动使用 jQuery 的 **$.ajax()** 相比,这种方法有利有弊,我将在最后讨论,但它对于许多场景来说绝对非常方便,并且是 Web 开发人员工具箱中重要的组成部分。

项目需求

我任意选择了一个项目来演示在一个啤酒厂的账户设置页面中使用朴实的 JS。它将**接受用户的用户名、密码和出生日期**。它将**模拟验证**用户名的唯一性,确保密码安全,并使输入的出生日期用户至少 21 岁。如果任何表单输入无效,用户将收到一些**红色错误文本**,并可以反复尝试直到成功。**页面在设置过程中不会重新加载**,因为这是 21 世纪;所有操作都将通过**AJAX** 完成。此外,开发工作将尽可能高效地进行。

创建 MVC 应用

我正在使用**Visual Studio 2012**,但由于这个项目不需要 MVC 4 的任何功能,我将保持简单并向后兼容 Studio 2010,选择 MVC 3。

  1. 创建一个名为“Microbrewery”的新 **ASP.NET MVC 3 Web 应用程序**,并选择**空白**项目,因为我们想了解所有必需的内容。
  2. 现在你已经有了一个 Web.config、Global.asax、基本的 _Layout.cshtml、一些 CSS 和 JS 文件。

创建模型

虽然我通常会将我的领域模型(业务层的一部分)与我的视图模型(表示层的一部分)分开,但为了简单起见,我将省略这种方法,所以这里只有一个“模型”。*我**不推荐**在生产代码中使用这种便利性。*

  1. 在 Models 下添加一个名为“**Account**”的新类。
  2. 为 Account 添加**字符串**属性 **Username**、**Password**、**ConfirmPassword**,以及 **DateTime** 属性 **BirthDate**。
  3. 将 **System.ComponentModel.DisplayNameAttribute** 应用于 ConfirmPassword 和 BirthDate,以便它们在 UI 中显示得更漂亮。
  4. 将 `System.ComponentModel.DataAnnotations.RequiredAttribute` 应用于每个属性。
  5. 将 `StringLengthAttribute` 应用于每个字符串属性,将最大长度设置为 20,最小长度设置为 4。
  6. 将 `CompareAttribute` 应用于密码字段,让每个字段都指向另一个字段进行相等性比较。
  7. 嗯,如何进行出生日期验证以检查用户是否年满 21 岁?不是的,JavaScript 不是答案。

创建自定义年龄验证属性

确定用户是否年满 21 岁只是简单的日期减法,但它并不像标准数据注释那样常见或简单。幸运的是,创建自定义 ValidationAttribute 非常容易。

  1. 创建一个名为“**Utility**”的项目文件夹。
  2. 在 Utility 中,创建一个名为“`AgeValidationAttribute`”的类,该类继承自 `System.ComponentModel.DataAnnotations.ValidationAttribute`。
  3. 创建一个名为“**MinimumAge**”的**整数**成员,并创建一个**带参数的构造函数**,该构造函数使用指定的值初始化 MinimumAge。
  4. **重写** `IsValid` 方法,该方法接受 object 和 ValidationContext 作为参数。
  5. 在重写的 IsValid 中,从 `DateTime.Today` 计算一个人可能出生的最晚日期,并且该日期也要满足 MinimumAge 指定的年龄。如果他们不够大,则返回一个带有有意义错误消息的 `ValidationResult`。否则返回 null。
  6. 现在回到我们之前的地方,将 `AgeValidationAttribute` 应用于模型中的 BirthDate,并**在构造函数中指定 21**。

创建视图

模型已准备就绪,因此创建一个视图来呈现给用户。同时为用户成功注册时创建一个简单的“谢谢”页面。

  1. 在 Views 下创建一个名为“**Account**”的目录。
  2. 添加一个**Razor 视图**,不指定母版页,因为我们要使用 _ViewStart 和 _Layout.cshtml。勾选将其创建为**部分视图**的复选框,这样就不会包含任何 HTML。指定视图应**强类型**并选择 **Account** 模型类。将视图命名为“**Index**”并添加。
  3. **创建一个 DIV** 来包含页面内容。
  4. 在 DIV 中,通过**using 语句**和 `Html.BeginForm` 以旧方式开始。
  5. 在表单的作用域内,包含一个 `Html.EditorForModel` 和一个 ``。EditorForModel 对于许多生产场景来说可能过于粗糙,但对于演示来说是节省时间的绝佳工具。
  6. 在 Account 下添加一个类似的名为“**Thanks**”的部分视图,并让它在 H2 中输出“Thanks”。

创建控制器

现在创建控制器来处理用户提交表单时的 POST 请求。

  1. 在 Controllers 下创建一个名为“**AccountController**”的控制器。
  2. 默认的 **HttpGet Index** 方法是默认创建的,并且保持原样即可。
  3. 创建一个**重载的 Index** 方法,该方法需要 **HttpPost** 并接受 **Account 模型**作为参数。点击“Submit”将 POST 到这里。
  4. 在接受 POST 的 Index 方法中,如果 `ModelState` 有效,则使用 **View** 方法将用户发送到 **Thanks** 视图。显然,在实际应用程序中,这会从业务层卸载一些工作。
  5. 如果 `ModelState` 无效,则再次使用 **View** 方法将他们发送回 **Index** 部分视图,让他们重新尝试。

第一次运行

我仍然对最先进的技术非常着迷,只需按 F5 键,就能得到一个接受用户输入并进行验证的页面。我感觉如此高效!但是,这部分内容已经存在一段时间了,而且还有更多内容。

这时,我填完了表单,按下了按钮,如果我没有正确填写表单,我会得到一些红色文本,指示需要修复什么。但如今,当 POST 请求通过 Internet 传输时,页面闪烁**看起来很糟糕**。如果事物看起来更像使用客户端验证那样就好了。有些人可能会认为这是**即时性和清晰的呈现**是客户端验证的好处。通过 AJAX,我们可以获得两全其美:美观且设计精良。

逻辑流程

 

流程很简单,应该很熟悉。站点的容器/框架是标准的 **_Layout.cshtml**。每次在 GET 或 POST 上渲染视图时,它都会在 **RenderBody** 期间放置在 _Layout 中,并且**整个 HTML 文档会被返回到客户端**,替换当前页面。

从 HTML 切换到 AJAX

使用朴实的 JS,这是一个令人惊讶的简单更改。几乎所有的繁重工作都会为您完成,并且到目前为止编写的几乎所有代码都保持不变。

  1. 在 Index.cshtml 中,将 `Html.BeginForm` 更改为 `Ajax.BeginForm`。
  2. 你会注意到你需要向 Ajax.BeginForm 提供一些方向,即 HTTP 方法和一个要更新的 DIV。这些以及其他几个选项都被整齐地封装在 **AjaxOptions** 类中。
  3. 创建一个新的 AjaxOptions 并设置以下值。通过向 Ajax.BeginForm 提供 AjaxOptions 实例,生成的 HTML FORM 将包含许多以“**data-ajax**”开头的属性,映射到设置的属性。
    1. HttpMethod = “POST”
    2. UpdateTargetId = “ParentDiv”
  4. 你会注意到我指定了 `UpdateTargetId` 为“ParentDiv”,这个 ID 尚不存在。我们希望将其作为整个啤酒厂网站的模式,因此在 **Views/Shared** 中编辑 **_Layout.cshtml**,并将 `RenderBody` 调用包装在一个 ID 为“`ParentDiv`”的 DIV 中。这将把一个 DIV 放置在 Index 视图的整个内容周围,无论它是作为常规 GET 渲染还是在 POST 后加载带有错误消息的视图!未来的页面也可以依赖这个直接的父容器作为目标。
  5. 在接受 POST 的 Index 方法中,将返回的 **View** 调用更改为 `PartialView` 调用。如果保留为 View,则将返回 _Layout.cshtml 顶部的整个 HTML 文档。虽然这可能也有效,但有点混乱。由于页面已经加载,我们只想**替换包含页面特定内容的 DIV**,因此最好使用 **PartialView**,它只包含 .cshtml 文件中的必要 HTML。
  6. 默认情况下,MVC 3 应用程序的 web.config 中有一个 appSettings 键,指示 `UnobtrusiveJavaScriptEnabled = true`。我认为朴实的 JS 足够棒,可以保留它,但如果您要扩展旧应用程序,而该应用程序可能会产生问题,也可以在控制器的构造函数中使用 `HtmlHelper.UnobtrusiveJavaScriptEnabled = true;` 来将其设置为 true。
  7. 最后是秘密武器。在 _Layout.cshtml 中,紧跟在 jQuery 库之后,添加一个指向 *jquery.unobtrusive-ajax.min.js* 的脚本标签。
  8. 现在按 F5 运行!

第二次运行

正如你所见,当你从服务器接收到响应时(无论是有效的还是无效的),**都没有整个页面的刷新**。如果出现错误,问题文本框似乎会亮起并显示错误消息。如果一切顺利,你将立即看到“Thanks”。请务必尝试各种验证场景,例如密码太短或太长或不匹配。当然,输入一个会让你小于 21 岁的出生日期,以查看自定义功能的运行情况。

你可能会注意到一个**小的视觉 bug**,因为我使用了 EditorForModel 和开箱即用的样式。你会注意到当页面用错误重绘时,文本框比数据录入部分时要短一点。这在页面重载版本中之前也发生过,但现在它非常明显,并且破坏了那种流畅感。最简单的解决方法是编辑 Site.css 中的**input-validation-error**,将边框设置为**2px**而不是**1px**。在你的生产解决方案中,你可能会有更复杂的样式。

带 AJAX 的逻辑流程

流程仍然很简单,但**更具针对性**。在此演示中,**ParentDiv** 包围了 `RenderBody` 的调用,当用户 GET 页面时,`RenderBody` 会返回 Index 视图。这是标准行为。关键的变化是,当用户 **POST** 时,控制器将其结果作为**部分视图**返回,并**替换 ParentDiv 的 HTML 内容**。

最终分析

我认为到目前为止,这种方法的优点已经很清楚了。**服务器端验证**(“唯一”的验证)得到了很好的支持,并且开发过程**高度简化**。使用此方法构建表单的开发人员会发现自己**极具生产力**,特别是因为学习的内容很少就能使事情**无缝且具有 AJAX 特性**。

但我知道有些开发人员会认为这种方法过于激进。这是因为控制器负责提供一个**功能齐全的部分视图**,即整块 HTML,以在客户端进行替换。这种行为类似于旧的 ASP.NET UpdatePanel,它有其支持者和反对者。我承认我通常属于第二类,因为开发人员常常不知道幕后发生的事情,导致页面难以维护、缓慢和/或混乱。尽管如此,关键是**传递了比严格必要的数据更多的数据**。该方法不是发送简洁的 JSON 结果数据块,而是发送格式化、冗长的 HTML。

因此,我们面临着所有开发中一个非常常见的考虑:**生产力与性能**。要明确的是,我从不提倡糟糕的性能。我提倡的是“足够好”的性能。“先让它工作。然后让它正确。最后让它快速。”这些是有顺序的。如果你正在编写搜索引擎或流行的社交门户,那么性能确实很重要,你可能需要手动编写 XMLHttpRequests。如果你正在编写一个**LOB 应用程序**,我猜你就是这样的,低于一秒的性能时序可能永远不会被任何用户注意到。因此,由于例如 500 毫秒与 100 毫秒的响应时间无关紧要,我宁愿花时间让业务**赚更多的钱**,通过完成**更多的工作**。以后,当我的表单大小和复杂性增加,或者用户群增加负载时,我才能真正看到性能下降,那时我才会重构为使用 `$.ajax()` 调用并处理特定响应。

最后,有很多特殊情况 UI 并不只是进行简单的表单输入数据收集。想象一个页面,用户在其中添加数据时,页面会不断动态增长。也许每周的摘要会折叠成 jQuery UI Accordions,并且每周的数据都可以展开和编辑。如果将其视为一个大型动态增长的表单,随着更多数据的添加,性能会随着时间的推移而下降。这里使用的朴实 AJAX 验证是不合适的。这只是一个例子,我确信还有其他例子。最终,作为设计者和开发人员,你需要运用良好的判断力。

完成的项目

完成的项目已作为 Visual Studio 2012 解决方案附件供您参考。

© . All rights reserved.