ASP.NET MVC5 集成客户端与服务器端验证及 Bootstrap 模态框
为 .NET MVC 新手提供关于 Bootstrap 模态框以及客户端/服务器端验证实现的简单教程。
引言
本文旨在提供一个关于 Bootstrap 模态框实现的入门教程,更重要的是,提供一种您希望实现的服务器端验证方式。它基于 Lyubomir Rumenov Velchev 在 CodeProject 上的一篇文章,位于此处(链接:这里)。
作为一名经验不足的开发者,我花费了大量时间来开发这个验证技术,希望这篇文章能帮助那些想实现类似功能的初学者,也希望有经验的程序员能够看看并帮助我改进代码。
背景
当构建一个默认的 MVC 5 项目时(我使用的是 Visual Studio 2015),它倾向于为每个 CRUD 功能使用单独的页面,这在当今新的、响应式的网站中有些过时。我想将这些功能(“创建”、“编辑”、“删除”)整合到基本的 Index 页面中,而不是让用户切换页面。
在遵循上述文章的指导下,这相对容易。然而,要正确实现一个带有您想要的验证的表单,我还花费了一些时间和精力。我很快意识到,要正确实现,有三件事需要立即改变:
- 我需要将表单改为使用 Ajax(特别是 Ajax.BeginForm() 辅助方法);
- 控制器方法需要改为返回 JSON 而不是 ViewResult,以便检索通常随视图返回的 ModelState 错误;以及
- 需要实现 JavaScript 来报告从控制器作为 JSON 返回的服务器端验证错误。
本文的其余部分假设您已经阅读并理解了 Velchev 先生的文章,请务必仔细查阅。我将重点指出几点(如下),然后将专注于我使用的验证技术的集成。
实现
1. 修改视图以使用 Ajax.BeginForm() 辅助方法
首先,模态框内的表单(在部分视图的内容中)需要修改为使用 Ajax.BeginForm()
辅助方法。
@using (Ajax.BeginForm("Create", "UsersAdmin", null, new AjaxOptions { HttpMethod = "POST", InsertionMode = InsertionMode.Replace, OnSuccess = "formReturn(data)", OnFailure = "error(data)" }))
Ajax.BeginForm
("Action", "Controller", "RouteValues", "AjaxOptions"
)。OnSuccess
和 OnFailure
属性很重要。它们指定了在控制器返回 JSON 数据时(即每个函数被告知接收的 data
参数)将被调用的 JavaScript 函数。Html.BeginForm
辅助方法生成的相同。{ @Html.AntiForgeryToken() <div class="form-horizontal"> @Html.ValidationSummary(true, "", new { @class = "text-danger" }) <div id="validation-summary" class="text-danger" hidden="hidden"> <ul></ul> </div> <div class="form-group"> @Html.LabelFor(model => model.FirstName, new { @class = "control-label col-md-3" }) <div class="col-md-9"> @Html.TextBoxFor(m => m.FirstName, new { @class = "form-control" }) @Html.ValidationMessageFor(model => model.FirstName, "", new { @class = "text-danger" }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Email, new { @class = "control-label col-md-3" }) <div class="col-md-9"> @Html.TextBoxFor(m => m.Email, new { @class = "form-control" }) @Html.ValidationMessageFor(model => model.Email, "", new { @class = "text-danger" }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.ConfirmEmail, new { @class = "control-label col-md-3" }) <div class="col-md-9"> @Html.TextBoxFor(m => m.ConfirmEmail, new { @class = "form-control" }) @Html.ValidationMessageFor(model => model.ConfirmEmail, "", new { @class = "text-danger" }) </div> </div> <div class="modal-form-buttons"> <button type="button" class="btn btn-danger" data-dismiss="modal"> <i class="fa fa-thumbs-o-down fa-lg confirm-button-thumbs"></i> </button> <button type="submit" value="Create" id="approve-btn" class="btn btn-primary"> <i class="fa fa-plus fa-lg confirm-button-thumbs"></i> </button> </div> </div> <!-- /.form --> }
2. 修改控制器以向 Ajax 调用返回 JSON
现在需要修改控制器,使其返回 JSON 而不是视图,主要是为了返回通常会与返回的视图一起出现的 ModelState 错误。通常,您的控制器看起来会像这样:
[HttpPost] public ActionResult Create (CreateViewModel model) { if (ModelState.IsValid) { // Create your thing then return to a different view } else { return View(model); } }
在 else
语句中返回到视图的模型包含了一些不知何故混入其中的 ModelState 错误。我们希望捕获这些错误并将它们返回到我们已经加载的同一个视图中。
为了做到这一点,我们应该编写一个方法供其他方法使用。
// // Public: Serializes the Key/Value pairs in ModelState errors and returns a Json object public JsonResult ModelStateErrorSerializer (ModelStateDictionary modelState) { // Initialize Dictionary for holding ModelState errors then loop through and add Dictionary<string, string> errors = new Dictionary<string, string>(); foreach (var item in modelState) { foreach (var error in item.Value.Errors) { errors.Add(item.Key, error.ErrorMessage); } } // Return the serialized object return Json(errors, JsonRequestBehavior.AllowGet); }
该方法接受您的控制器操作的 ModelState
对象,其中包含我们想要序列化为 JSON 的键值对列表。我们可以按如下方式从控制器中调用它:
else { var errors = ModelStateErrorSerializer(ModelState); ... }
注意: 对于我的项目,我希望能够从其他控制器调用此方法,因此我创建了一个新的控制器——“BaseController”,然后将我的其他控制器继承自它,以便它们可以使用此方法。之后,我可以在此基础控制器中添加其他“全局”方法。
一旦我们收到这个对象,它将包含一个名为 Data
的属性,我们通过 errors.Data
来访问我们的错误。然后,我们希望将 Json 返回到视图以完成 Ajax 调用。
else { var errors = ModelStateErrorSerializer(ModelState); return Json(new { errors = errors.Data }, JsonRequestBehavior.AllowGet); }
3. 在视图中使用 JavaScript 处理 ModelState 错误
我想将服务器端验证集成到表单的正确字段下方(尽管客户端验证应该在提交之前捕获这些)。我还知道需要有一个地方来包含非特定于字段的错误。我首先通过在部分视图中添加一个隐藏的 Div 元素来实现后者:
<div id="validation-summary" class="text-danger" hidden="hidden"> <ul></ul> </div>
我选择将其放在 @Html.ValidationSummary(...) 辅助方法正下方,jQuery Validate 错误将在此处呈现。
现在我们可以开始构建我们的 formReturn(data)
函数了,如果您还记得的话,这是我们 Ajax Post 调用 Create
方法时指定的“Success”函数。我已将我的代码粘贴在下面,并逐行注释了。
/* * Title: formReturn (Success Function for Create post Ajax call) * Description: Accepts JSON object from [HttpPost]Create method consisting * of all ModelState errors, then displays in default places in form. */ function formReturn(data) { // Null check on the data parameter (should never be null) if (data != null) { // Check which element has been returned from the controller (errors or Url for success) if (data.errors) { // Clear any errors already on the form $("span[data-valmsg-for]").text(""); // Note: #validation-summary is a custom field added to the form as .NET's won't fire var $valSum = $('#validation-summary'); $valSum.hide(); // Access actual data in object per controller "data = errors*.Data" var errors = data.errors; // Access all keys in object and determine length var keys = Object.keys(errors); var len = keys.length; // Loop over keys and select the span on the form which // has the data-valmsg-for attribute set to the key (managed by .NET), // then set the value of the error message to the key's corresponding value. for (var i = 0; i < len; i++) { var $span = $('span[data-valmsg-for="' + keys[i] + '"]'); // Check if the span doesn't exist... if ($span == undefined || $span == null || keys[i] == "") { // ...then the error message is for something other than a field so display custom $('#validation-summary > ul').append("<li>" + errors[keys[i]] + "</li>"); $valSum.show(); } else { // Otherwise add the value (actual error message) to the span $span.text(errors[keys[i]]); } } } else if (data.Url) { // If the redirect url came back then redirect to it window.location.href = data.Url; } else { // Otherwise something when wrong as nothing else should be returned RenderPartialInModal("_GeneralError"); } } else { // 'data' should never be null from the Ajax call alert("Error: data parameter cannot be null."); } }
您可以看到,我们将控制器方法返回的对象作为 data
参数接收。然后我们检查 errors
参数,并开始一个循环过程,提取与 data-valmsg-for
HTML 标签相等的键。
您将看到我包含的一些附加功能:
else if (data.Url)
... - 当没有错误时,我会发送“Details”ActionResult的 URL,我希望在创建用户后将用户重定向到该 URL。我没有包含这方面的细节,因为它超出了本文的范围(但绝不困难)。else { RenderPartialInModal("_GeneralError") }
- 这是一个我编写的单独的 JavaScript 函数,它与一个控制器方法协同工作,将一个 PartialView 转换为一个字符串,以便在模态框中渲染。在这种情况下,我使用它来弹出错误消息给用户。不过,这是另一篇文章的主题。
关注点
在实现这个过程中,有一个主要的问题与 MVC 渲染脚本的方式有关。
尽管 jQuery 客户端验证在 web.config
中已启用,jquery-val 脚本已按正确的顺序包含在脚本包中,并且脚本包已按正确的顺序包含在包含模态框容器的 _Layout
视图中,但开箱即用的 jQuery 客户端验证在模态框中未能触发。我发现要使其正常工作,需要从 _Layout
视图中删除 JQuery Val 包,并在每个包含我的表单的部分视图中单独渲染它。
我最初认为我只需将其添加到部分视图(并保留在 _Layout
视图中),但这会导致几乎同时向控制器发出两个独立的请求。您可以通过分离包来解决此双请求问题,但我认为我的方法更有意义。
历史
这是本文的第一个/初始版本。