[ASP.NET MVC 小牛之路]16 - Model 驗證

Liam Wang發表於2013-11-22

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

相關文章