MVC驗證10-到底用哪種方式實現客戶端服務端雙重非同步驗證

Darren Ji發表於2014-03-07

本篇將通過一個案例來體驗使用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 效果

提交之前:

1.1


沒有填寫任何內容,提交報錯:

1.2


沒有輸入darren,提交報錯:

1.3


輸入正確,提交成功:

1.4

 

  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 效果

提交之前:

2.1


沒有填寫任何內容,提交報錯:

2.2


沒有輸入darren,提交報錯:

2.2沒有冒泡之前


雖然報錯,但注意到存在一個問題:地址變成了/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。

2.2沒有冒泡之後


輸入正確,提交成功:

1.4

 

  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 效果

沒有輸入任何資訊,提交,報錯:

3.1

 

輸入錯誤使用者名稱,提交:

3.2


發現,當第二次輸入錯誤資訊,提交,竟然跳轉到了Home/ValidCustomer,而且返回的是json字串。為什麼?

 

當第一輸入錯誤資訊,提交到控制器方法,當驗證失敗,會執行return Json(new { vd = false, pv = this.RenderPartialViewToString("_Form", customer) });返回的是_Form.cshtml部分檢視,雖然第一次驗證失敗,沒有跳轉,但實際上,Index.cshtml的<div id="FormContainer">中已經有了_Form.cshtml部分檢視檢視內容:

展開



審查第一次提交後的html元素,如圖:

3.3

 

解決方法:
讓控制器返回部分檢視字串的時候,不要再返回_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) });
        }
    }
}
 

當第二次提交錯誤資訊時,不會跳轉:
3.4

 

  總結

當涉及到表單非同步提交的:
1、優先考慮使用MVC自帶的Ajax.BeginForm()方法,較快。
2、其次考慮"jQuery+Html.BeginForm()方式",較慢,因為需要等待Html.BeginForm()提交。

當涉及不到表單,只涉及部分屬性非同步提交的:
1、優先考慮“MVC驗證08-jQuery非同步驗證”:通過jquery,返回字串,並把錯誤資訊精確顯示到指定html元素。
2、其次考慮本篇的"通過jquery,返回json字串,json字串中包含部分檢視及驗證資訊"。

相關文章