上一篇博文 [ASP.NET MVC 小牛之路]15 - Model Binding 中講了MVC在Model Binding過程中如何根據使用者提交HTTP請求資料建立Model物件。在實際的專案中,我們需要對使用者提交的資訊進行驗證。MVC 對驗證提供了較好的支援,如可以通過 Model 後設資料設定驗證規則、用 ModelState 來處理錯誤資訊等。本文將介紹 Model 的各種驗證及其使用。雖然 Model 驗證使用起來很簡單,但為了更深入的理解它,強烈建議大家在閱讀本文前先閱讀 [ASP.NET MVC 小牛之路]15 - Model Binding。
本文目錄
示例準備
按照慣例,先建立一個MVC應用程式(基本模板)。建立一個名為 Appointment 的Model,程式碼如下:
using System; using System.ComponentModel.DataAnnotations; namespace MvcApplication1.Models { public class Appointment { public string ClientName { get; set; } [DataType(DataType.Date)] public DateTime Date { get; set; } public bool TermsAccepted { get; set; } } }
再建立一個Controller,新增 MakeBooking Action,如下:
public class HomeController : Controller { public ViewResult MakeBooking() { return View(new Appointment { Date = DateTime.Now }); } [HttpPost] public ViewResult MakeBooking(Appointment appt) { return View("Completed", appt); } }
然後為兩個版本的MakeBooking Action方法分別新增兩個View,一個 MakeBooking.cshtml :
@model MvcApplication1.Models.Appointment <h4>Book an Appointment</h4> @using (Html.BeginForm()) { <p>Your name: @Html.EditorFor(m => m.ClientName)</p> <p>Appointment Date: @Html.EditorFor(m => m.Date)</p> <p>@Html.EditorFor(m => m.TermsAccepted) I accept the terms & conditions</p> <input type="submit" value="Make Booking" /> }
和一個Completed.cshtml:
@model MvcApplication1.Models.Appointment <h4>Your appointment is confirmed</h4> <p>Your name is: <b>@Html.DisplayFor(m => m.ClientName)</b></p> <p>The date of your appointment is: <b>@Html.DisplayFor(m => m.Date)</b></p>
使用 ModelState
ModelState 是 Controller 抽象類的一個屬性,它是 MVC 處理完驗證時要使用的一核心物件,提供了對驗證結果的存、取和判斷。所以驗證使用者提交的資料,最直接的方法是在Action方法中使用 ModelState 對Model物件的屬性值自行判斷合法性。下面用一個示例來說明。
修改帶 Appointment 型別引數的 MakeBooking action 方法,程式碼如下:
[HttpPost] public ViewResult MakeBooking(Appointment appt) { if (string.IsNullOrEmpty(appt.ClientName)) { ModelState.AddModelError("ClientName", "Please enter your name"); } if (ModelState.IsValidField("Date") && DateTime.Now > appt.Date) { ModelState.AddModelError("Date", "Please enter a date in the future"); } if (!appt.TermsAccepted) { ModelState.AddModelError("TermsAccepted", "You must accept the terms"); } if (ModelState.IsValid) { return View("Completed", appt); } else { return View(); } }
在這我們通過 ModelState 檢查被Model Binder賦過值的引數物件,如果物件的屬性值不合法則通過 ModelState.AddModelError 方法新增一個錯誤資訊。ModelState.IsValidField 方法用於檢查使用者提交的值是否能夠被Model Binder成功賦值給指定的屬性。若都未通過驗證,則重新呈現 MakeBooking.cshtml 檢視,View 會根據 ModelState 中的錯誤資訊給對應的 input 新增一個 input-validation-error 樣式類,該樣式類在預設引用的 /Content/Site.css 下的定義為:
.input-validation-error { border: 1px solid #f00; background-color: #fee; }
執行效果和生成的 Html 程式碼如下:
這會就有個疑問了,勾選框和文字框都應用了 input-validation-error 樣式類,為什麼勾選框就沒有效果呢。其實大部分主流瀏覽器(包括Chrome 和 Firefox)都會忽略單元框上的樣式。在前面的博文 [ASP.NET MVC 小牛之路]13 - Helper Method 中我們知道了如何自定義 Helper Method 模板,對於勾選框沒有樣式的問題,我們就可以通過自定義 Helper Method 模板解決這個問題,在 /Views/Shared/EditorTemplates 資料夾下建立一個 Boolean.cshtml 分部檢視,程式碼如下:
@model bool? @if (ViewData.ModelMetadata.IsNullableValueType) { @Html.DropDownListFor(m => m, new SelectList(new[] { "Not Set", "True", "False" },Model)) } else { ModelState state = ViewData.ModelState[ViewData.ModelMetadata.PropertyName]; bool value = Model ?? false; if (state != null && state.Errors.Count > 0) { <div class="input-validation-error" style="float:left"> @Html.CheckBox("", value) </div> } else { @Html.CheckBox("", value) } }
再次執行程式,可以看到勾選框也有了框色的邊框,效果如下:
顯示驗證訊息
樣式是為了讓使用者快速地定位到沒有正確輸入的地方,另外,對使用者提交欲提交的資料進行驗證完後,還應該對沒有通過驗證的欄位有給予訊息提示。驗證訊息的顯示,可以簡單的分為兩種,一種是Model級的,另一種是屬性級的,我們先來看Model級的。
我們在 MakeBooking.cshtml 檢視中加入一句 @Html.ValidationSummary() 程式碼,如下:
... @using (Html.BeginForm()) { @Html.ValidationSummary() <p>Your name: @Html.EditorFor(m => m.ClientName)</p> ... }
執行效果和生成的驗證訊息HTML程式碼分別如下:
同樣,在 /Content/Site.css 檔案中也定義了 validation-summary-errors 樣式類,如下:
.validation-summary-errors { font-weight: bold; color: #f00; }
Html.ValidationSummary() 還有三些過載方法:Html.ValidationSummary(bool) 、 Html.ValidationSummary(string) 和 Html.ValidationSummary(bool, string) 。第一個是當引數為true時,只顯示Model級的驗證訊息(如果 ModelState.AddModelError 方法的第一個引數沒有指定屬性名稱,則為Model級的),第二個是為所有的驗證訊息顯示一個標題,第三個是前兩個的結合。
至於屬性級的驗證訊息顯示,也很簡單,使用方法如下:
@using (Html.BeginForm()) { @Html.ValidationSummary(true) <p>@Html.ValidationMessageFor(m => m.ClientName)</p> <p>Your name: @Html.EditorFor(m => m.ClientName)</p> <p>@Html.ValidationMessageFor(m => m.Date)</p> <p>Appointment Date: @Html.EditorFor(m => m.Date)</p> <p>@Html.ValidationMessageFor(m => m.TermsAccepted)</p> <p>@Html.EditorFor(m => m.TermsAccepted) I accept the terms & conditions</p> <input type="submit" value="Make Booking" /> }
執行程式,效果如下:
Model Binder 提供的驗證
除了在 Action 方法中進行驗證,預設的 Model Binder (DefaultModelBinder 類)在對 Model 繫結值時也有驗證的處理。下面我們來看看它實現驗證的效果。
把 Action 中的 ModelState.AddModelError 方法都刪除,刪除後如下:
[HttpPost] public ViewResult MakeBooking(Appointment appt) { if (ModelState.IsValid) { return View("Completed", appt); } else { return View(); } }
執行程式,可以看到預設的 Model Binder 實現的驗證結果如下:
當預設的Model Binder不能夠從提交的表單元素的值中建立一個 DateTime 型別的物件時,則會為 Date 欄位新增一個錯誤(欄位不能為空)。預設的 Model Binder 為 Model 物件的每個屬性提供了一些基本的驗證處理。例如,對於值型別,如果Binder未能給它繫結到值,它會把錯誤資訊新增到ModelState中,然後由 Helper Method 為該欄位顯示相應的錯誤訊息。
預設的Model Binder(DefaultModelBinder 類)提供了一些給Binder新增驗證處理的可重寫方法。如 OmModelUpdated 和 SetProperty,前者在Binder為Model的所有屬性賦值後執行,後者在Binder為屬性賦值時執行。當我們通過繼承 DefaultModelBinder 來自定義 Model Binder時,則可以重寫這些方法來實現一些特殊的驗證需求。關於自定義 Model Binder 請閱讀本系列的 [ASP.NET MVC 小牛之路]15 - Model Binding 文章。
但對於MVC模式來說,如果把驗證的規則放在自定義的 Model Binder 類中似乎並不合適。更多的時候我們會選擇使用後設資料的方式把驗證的規則放在Model類中。
使用後設資料定義驗證規則
MVC 框架支援使用後設資料來表示Model驗證的規則。相對於在 Action 方法中的驗證,使用後設資料的好處在於能使某個Model的驗證規則應用於整個應用程式。DefaultModelBinder 在繫結Model時,會檢查該Model上提供了驗證規則的特性後設資料。你可以看到下面對 Appointment model 應用的驗證規則特性:
public class Appointment { [Required] public string ClientName { get; set; } [DataType(DataType.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; } }
下面列出了MVC內建的驗證特性:
所有的用於驗證的特性都可以像下這樣指定錯誤訊息:
[Required(ErrorMessage="Please enter a date")]
如果沒有指定錯誤訊息,MVC會像前一節的例子那樣使用預設的訊息。
自定義驗證特性類
繼承 ValidationAttribute
MVC 內建的用於驗證特性是一些常用的,當這些特性不能滿足我們的需求時,我們可以通過繼承 ValidationAttribute 類自定義一個特性。例如,在上面的 Appointmen 中用的是 Range 特性來保證 TermsAccepted 的值必須為 true,這看起來很怪,我們可以為此自定義一個特性。
新增一個 Infrastructure 資料夾,在該資料夾中新增一個名為 MustBeTrueAttribute 的類,程式碼如下:
public class MustBeTrueAttribute : ValidationAttribute { public override bool IsValid(object value) { return value is bool && (bool)value; } }
這個特性類重寫了基類的 IsValid 方法,Model Binder 將使用這個特性類來驗證應用了該特性的屬性的值。這個類的驗證邏輯很簡單,即如果是 true 值則通過驗證。然後我們在 Appointment model中對 TermsAccepted 屬性應用該特性,如下:
... [MustBeTrue(ErrorMessage="You must accept the terms")] public bool TermsAccepted { get; set; } ...
這樣看起來比使用Range更簡潔易讀。執行效果如下:
繼承內建的特性類
每個內建的特性類都是繼承自 ValidationAttribute 類,都有一個可以被重寫的 IsValid 方法,所以我們也可以通過繼承內建的特性類來自定義。為此,我們再舉個例子。
在 Infrastructure 檔案下新增一個名為 FutureDateAttribute 的類,程式碼如下:
public class FutureDateAttribute : RequiredAttribute { public override bool IsValid(object value) { return base.IsValid(value) && ((DateTime)value) > DateTime.Now; } }
將此特性應用到 Appointment model的 Date 屬性上,如下:
[FutureDate(ErrorMessage="Please enter a date in the future")] public DateTime Date { get; set; }
這樣我們就可以實現 Date 屬性值必須大於當前時間的驗證。
自定義 Model 級驗證特性
上面我們建立的自定義驗證特性都是應用在屬性上的,這就限制了驗證的規則只能和當前這個屬性相關。如果 Model 中的多個屬性準定了一個驗證規則。例如,Joe這個人星期一這天不能預約,這個驗證規與 ClientName 和 Date 兩個屬性相關,所以需要定義一個 Model 級的驗證特性,下面演示如何定義 Model 級的驗證特性。
在 Infrastructure 檔案下新增一個名為 NoJoeOnMondaysAttribute 的類,程式碼如下:
public class NoJoeOnMondaysAttribute : ValidationAttribute { public NoJoeOnMondaysAttribute() { ErrorMessage = "Joe cannot book appointments on Mondays"; } public override bool IsValid(object value) { Appointment app = value as Appointment; if (app == null || string.IsNullOrEmpty(app.ClientName) || app.Date == null) { return true; } else { return !(app.ClientName == "Joe" && app.Date.DayOfWeek == DayOfWeek.Monday); } } }
把這個特性應用在 Appointment model上,如下:
[NoJoeOnMondays] public class Appointment { ... }
右鍵瀏覽 MakeBooking 檢視,效果如下:
Model 的自驗證
另一個驗證技術是 Model 的自驗證,即在 Model 類內部編寫驗證邏輯方法,通過實現 IValidatableObject 介面來告訴 MVC 該某個 Model 是否為自驗證的 Model。
下面我們讓 Appointment model 實現 IValidatableObject 介面使它包含自驗證功能:
public class Appointment : IValidatableObject { public string ClientName { get; set; } [DataType(DataType.Date)] public DateTime Date { get; set; } public bool TermsAccepted { get; set; } public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { List<ValidationResult> errors = new List<ValidationResult>(); if (string.IsNullOrEmpty(ClientName)) { errors.Add(new ValidationResult("Please enter your name")); } if (DateTime.Now > Date) { errors.Add(new ValidationResult("Please enter a date in the future")); } if (errors.Count == 0 && ClientName == "Joe" && Date.DayOfWeek == DayOfWeek.Monday) { errors.Add(new ValidationResult("Joe cannot book appointments on Mondays")); } if (!TermsAccepted) { errors.Add(new ValidationResult("You must accept the terms")); } return errors; } }
IValidatableObject 介面只定義了一個方法,Validate。該方法的返回值是一個 ValidationResult 型別的集合,每個 ValidationResult 物件代表一個驗證錯誤。如果一個 Model 實現了 IValidatableObject 介面,MVC 會在 Model Binder 為 Model 的每個屬性賦值後呼叫Validate方法。相對於在 action 方法中的驗證,這種 Model 自驗證更為靈活,而且把驗證邏輯放在對應的Model中,保證了程式碼的一致性,方便維護。最後來看來執行結果:
使用客戶端驗證
客戶端驗證在Web.config中有兩個開關,預設都是啟用的,如下:
... <appSettings> <add key="ClientValidationEnabled" value="true"/> <add key="UnobtrusiveJavaScriptEnabled" value="true"/> </appSettings> ...
要啟用客戶端驗證,這兩個值都需要設為true。你也可以在單個的View中通過設定HtmlHelper.ClientValidationEnabled 和 HtmlHelper.UnobtrusiveJavaScriptEnabled的值來開啟或關閉客戶端驗證。啟用時還需要包含三個JS引用:
- /Scripts/ jquery-1.7.1.min.js
- /Scripts/ jquery.validate.min.js
- /Scripts/ jquery.validate.unobtrusive.min.js
新增這些引用最簡單的方法是使用MVC 4新加的一個叫捆綁的功能(將在後續博文中介紹),如下在 /Views/Shared/_Layout.cshtml 檔案中下面的程式碼和引用以上三個檔案是一樣的:
<body> @RenderBody() @Scripts.Render("~/bundles/jquery") @Scripts.Render("~/bundles/jqueryval") @RenderSection("scripts", required: false) </body>
當我們啟用客戶端驗證後,要使用起來,最簡單的方便是對Model應用驗證特性,如Required、Range等。為了演示,我們修改 Appointment 類如下:
public class Appointment { [Required] [StringLength(10, MinimumLength = 3)] public string ClientName { get; set; }
[DataType(DataType.Date)] public DateTime Date { get; set; }
public bool TermsAccepted { get; set; } }
這樣做就可以了,執行程式,在 name 欄位輸入框隨便輸入一個字元,則即刻出現錯誤訊息,如下所示:
這裡的驗證規則是通過後臺指定的。但並不是所有後臺使用的驗證都有對應的客戶端驗證,例如 action 中的驗證、應用Model級的驗證特性和Model的自驗證都是沒有客戶端驗證的。
客戶端驗證如何工作
使用 MVC 提供的客戶端驗證的好處之一是不用寫 JavaScript 程式碼。它的工作方法類似於 [ASP.NET MVC 小牛之路]14 - Unobtrusive Ajax 文章中的 Unobtrusive Ajax,MVC 通過生成 HTML 屬性來表示驗證規則。如果沒有啟用客戶端驗證,@Html.EditorFor(m => m.ClientName) 生成的 HTML 程式碼是:
<input class="text-box single-line" id="ClientName" name="ClientName" type="text" value="" />
啟用客戶端驗證生成的 HTML 程式碼是:
<input class="text-box single-line" data-val="true" data-val-length="The field ClientName must be a string with a minimum length of 3 and a maximum length of 10."
data-val-length-max="10" data-val-length-min="3" data-val-required="The ClientName field is required."
id="ClientName" name="ClientName" type="text" value="" />
引入的兩個客戶端驗證的 jQurey 庫根據 data-val 的屬性值來判斷HTML元素是否需要驗證,而驗證規則是被名稱為 data-val-<name> 的屬性指定的,<name> 代表的是規則名(如data-val-length-max),然後根據這些個屬性的值來實現具體的驗證規則。
MVC 客戶端驗證的另一個好處是,使用者可以即時的看到驗證訊息,更快地得到反饋。當然,如果使用者禁用了JavaScript, MVC 就會走後臺驗證。
你也可以不使用 MVC 特性來實現客戶端驗證,如果你願意花時間研究一下 jquery.validate.js ,也可以很方便地實現客戶端驗證。
使用 Remote 驗證
最後要介紹的一種驗證是使用 Remote 驗證。這種驗證實際上就是通過 Ajax 實現的,只是被MVC封裝好了,用起來簡單多了,也不需要寫 JavaScript 程式碼。下面通過具體的例子說明 Remote 驗證的用法。
在 HomeController 中新增一個用於 Remote 驗證的 Action 方法,程式碼如下:
public JsonResult ValidateDate(string Date) { DateTime parsedDate; if (!DateTime.TryParse(Date, out parsedDate)) { return Json("Please enter a valid date (yyyy/mm/dd)", JsonRequestBehavior.AllowGet); } else if (DateTime.Now > parsedDate) { return Json("Please enter a date in the future", JsonRequestBehavior.AllowGet); } else { return Json(true, JsonRequestBehavior.AllowGet); } }
用於 Remote 驗證的Action 方法必須返回一個 JsonResult 型別的結果,至於 Json 方法為什麼要指定第二個引數為 JsonRequestBehavior.AllowGet 請看 [ASP.NET MVC 小牛之路]14 - Unobtrusive Ajax 文章。
然後在 Appointment model 的 Date 屬性上應用 Remote 特性,需要指定實施驗證規則的 Action 方法名和 Controller 名,如下:
public class Appointment {public string ClientName { get; set; }
[DataType(DataType.Date)] [Remote("ValidateDate", "Home")] public DateTime Date { get; set; }
public bool TermsAccepted { get; set; } }
執行程式,效果如下:
效果上和客戶端驗證差不多,但驗證的處理是在 Controller 中的 Action 中發生的。應用 Remote 特性的欄位,每次改變它的值都會呼叫一次後臺,所以從某種意義上來說,我們應該儘量避免使用這種驗證,除了那種不得不與後臺互動的驗證,如檢查一個使用者名稱是否已經存在。
參考:《Pro ASP.NET MVC 4 4th Edition》