上一篇博文 [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》