模型驗證是在模型綁定時檢查從HTTP請求接收的數據是否合規以保證數據的有效性,在收到無效數據時給出提示幫助用戶糾正錯誤的數據。
顯式模型驗證
驗證數據最直接的方式就是在action方法中對接收的數據驗證,以下面的Model為例:
public class Appointment { public string ClientName { get; set; } public DateTime Date { get; set; } public bool TermsAccepted { get; set; } }
我們要求ClientName不能為空;約會日期Date不能早於當前日期,日期的格式可以在web.config中使用<globalization culture="en-US" uiCulture="enUS"/>來指定,否則使用服務器默認的時區格式;TermsAccepted必須為true。我們在MakeBooking.cshtml視圖中收集數據:
@model ModelValidation.Models.Appointment @{ ViewBag.Title = "Make A Booking"; } <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" /> }
直接在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) { // statements to store new Appointment in a // repository would go here in a real project return View("Completed", appt); } else { return View(); } } ...
ModelState.IsValidField()檢查模型綁定器能否成功綁定“Date”屬性,如果數據不合法使用ModelState.AddModelError()添加錯誤消息。如果沒有任何錯誤,ModelState.IsValid=true,我們可以繼續正常操作,否則返回數據輸入界面。HTML.EditFor()幫助函數會檢查ModelState是否包含當前屬性的錯誤,如果有錯誤會為生成的元素添加CSS類input-validation-error,默認的input-validation-error類定義在~/Content/Site.css中:
... .input-validation-error { border: 1px solid #f00; background-color: #fee; } ...
其效果就是使得輸入控件邊框變紅、背景變粉紅以提示用戶有錯誤發生。如果自己編寫的HTML幫助函數要支持驗證錯誤提示,可以參考System.Mvc.Web.Html.InputExtensions的源代碼是如何實現的。
一些瀏覽器比如Chrome和Firefox會忽略應用在復選框Checkbox上的CSS屬性,我們可以通過前面講到的自定義模板來解決:
@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) } }
這里定義了一個bool類型專用的自定義模板,使用div標簽包裝checkbox,從modelstate檢查當前屬性是否有錯誤,有錯誤時添加錯誤提示的CSS類到div標簽上。
顯示驗證消息
除了通過CSS風格提示錯誤,我們可以將添加到modelstate的錯誤消息在視圖中顯示給用戶:
@model ModelValidation.Models.Appointment @{ ViewBag.Title = "Make A Booking"; } <h4>Book an Appointment</h4> @using (Html.BeginForm()) { @Html.ValidationSummary() <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" /> }
Html.ValidationSummary()幫助函數將ModelState中的錯誤消息以列表的方式羅列出來顯示給用戶。ValidationSummary有幾種重載形式:
重載形式 | 說明 |
Html.ValidationSummary() | 匯總顯示所有的驗證錯誤 |
Html.ValidationSummary(bool) | 如果bool參數=true,只顯示Model層次的錯誤,否則所有的驗證錯誤都顯示 |
Html.ValidationSummary(string) | 在所有錯誤消息之前再顯示string給出的字符串 |
Html.ValidationSummary(bool, string) | 同Html.ValidationSummary(bool),只是在錯誤消息前多顯示string給出的字符串 |
所謂Model層次的錯誤,其實就是使用ModelState.AddModelError()添加錯誤消息時第一個代表錯誤屬性的參數留空,比如:
... if (ModelState.IsValidField("ClientName") && ModelState.IsValidField("Date") && appt.ClientName == "Joe" && appt.Date.DayOfWeek == DayOfWeek.Monday) { ModelState.AddModelError("", "Joe cannot book appointments on Mondays"); } ...
除了Html.ValidationSummary(),我們可以將錯誤消息緊鄰輸入控件挨個顯示:
@model ModelValidation.Models.Appointment @{ ViewBag.Title = "Make A Booking"; } <h4>Book an Appointment</h4> @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" /> }
Html.ValidationMessageFor()在屬性有錯誤時顯示對應的錯誤消息,為避免在匯總消息中重復顯示,這里使用true參數調用Html.ValidationSummary(true)。
模型綁定時驗證
默認模型綁定器DefaultModelBinder內建在綁定時驗證數據,比如我們輸入非日期格式給Date屬性,綁定器會給出“The value 'xxx' is not valid for Date.”的錯誤消息。我們可以重載DefaultModelBinder的一些方法來添加有用的信息:
方法 | 說明 | 默認實現的功能 |
OmModelUpdated | 在綁定器試圖給模型對象所有屬性賦值時調用 | 根據模型metadata給出的驗證規則驗證數據添加錯誤消息到ModelState |
SetProperty | 在綁定器視圖給模型對象的某個屬性賦值時調用 | 如果模型屬性不能是Null但是沒有數據來綁定時添加“The <name> field is required”消息到ModelState,如果有數據但是處理錯誤比如類型轉換失敗添加“The value <value> is not valid for <name>”消息到ModelState |
使用元數據指定驗證規則
更多的時候我們不需要重載默認模型綁定器,因為我們可以更方便的使用metadata在數據模型上添加驗證規則:
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; } }
這里使用了Required和Range兩個特性,前者表示數據是必須的,后者指定了一個可用值范圍;ErrorMessage則是錯誤時的提示消息,如果不指定則使用上表中的默認消息。可用的驗證特性包括:
特性 | 示例 | 說明 |
Compare | [Compare("MyOtherProperty")] | 兩個屬性必須相同值,比如我們要求用戶重復輸入兩次郵件地址時有用 |
Range | [Range(10, 20)] | 屬性值必須在指定的數值范圍內,可以使用數值類型的最大最小值比如int.MinValue、int.MaxValue |
RegularExpression | [RegularExpression("pattern")] | 字符串值必須匹配正則表達式,默認大小寫敏感,可以使用(?i)修飾符關閉大小寫敏感,比如[RegularExpression("(?i)mypattern")] |
Required | [Required] | 屬性值必須非空或者不能只是空格,如果允許全空格可以[Required(AllowEmptyStrings = true)] |
StringLength | [StringLength(10)] | 字符串長度不能超過給定的最大長度,也可以指定最小長度:[StringLength(10, MinimumLength=2)] |
上面的例子中沒有使用Required特性驗證bool類型的TermsAccepted,這是因為EditFor()在渲染Checkbox會多給出一個hidden的輸入元素,即使我們沒有選中checkbox返回的結果中仍然是有值的。使用Range看上去比較別扭,好在我們可以創建自定義的驗證特性類來改進:
public class MustBeTrueAttribute : ValidationAttribute { public override bool IsValid(object value) { return value is bool && (bool)value; } }
這里驗證輸入數據是否是bool類型且為true,使用這個自定義驗證特性很簡單:
.. [MustBeTrue(ErrorMessage="You must accept the terms")] public bool TermsAccepted { get; set; } ...
除了從ValidationAttribute擴展自定義特性,我們可以直接從內建的驗證特性擴展:
public class FutureDateAttribute : RequiredAttribute { public override bool IsValid(object value) { return base.IsValid(value) && ((DateTime)value) > DateTime.Now; } }
這里從Require驗證特性擴展,在調用基類的驗證后再做附加的檢查。
以上的驗證特性都是針對模型單個屬性的,我們還可以為整個模型創建自定義驗證特性:
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) { // we don't have a model of the right type to validate, or we don't have // the values for the ClientName and Date properties we require return true; } else { return !(app.ClientName == "Joe" && app.Date.DayOfWeek == DayOfWeek.Monday); } } }
這里檢查客戶名稱和約定日期,不允許客戶名稱Joe在星期一預約,我們可以將這個特性應用在整個模型類上:
[NoJoeOnMondays] public class Appointment { [Required] public string ClientName { get; set; } [DataType(DataType.Date)] [FutureDate(ErrorMessage="Please enter a date in the future")] public DateTime Date { get; set; } [MustBeTrue(ErrorMessage="You must accept the terms")] public bool TermsAccepted { get; set; } }
有了這些驗證規則特性,控制器類的action方法可以極大的簡化為:
... [HttpPost] public ViewResult MakeBooking(Appointment appt) { if (ModelState.IsValid) { // statements to store new Appointment in a // repository would go here in a real project return View("Completed", appt); } else { return View(); } } ...
自驗證模型
模型驗證的另外一種是為模型類實現IValidatableObject接口創建可自驗證的模型類:
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using ModelValidation.Infrastructure; namespace ModelValidation.Models { 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; } } }
模型綁定器在試圖給模型對象賦值時調用Validate(),返回結果是一個錯誤列表,使用這種方式我們可以在一個地方做完所有的數據驗證。
客戶端驗證
以上講的都是數據提交到服務器上后的驗證,客戶端我們可以通過腳本在數據提交到服務器前驗證,MVC支持“unobtrusive client-side validation”,unobtrusive意指在輸出HTML標簽時添加特定HTML標簽,過MVC的JAVA腳本驗證庫利用這些專用特性進行數據驗證。要使用客戶端驗證首先需要在web.config中啟用:
... <appSettings> <add key="ClientValidationEnabled" value="true"/> <add key="UnobtrusiveJavaScriptEnabled" value="true"/> </appSettings> ...
上面的兩個設置必須都為true,我們可以在Razor代碼塊中使用HtmlHelper.ClientValidationEnabled和HtmlHelper.UnobtrusiveJavaScriptEnabled為單個視圖配置是否使用客戶端驗證。我們還必須保證以下腳本文件被添加到視圖或者布局文件中:
- /Scripts/ jquery-1.7.1.min.js
- /Scripts/ jquery.validate.min.js
- /Scripts/ jquery.validate.unobtrusive.min.js
可以看到客戶端驗證仍然是依賴於Jquery的。在啟用客戶端驗證后,我們添加到模型類上的內建驗證特性比如Requried、StringLength就可以直接工作了,數據驗證錯誤時java腳本會給出錯誤提示。具體來講EditFor()這些模板幫助函數在啟用客戶端驗證后會輸出一些額外的特性,比如上面ClientName屬性:
... <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="" /> ...
JQuery驗證函數查找data-val=true的元素進行驗證,data-val-<xxx>則是具體的驗證規則,比如這里的data-val-length和data-val-required。通過元數據指定的驗證規則既可以在客戶端使用,也可以在服務器端使用,為我們帶來了極大的方便,而且即使在客戶端禁用了JAVA腳本,服務器端的數據驗證仍然有效。
遠程驗證
遠程驗證是客戶端驗證和服務端驗證的折中方式,客戶端在背后通過Ajax請求向服務端驗證數據,典型的應用場景可以是用戶名的驗證,在用戶名驗證成功后才允許用戶繼續后續的輸入。使用遠程驗證是從控制器定義一個用於驗證的action方法開始:
... public JsonResult ValidateDate(string Date) { DateTime parsedDate; if (!DateTime.TryParse(Date, out parsedDate)) { return Json("Please enter a valid date (mm/dd/yyyy)", JsonRequestBehavior.AllowGet); } else if (DateTime.Now > parsedDate) { return Json("Please enter a date in the future", JsonRequestBehavior.AllowGet); } else { return Json(true, JsonRequestBehavior.AllowGet); } } ...
驗證action方法必須有一個和要驗證字段同名的參數,這里定義Date為字符串類型是有考慮的。模型綁定如果不能從請求數據中轉換成日期類型會發生異常,遠程驗證無法在客戶端顯示異常信息會被靜悄悄的丟棄,所以一般我們使用字符串類型,在驗證方法內部顯式的轉換數據類型。驗證方法返回一個返回一個JsonResult對象,驗證成功我們封裝true,不成功封裝錯誤信息,無論哪種結果我們使用JsonRequestBehavior.AllowGet標識驗證結果可以通過GET請求。
有了遠程驗證action,我們需要添加remote驗證特性到相應的模型類屬性上:
public class Appointment { [Required] [StringLength(10, MinimumLength = 3)] public string ClientName { get; set; } [DataType(DataType.Date)] [Remote("ValidateDate", "Home")] public DateTime Date { get; set; } public bool TermsAccepted { get; set; } }
在Remote驗證特性中指定用於驗證的控制器名稱和action,MVC的javascript驗證庫按此生成的URL請求並驗證。遠程驗證會在用戶第一次提交表單時生效,以及此后的每一次編輯數據的動作,比如每一次的按鍵都會執行一次遠程驗證,這是我們在帶寬有限時需要考慮的。
以上為對《Apress Pro ASP.NET MVC 4》第四版相關內容的總結,不詳之處參見原版 http://www.apress.com/9781430242369。