對於Web開發人員來說,用戶輸入驗證一直是一個挑戰。不僅在客戶端瀏覽器中需要執行驗證邏輯,在服務器端也需要執行。如果覺得驗證是令人望而生畏的繁雜瑣事,ASP.NET MVC框架提供了數據注解的方式幫助我們處理這些瑣事。
8.1 驗證注解的使用
數據注解特性定義在名稱空間System.ComponentModel.DataAnnotations中,它們提供了服務器端驗證的功能,當在模型的屬性上使用這些特性時,框架也支持客戶端驗證。在名稱空間DataAnnotations中,有4個特性可以用來應對一般的驗證場合。下面從Required特性開始對它們逐一介紹。
8.1.1 Required 驗證特性
這個特性表必須的、不能為空的。用於不為空校驗。
示例1
[Required(ErrorMessage="姓名不得為空")] public string Name { get; set; } //姓名 |
8.1.2 StringLength 驗證特性
這個特性可以驗證字符串長度,一般用來驗證屬性的最大長度,也支持同時設置最小長度。如示例2所示
示例2
[DisplayName("密碼")] [StringLength(20,ErrorMessage = "{0}不能超過{1}個字符")] public string Password { get; set; }
[DisplayName("密碼")] [StringLength(20,MinimumLength=6, ErrorMessage = "{0}長度必須在{2}和{1}之間")] public string Password { get; set; }
|
使用ErrorMessage屬性,一般都會在字符串中使用占位符,上述代碼中,{0}對應屬性的名稱,{1}對應maximum數據,{2}對應minimum數據。
8.1.3 Range驗證特性
Range 驗證特性可以驗證數字(整數和浮點數)、時間等類型的范圍,對應的構造函數如下所示。
public RangeAttribute(int minimum, int maximum);
public RangeAttribute(double minimum, double maximum);
public RangeAttribute(Type type, string minimum, string maximum);
這三個構造函數主要用來驗證時間等特殊類型。如示例3所示。
示例3
[DisplayName("年齡")] [Range(18,35,ErrorMessage ="{0}必須在{1}和{2}之間")] public int Age { get; set; } [DisplayName("生日")] [Range(typeof(DateTime),"2001-01-01","2019-12-31",ErrorMessage = "{0}必須在{1}和{2}之間"] public DateTime? BornDate { get; set; } |
上述代碼中,{0}對應屬性的名稱,{1}對應minimum數據,{2}對應maximum數據。
8.1.4 Compare 驗證特性
使用Compare驗證特性一般用來驗證兩個屬性的值是否一致。如示例4所示。
示例4
public string Passowrd { get; set; } [Compare("Password",ErrorMessage ="兩次輸入的密碼必須一致")] public string PasswordConfig { get; set; } |
8.1.5 RegularExpression驗證特性
RegularExpression驗證特性的關鍵是正則表達式的編寫,如示例5所示。
示例5
[RegularExpression(@"^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$", ErrorMessage = "{0}格式不正確")] public string Email { get; set; } |
8.1.6 Remote驗證特性
同其它幾個驗證特性不同,Remote特性的命名空間是System.Web.Mvc。Remote特性利用服務器端的回調函數執行客戶端的驗證邏輯。如示例6所示。
示例6
//控制器代碼 public ActionResult CheckTelephone(string telephone) { if (telephone=="13636595489") { return Json("手機號"+telephone+ "已經存在", JsonRequestBehavior.AllowGet); } return Json(true, JsonRequestBehavior.AllowGet); } //實體類代碼 [System.Web.Mvc.Remote("CheckTelephone", "Default", ErrorMessage ="手機號碼已經存在")] public string Telephone { get; set; } |
8.2 自動生成客戶端驗證
8.2.1 使用視圖模板創建視圖
ASP.NET MVC 框架支持根據模型自動生成客戶端驗證代碼或腳本,但必須滿足以下條件:
- 必須要用必須使用HtmlHelper擴展方法輸出HTML表單。
- 必須要用強類型頁面。
- 必須要引用Jquery庫、Jquery.Validate、Jquery.Validate.unobtrusive這三個文件。
- web.config文件中這兩個值必須為True。默認為True
<appSettings> <add key="ClientValidationEnabled" value="true" /> <add key="UnobtrusiveJavaScriptEnabled" value="true" /> </appSettings> |
示例7
@using (Html.BeginForm()) { <div class="form-horizontal"> <div class="form-group"> @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" }) </div> </div> </div> } |
在示例7中,Html.EditorFor()方法和Html.TextBoxFor()方法類似,用來輸出input標簽(后面內容介紹區別)。的View視圖會生成如下代碼。
<form action="/Home/Login" method="post"> <div class="form-horizontal"> <div class="form-group"> <label class="control-label col-md-2" for="Name">姓名</label> <div class="col-md-10"> <input class="form-control text-box single-line" data-val="true" data-val-required="姓名不得為空" id="Name" name="Name" type="text" value="" /> <span class="field-validation-valid text-danger" data-valmsg-for="Name" data-valmsg-replace="true"></span> </div> </div> </div> </form> |
示例7中,Html.ValidationMessageFor()和Html.ValidationMessage()擴展方法用來生成HTML的驗證消息。由HTML輔助方法默認生成的HTML都附加了符合HTML5標准的data-*屬性,這些屬性是根據模型自動生成的,引用的客戶端腳本結合這些屬性就構成了完整的客戶端驗證功能。
ASP.NET MV 還提供了 一些列視圖模板,來簡化示例8的復雜工作。在添加視圖時,在"添加視圖"對話框中的模板選項中選擇不同的模板。如圖8-1所示。
圖8-1 選擇視圖模板
系統提供了Create、Delete、Details、Edit 和 List 模板,選擇這些模板,系統會自動生成帶有驗證的、如示例8中所示的視圖代碼。在實際開發中,我們只需要做適當的修改即可。
8.2.2 ModelState 和服務器端驗證
在模型中定義驗證規則后,ASP.NET MVC 在將數據映射到模型時,會自動應用模型類上的驗證規則。在驗證的過程中,它會自動把驗證錯誤信息添加到ModelState數據字典,這時只需要讀取其IsVaild 屬性,就可以判斷是否通過。
另外也可以使用 ModeState 的 AddModelError()方法添加自定義的錯誤信息,用法如示例8所示。
示例8
public ActionResult RegisterUser(User user) { if (ModelState.IsValid) { UserManager manager = new UserManager(); if (!manager.Register(user)) { ModelState.AddModelError("doubleUser", "用戶名已使用,請重新輸入!"); return View("Register2", user); } else//注冊成功 { return Redirect("~/"); } } return View("Register2", user); } |
當服務器端驗證生效后,視圖中使用 Html.ValidationMessage()來輸出驗證信息,除此之外還有Html.ValidationSummary()用來顯示驗證信息的匯總信息。
8.3 自定義驗證規則
除了 ASP.NET MVC 中提供驗證規則外,我們還可以自定義驗證規則。例如,我們要求用戶名不允許是"admin"、"sa"等特殊字符串。
要實現自定義驗證規則,只要繼承 ValidationAttribute類,並重寫IsValid方法即可。如示例9所示。
示例9
public class UserNameAttribute:ValidationAttribute { protected override ValidationResult IsValid(object value, ValidationContext validationContext) { string[] keywords = { "admin", "sa" }; if (!keywords.Contains(value)) { return ValidationResult.Success; } else { return new ValidationResult("不允許使用關鍵字!"); } } } |
IsValid方法中的第一個參數 value 是要驗證的對象的值,ValidationContext參數,提供了很多可在IsValid返回發內部使用的信息,如模型類型、模型對象實例、用來驗證屬性的人性化顯示名稱以及其他有用信息。
上面代碼中的問題在於硬編碼的錯誤提示消息那行代碼。使用數據注解的開發人員希望可以使用ValidationAttribute的ErrorMessage屬性來自定義錯誤提示消息。同時還要與其他驗證特性一樣,提供一個默認的錯誤提示消息(開發人員沒有提供自定義的錯誤提示消息時使用)並且還要利用驗證的屬性名稱生成錯誤提示消息。對示例9的完善如示例10所示。
示例10
public class UserNameAttribute:ValidationAttribute { public UserNameAttribute() :base("{0}不允許使用關鍵字!") { } protected override ValidationResult IsValid(object value, ValidationContext validationContext) { string[] keywords = { "admin", "sa" }; if (!keywords.Contains(value)) { return ValidationResult.Success; } else { var errorMessage = FormatErrorMessage(validationContext.DisplayName); return new ValidationResult(errorMessage); } } } |
前面的代碼做了兩處改動:
- 首先,向基類的構造函數傳遞了一個默認的錯誤提示消息。如果正在面向國際開發應用程序的話,就應該從一個資源文件中提取這個默認的錯誤提示消息。
-
注意,默認的錯誤提示消息中包含了一個參數占位符({0})。這個占位符之所以存在,是因為第二處改動,即調用繼承的FormatErrorMessage方法會自動使用顯示的屬性名稱來格式化這個字符串。
FormatErrorMessage可以確保我們使用合適的錯誤提示消息字符串。這條代碼語句需要傳遞name屬性的值,這個值可以通過validationContext參數的DisplayName屬性獲得。構造完驗證邏輯后,就可以將其應用到任何模型屬性上。
[UserName] public string Name { get; set; }
[UserName(ErrorMessage=" {0}使用了關鍵字!")] public string Name{get;set;} |
8.4 顯示和編輯注解
8.4.1 DisplayName 和 Display 特性
DisplayName用來在視圖中通過 Html.LabelFor()方法顯示屬性的名稱。如:
[DisplayName("姓名")] public string Name { get; set; } |
同樣是為屬性定義說明,還有另一種注解:Dispaly,它的作用並不僅僅是指定顯示的內容,甚至能夠指定顯示的順序。
[Display(Name="姓名",Order=15000)] public string Name { get; set; } |
order的默認參數是10000, Display在用於屬性的顯示名稱上,具有更高的優先級。
8.4.2 HiddenInput 和 ScaffoldColumn 特性
我們的模型中經常需要定義一個名稱中包含有Id的作為key的屬性,但是我們在顯示的時候又不想將這些屬性顯示出來,那該怎么辦呢?就是對顯示隱藏,我們可以使用HiddenInput。如示例11所示。
示例11
[HiddenInput] public int Id{ get; set; }
[HiddenInput(DisplayValue = false)] public int UserId { get; set; } |
使用了HiddenInput,默認情況下屬性會以只讀形式顯示出來,像是上面的Name,要想完全隱藏,就得將DisplayValue設置為false。
隱藏屬性在HTML上的顯示,我們還可以使用ScaffoldColumn特性。使用ScaffoldColumn並不是為屬性設置type = "hidden",它是直接將該屬性從基架中刪除。如:
[ScaffoldColumn(false)] public int UserId{ get; set; } |
即使從基架中將該屬性刪除,模型綁定器仍然會試圖為該屬性賦值,這樣就為典型的攻擊"重復提交"提供了機會。要想防止這種攻擊,我們可以利用Bind特性。
Bind特性可以選擇模型綁定器要綁定的值。像是這樣:
示例12
[Bind(Include = "Name, Email")] public class User { public int UserId{ get; set;} public string Name{ get; set;} public string Email{ get; set;} } |
這樣模型綁定器就只綁定Name和Email屬性。 當然,我們也可以選擇不綁定的屬性:
[Bind(Exclude ="UserId")] public class User { public int UserId{ get; set;} public string Name{ get; set;} public string Email{ get; set;} } |
這樣,上面所講的"重復提交"攻擊就無法發揮作用了。但是必須注意,使用"Include"的白名單比起使用"Exclude"的黑名單更加安全,因為我們永遠也不知道黑客會用怎樣的方式來攻擊我們。
Bind既可以用於模型,也可以用於控制器操作的參數。
8.4.3 ReadOnly特性
如果需要確保默認的模型綁定器不使用請求中的新值來更新屬性,可在屬性上添加ReadOnly特性。(並不會在Html代碼中生成 ReadOnly 屬性)
8.4.4 DataType 特性
DataType特性可為運行時提供關於屬性的特定用途信息。例如,String類型的屬性可應用於很多場合一一可以保存e-mail地址、URL或是密碼。 如示例13所示。
示例13
DataType(DataType.Password) public string Password{ get; set; } |
除了 Password 類型外,還有Custom, DateTime, Date, Time, Duration, PhoneNumber, Currency, Text, Html, MultilineText, EmailAddress, Password, Url, ImageUrl, CreditCard,
PostalCode, Upload。
8.4.5 DisplayFormat 特性
DisplayForat特性可用來處理屬性的各種格式化選項。如示例14所示。
示例14
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:c}")] public decimal Price { get; set; } |
上面的代碼可將模型的 Price 屬性值格式化為貨幣值形式。ApplyFormatInEditMode參數的值默認是false,如果想把 Price 屬性格式化為表單輸入元素,需要將屬性ApplyFormatInEditMode的值設置為true。例如,當把模型中decimal類型的 Price 屬性值設置為12.1時,將在視圖中看到¥12.10的輸出結果。
DisplayFormat還有一個string屬性:NullDisplayText,它表示針對空值(Null)對象的顯示文本。
8.4.6 UIHint特性
UIHint特性給ASP.NET MVC運行時提供了一個模板名稱,以備調用模板輔助方法,如(DisplayFor和EditorFor)渲染時輸出使用。也可以定義自己的模板輔助方法來重寫ASP.NET MVC的默認行為。
8.5 自定義模板
Html.EditorFor()和 Html.DisplayFor()的特殊之處在於它支持模板功能。
8.5.1 在Views/Shared目錄下創建模板
示例15完成一個日歷模板,該模板可以為DateTime類型的模型屬性自動應用該模板。
步驟:
- 在Views/Shared 目錄下創建"EditorTemplates"文件夾。
- 在"EditorTemplates"文件夾下按類型名稱命名視圖:DateTime.cshtml。
- 實現模板代碼。
示例15
@model DateTime? @Html.TextBoxFor(m => m, "", new { @class = "Wdate", onfocus = "$(this).datepicker()" }) |
在視圖中,下面代碼將直接導致輸出一個jQuery日歷文本框。因為BornDate屬性是DateTime 類型,將自動應用這個模板。(在視圖中需要引入相關js文件)
@Html.EditorFor(m=>m.PublishDate)
如果不想讓每一個DateTime編輯器都擁有DatePicker小部件,而是讓一少部分特定的編輯器擁有。此時,可以把自定義的模板文件名改為"SpecialDateTime.cshtml",這樣框架就不會為DateTime模型選擇該模板,除非指定該模板名稱。如下面代碼所示。
@Html.EditorFor(m=>m.PublishDate,"SpecialDateTime")
另外,也可以在PublishDate屬性上使用UIHint特性來指定模板名稱:
[UIHint("SpecialDateTime")]
public DateTime? PublishDate { get;set;}
8.5.2 在Views/Model目錄下創建模板
示例16完成將出版社顯示成下拉框的模板。
步驟:
- 在Views/Book 目錄下創建"EditorTemplates"文件夾。
- 在"EditorTemplates"文件夾下按類型名稱命名視圖:Publisher.cshtml。
- 實現模板代碼。
示例16
@model BookShop.Models.Publisher
@Html.DropDownListFor(m=>m.Id,(SelectList)ViewData["publisher"],"---請選擇---") |
在示例16的代碼中,模板視圖對應的類型是BookShop.Models.Publisher,在視圖中下面的代碼將自動將出版社顯示為下拉框列表。
@Html.EditorFor(model=>model.Publisher)
Html.DisplayFor()使用自定義模板的方式和 Html.EditorFor類似,區別在於將模板文件夾改為"DisplayTemplates"