本篇將通過一個案例來體驗使用MVC的Ajax.BeginForm或jQuery來實現非同步提交,並在客戶端和服務端雙雙獲得驗證。希望能梳理、歸納出一個MVC非同步驗證的通用解決思路。本篇主要涉及:
1、通過Ajax.BeginForm()方式,返回部分檢視顯示驗證資訊。
2、通過jQuery+Html.BeginForm()方式,返回部分檢視顯示驗證資訊。
3、通過jquery,返回json字串,json字串中包含部分檢視及驗證資訊。
此外,如下2篇是本文的"兄弟篇",只不過沒有像本篇這樣把多種實現方式放在一個案例中實現。
MVC驗證08-jQuery非同步驗證:通過jquery,返回字串,並把錯誤資訊精確顯示到指定html元素。
MVC驗證09-使用MVC的Ajax.BeginForm方法實現非同步驗證:通過Ajax.BeginForm方式,返回部分檢視顯式驗證資訊。
準備工作
□ 實現客戶端驗證所需的js檔案
不管js檔案是放在_Layout.cshtml中,還是放在具體的檢視頁,也不管BundleConfig.cs中捆版了那些js和css。以下js檔案是必須的:
1、jquery的某個版本
2、jquery.validate.js
3、jquery.validate.unobtrusive.js
□ 實現客戶端驗證的配置
在網站Web.config中,相關的屬性必須設定為true:
<appSettings>
...
<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />
</appSettings>
□ 即將用到的View Model
using System.ComponentModel.DataAnnotations;
using jan.Extension;
namespace jan.Models
{
public class Customer
{
[Required]
[ValidUserNameAttribue(ErrorMessage = "使用者名稱只能為darren")]
[Display(Name = "使用者名稱")]
public string UserName { get; set; }
}
}
□ 自定義驗證特性ValidUserNameAttribue
using System.ComponentModel.DataAnnotations;
namespace jan.Extension
{
public class ValidUserNameAttribue : ValidationAttribute
{
public override bool IsValid(object value)
{
//只有同時滿足2個條件就讓通過,否則驗證失敗
return (value != null && value.ToString() == "darren");
}
}
}
1、通過Ajax.BeginForm方式,返回部分檢視顯示驗證資訊
□ 1、1 Index.cshtml檢視
如果把Index.cshtml看作主檢視的話,需要非同步獲取的內容放在部分檢視中,主檢視通過Html.Partial()來顯示部分檢視內容。
@model jan.Models.Customer
@{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
@DateTime.Now: Index.cshtml檢視被渲染
<hr/>
<div id="FormContainer">
@Html.Partial("_Form")
</div>
□ 1.2 部分檢視_Form.cshtml,驗證失敗返回的部分檢視
用Ajax.BeginForm()方法實現。
@model jan.Models.Customer
@DateTime.Now: _Form.cshtml檢視被渲染
<hr/>
@using (Ajax.BeginForm("ValidCustomer", new AjaxOptions() { UpdateTargetId = "FormContainer", OnSuccess = "$.validator.unobtrusive.parse('form');" }))
{
<p>
@Html.LabelFor(m => m.UserName):
@Html.EditorFor(m => m.UserName)
</p>
<p style="color:red;">
@Html.ValidationMessageFor(m => m.UserName)
</p>
<input type="submit" value="提交"/>
}
UpdateTargetId = "FormContainer"中的FormContainer是主檢視的div,部分檢視非同步提交返回的內容顯示到id為FormContainer的div中。
OnSuccess = "$.validator.unobtrusive.parse('form');" 每次提交完後再初始化表單,準備下一次被提交。
□ 1.3 _Success.cshtml,驗證成功返回的部分檢視
@model jan.Models.Customer
@Model.UserName 是有效的
□ 1.4 HomeController
using System.Web.Mvc;
using jan.Models;
namespace jan.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return View(new Customer());
}
[HttpPost]
public ActionResult ValidCustomer(Customer customer)
{
return PartialView(!ModelState.IsValid ? "_Form" : "_Success", customer);
}
}
}
□ 1.5 效果
提交之前:
沒有填寫任何內容,提交報錯:
沒有輸入darren,提交報錯:
輸入正確,提交成功:
2、通過jQuery+Html.BeginForm方式,返回部分檢視顯示驗證資訊
□ 2.1 Index.cshtml檢視
增加了一個動態顯示載入的div,使用了jquery ui的progress dialog,提交的時候顯示載入圖片。
展開
□ 2.2 部分檢視_Form.cshtml,驗證失敗返回的部分檢視
點選"提交"觸發jquery中的表單提交事件。
@model jan.Models.Customer
@DateTime.Now: _Form.cshtml檢視被渲染
<hr/>
@using (Html.BeginForm("ValidCustomer", "Home"))
{
<p style="color:red;">
@Html.ValidationMessageFor(m => m.UserName)
</p>
<p>
@Html.LabelFor(m => m.UserName):
@Html.EditorFor(m => m.UserName)
</p>
<input type="submit" value="提交" />
}
□ 2.3 _Success.cshtml,驗證成功返回的部分檢視
@model jan.Models.Customer
@Model.UserName 是有效的
□ 2.4 HomeController
其中的Thread.Sleep(2000)是模擬請求時間稍長,前臺檢視顯式載入效果。
using System.Web.Mvc;
using jan.Models;
namespace jan.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return View(new Customer());
}
[HttpPost]
public ActionResult ValidCustomer(Customer customer)
{
Thread.Sleep(2000);
return PartialView(!ModelState.IsValid ? "_Form" : "_Success", customer);
}
}
}
□ 2.5 效果
提交之前:
沒有填寫任何內容,提交報錯:
沒有輸入darren,提交報錯:
雖然報錯,但注意到存在一個問題:地址變成了/Home/ValidCustomer?而在Index.cshtml的jquery中,讓每次提交成功後,返回的部分檢視渲染到Index.cshtml的id為FormContainer的div中。為什麼?
每次返回部分檢視被渲染到Index.cshtm中id為FormContainer的div中,這部分屬動態內容,而類似$("form").on("submit", function (event)這樣的寫法,對動態內容是無效的。根據"DOM冒泡"的事實,應該把submit事件註冊給form的父元素,當點選form中的提交按鈕,根據"DOM冒泡",觸發了form父元素的submit事件,而包含在form父元素下的所有動態內容,此時會受到submit事件的影響。Index.cshtm中完整js如下:
展開
再次輸入錯誤的使用者名稱,提交,也報錯,但沒有跳轉到/Home/ValidCustomer。
輸入正確,提交成功:
3、通過jquery,返回json字串,json字串中包含部分檢視及驗證資訊
□ 3.1 HomeController
返回給前臺json字串,一個key用來提示是否驗證成功,一個key是部分檢視的html元素字串。
using System.Web.Mvc;
using jan.Extension;
using jan.Models;
using System.Threading;
namespace jan.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return View(new Customer());
}
[HttpPost]
public ActionResult ValidCustomer(Customer customer)
{
Thread.Sleep(2000);
if (!ModelState.IsValid)
{
return Json(new { vd = false, pv = this.RenderPartialViewToString("_Form", customer) });
}
return Json(new { vd = true, pv = this.RenderPartialViewToString("_Success", customer) });
}
}
}
□ 3.2 需要一個擴充套件方法,能把部分檢視、model、以及錯誤資訊轉換成字串
展開
□ 3.3 Index.cshtml檢視
這一次,不再需要部分檢視,所有的提交和返回資料都發生在一個檢視頁面上。
注意:
不要直接給提交按鈕註冊click事件,$('#btn').on("click", function(event),這樣會對動態生成的內容無效。而應該這樣寫:$('#FormContainer').on("click","#btn", function(event)
展開
□ 3.4 效果
沒有輸入任何資訊,提交,報錯:
輸入錯誤使用者名稱,提交:
發現,當第二次輸入錯誤資訊,提交,竟然跳轉到了Home/ValidCustomer,而且返回的是json字串。為什麼?
當第一輸入錯誤資訊,提交到控制器方法,當驗證失敗,會執行return Json(new { vd = false, pv = this.RenderPartialViewToString("_Form", customer) });返回的是_Form.cshtml部分檢視,雖然第一次驗證失敗,沒有跳轉,但實際上,Index.cshtml的<div id="FormContainer">中已經有了_Form.cshtml部分檢視檢視內容:
展開
審查第一次提交後的html元素,如圖:
解決方法:
讓控制器返回部分檢視字串的時候,不要再返回_Form.cshtml部分檢視字串。
可以自定義針對Customer的一個檢視,該檢視中不僅有input,提交按鈕,而且還有錯誤資訊。
關於自定義MVC檢視引擎、檢視,請參考:
自定義MVC檢視引擎ViewEngine 建立Model的專屬檢視
■ 3.4.1 實現IView介面
using jan.Models;
using System.Web.Mvc;
using System.Web.Mvc.Html;
namespace jan.Extension
{
public class CustomerView : IView
{
public void Render(ViewContext viewContext, System.IO.TextWriter writer)
{
var allErrors = viewContext.ViewData.ModelState["UserName"].Errors;
string msg = string.Empty;
foreach (ModelError error in allErrors)
{
msg = msg + error.ErrorMessage + " ";
}
//自定義輸出檢視的html格式
writer.Write("<p style='color:red'>"+msg+"</p>");
writer.Write("<p>使用者名稱:<input type='text' id='UserName' /></p>");
writer.Write("<p><input type='button' id='btn' value='提交' /></p>");
}
}
}
可以在ViewContext中根據某個屬性拿到錯誤資訊:viewContext.ViewData.ModelState["UserName"].Errors。
■ 3.4.2 實現IViewEngine介面
讓ViewEngine工作的時候,如果發現部分檢視名是CustomerView,就返回自定義檢視。
using System;
using System.Web.Mvc;
namespace jan.Extension
{
public class CustomerViewEngine : IViewEngine
{
public ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
if (partialViewName == "CustomerView")
{
return new ViewEngineResult(new CustomerView(), this);
}
else
{
return new ViewEngineResult(new String[]{"針對Customer的檢視還沒建立!"});
}
}
public ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
if (viewName == "CustomerView")
{
return new ViewEngineResult(new CustomerView(), this);
}
else
{
return new ViewEngineResult(new String[] { "針對Customer的檢視還沒建立!" });
}
}
public void ReleaseView(ControllerContext controllerContext, IView view)
{
}
}
}
■ 3.4.3 Controller的靜態擴充套件方法
關鍵程式碼:ViewEngines.Engines.FindPartialView(controller.ControllerContext, viewName);
這時候,ViewEngines一旦找到部分檢視名稱是CustomerView,就會返回自定義檢視的ViewEngineResult,最終寫進流,返回自定義檢視字串。
展開
■ 3.4.4 =HomeController中,驗證失敗,返回自定義部分檢視
using System.Web.Mvc;
using jan.Extension;
using jan.Models;
using System.Threading;
namespace jan.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return View(new Customer());
}
[HttpPost]
public ActionResult ValidCustomer(Customer customer)
{
Thread.Sleep(2000);
if (!ModelState.IsValid)
{
//return Json(new { vd = false, pv = this.RenderPartialViewToString("_Form", customer) });
return Json(new { vd = false, pv = this.RenderPartialViewToString("CustomerView", customer) });
}
return Json(new { vd = true, pv = this.RenderPartialViewToString("_Success", customer) });
}
}
}
總結
當涉及到表單非同步提交的:
1、優先考慮使用MVC自帶的Ajax.BeginForm()方法,較快。
2、其次考慮"jQuery+Html.BeginForm()方式",較慢,因為需要等待Html.BeginForm()提交。
當涉及不到表單,只涉及部分屬性非同步提交的:
1、優先考慮“MVC驗證08-jQuery非同步驗證”:通過jquery,返回字串,並把錯誤資訊精確顯示到指定html元素。
2、其次考慮本篇的"通過jquery,返回json字串,json字串中包含部分檢視及驗證資訊"。