使用Visual Studio 2010和MVC 2.0增強(qiáng)驗(yàn)證功能
在開(kāi)始之前,我不得不說(shuō)明我已經(jīng)安裝了Visual Studio 2010 RC1,并使用它將老版本轉(zhuǎn)換為ASP.Net 4.0,大多數(shù)情況下,當(dāng)你接收到來(lái)自用戶(hù)從form表單post來(lái)的信息后,你的驗(yàn)證代碼往往會(huì)檢查相應(yīng)的值是否存在,數(shù)據(jù)類(lèi)型是否正確以及數(shù)據(jù)的范圍是否正確。
如果將驗(yàn)證代碼放到一個(gè)集中的地方時(shí),那類(lèi)似上面所說(shuō)的改變會(huì)不會(huì)變得更簡(jiǎn)單些?Model中的DataAnnotations正是為此而來(lái),在MVC 2.0中,這一特性被包含在內(nèi)。
DataAnnotations作為.net Framework的一部分已經(jīng)有一段時(shí)間了,但是MVC 2.0中增加了ModelMetaData類(lèi),這是儲(chǔ)存MetaData的容器,默認(rèn)會(huì)使用同樣也是新增類(lèi)的DataAnnotationsMetaDataProvider類(lèi)。因?yàn)閭魅氲闹禃?huì)由Action方法接受model binding作為匹配傳入?yún)?shù)和action的參數(shù)而介入。
在MVC 2.0中,默認(rèn)的model binder使用DataAnnotationsMetaDataProvider來(lái)獲取metadata中model binder嘗試匹配的對(duì)象,如果驗(yàn)證用的metadata存在,則其會(huì)通過(guò)對(duì)對(duì)象的屬性和傳入的值比較來(lái)進(jìn)驗(yàn)證,這類(lèi)meta由你通過(guò)使用標(biāo)簽(Attribute)修飾屬性來(lái)實(shí)現(xiàn)。
下面例子中我通過(guò)原程序中添加聯(lián)系人這一過(guò)程來(lái)描述使用DataAnnotatioins的方法,這里我們使用自定義ViewModel,名為:ContactPersonViewModel。通過(guò)Contact.Add()這個(gè)action方法來(lái)添加聯(lián)系人,代碼如下:
- using System;
- using System.Collections.Generic;
- using System.Web.Mvc;
- using System.ComponentModel;
- namespace ContactManagerMVC.Views.ViewModels
- {
- public class ContactPersonViewModel
- {
- public int Id { get; set; }
- public string FirstName { get; set; }
- public string MiddleName { get; set; }
- public string LastName { get; set; }
- public DateTime DateOfBirth { get; set; }
- public IEnumerable<SelectListItem> Type { get; set; }
- }
- }
下面,我在為屬性添加一些標(biāo)簽(Attribute):
- using System;
- using System.Collections.Generic;
- using System.Web.Mvc;
- using System.ComponentModel.DataAnnotations;
- using ContactManagerMVC.Attributes;
- using System.ComponentModel;
- namespace ContactManagerMVC.Views.ViewModels
- {
- public class ContactPersonViewModel
- {
- public int Id { get; set; }
- [Required(ErrorMessage = "Please provide a First Name!")]
- [StringLength(25, ErrorMessage = "First name must be less than 25 characters!")]
- [DisplayName("First Name")]
- public string FirstName { get; set; }
- [DisplayName("Middle Name")]
- public string MiddleName { get; set; }
- [Required(ErrorMessage = "Please provide a Last Name!")]
- [StringLength(25, ErrorMessage = "Last name must be less than 25 characters!")]
- [DisplayName("Last Name")]
- public string LastName { get; set; }
- [Required(ErrorMessage = "You must provide a Date Of Birth!")]
- [BeforeTodaysDate(ErrorMessage = "You can't add someone who hasn't been born yet!")]
- [DisplayName("Date Of Birth")]
- public DateTime? DateOfBirth { get; set; }
- public IEnumerable<SelectListItem> Type { get; set; }
- }
- }
上面標(biāo)簽的絕大多數(shù)標(biāo)簽都是在System.ComponentModel.Annotations命名空間內(nèi),只有RequiredAttribute標(biāo)簽不在此命名空間內(nèi),這個(gè)標(biāo)簽聲明此值必須是一個(gè)有效值,并且包含ErrorMessage屬性。這個(gè)屬性可以讓你傳入自定義錯(cuò)誤信息。StringLengthAttribute標(biāo)簽指定了屬性可以接受的最小值和***值范圍。當(dāng)和RequiredAttribute標(biāo)簽結(jié)合使用時(shí),只需要設(shè)置可以接受的***值。DisplayNameAttribute用于設(shè)置屬性如何顯示。#p#
上面標(biāo)簽中BeforeTodaysDateAttribute標(biāo)簽并不是.net Framework所提供,這是一個(gè)自定義標(biāo)簽,用于檢測(cè)日期是否比當(dāng)前的日期要早,你可以看到ErrorMessage值被設(shè)置。這個(gè)標(biāo)簽用于防止任何被添加到聯(lián)系人列表的聯(lián)系人還未出生,下面是這個(gè)標(biāo)簽的代碼:
- using System.ComponentModel.DataAnnotations;
- using System;
- namespace ContactManagerMVC.Attributes
- {
- public class BeforeTodaysDateAttribute : ValidationAttribute
- {
- public override bool IsValid(object value)
- {
- if (value == null)
- {
- return true;
- }
- DateTime result;
- if (DateTime.TryParse(value.ToString(), out result))
- {
- if (result < DateTime.Now)
- {
- return true;
- }
- }
- return false;
- }
- }
- }
很簡(jiǎn)單是吧,這個(gè)類(lèi)繼承了ValidationAttribute并重寫(xiě)了IsValid()虛方法,如果未提供值,或是值小于當(dāng)前日期(DateTime.Now),則返回True.利用標(biāo)簽(Attribute)的方式讓在一個(gè)集中的地方應(yīng)用驗(yàn)證規(guī)則變得簡(jiǎn)單,現(xiàn)在,只要ContactPersonViewModel在程序中被用到了,則驗(yàn)證規(guī)則同時(shí)也會(huì)被應(yīng)用到。但現(xiàn)在DefaultModelBinder內(nèi)的DataAnnotations被支持,下面來(lái)看新版本的Add Partial View:
- <%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<ContactPersonViewModel>" %>
- <script type="text/javascript">
- $(function() {
- $('#DateOfBirth').datepicker({ dateFormat: 'yy/mm/dd' });
- });
- $('#save').click(function () {
- $.ajax({
- type: "POST",
- url: $("#AddContact").attr('action'),
- data: $("#AddContact").serialize(),
- dataType: "text/plain",
- success: function (response) {
- if (response == "Saved") {
- window.location = "/";
- }else {
- $("#details").html(response);
- }
- }
- });
- });
- </script>
- <% using (Html.BeginForm("Add", "Contact", FormMethod.Post, new { id = "AddContact" }))
- {%>
- <table>
- <tr>
- <td class="LabelCell"><%= Html.LabelFor(m => m.FirstName)%> </td>
- <td><%= Html.TextBox(m => m.FirstName)%>
- <%= Html.ValidationMessageFor(m => m.FirstName)%></td>
- </tr>
- <tr>
- <td class="LabelCell"><%= Html.LabelFor(m => m.MiddleName)%> </td>
- <td><%= Html.TextBox(m => m.MiddleName)%></td>
- </tr>
- <tr>
- <td class="LabelCell"><%= Html.LabelFor(m => m.LastName)%> </td>
- <td><%= Html.TextBox(m => m.LastName)%>
- <%= Html.ValidationMessageFor(m => m.LastName)%></td>
- </tr>
- <tr>
- <td class="LabelCell"><%= Html.LabelFor(m => m.DateOfBirth)%> </td>
- <td><%= Html.TextBox(m => m.DateOfBirth)%>
- <%= Html.ValidationMessageFor(m => m.DateOfBirth)%></td>
- </tr>
- <tr>
- <td class="LabelCell"><%= Html.LabelFor(m => m.Type)%></td>
- <td><%= Html.DropDownList("Type")%>
- </td>
- </tr>
- <tr>
- <td class="LabelCell"></td>
- <td><input type="button" name="save" id="save" value="Save" /></td>
- </tr>
- </table>
- <% } %>
可以看出,這里使用新的強(qiáng)類(lèi)型Html Helper.對(duì)前面項(xiàng)目修改的兩處是利用了jQuery代碼。***處是添加聯(lián)系人的Partial View是通過(guò)AJax提交,如果驗(yàn)證失敗,則添加的form會(huì)再次被顯示,如果驗(yàn)證通過(guò),新的聯(lián)系人被添加到列表中,頁(yè)面會(huì)刷新繼而顯示更新后包含新聯(lián)系人的列表。
由于下面幾種原因,原來(lái)的Action方法需要被修正。首先修改action方法使其接受ContactPersonViewModel而不是ContactPerson作為參數(shù),這是因?yàn)橄嚓P(guān)的驗(yàn)證規(guī)則應(yīng)用于ContactPersonViewModel,如果不將參數(shù)類(lèi)型改變,那model binder依然能將傳入的值和ContactPerson的屬性相匹配,但所有的驗(yàn)證規(guī)則就不復(fù)存在了。第二個(gè)改變是檢查ModelState的IsValid屬性是否有效,否則整個(gè)驗(yàn)證就變得毫無(wú)意義.
- [AcceptVerbs(HttpVerbs.Post)]
- public ActionResult Add([Bind(Exclude = "Id, Type")]ContactPersonViewModel person)
- {
- if (ModelState.IsValid)
- {
- var p = new ContactPerson
- {
- FirstName = person.FirstName,
- MiddleName = person.MiddleName,
- LastName = person.LastName,
- Type = Request.Form["Type"].ParseEnum<PersonType>()
- };
- if (person.DateOfBirth != null)
- p.DateOfBirth = (DateTime)person.DateOfBirth;
- ContactPersonManager.Save(p);
- return Content("Saved");
- }
- var personTypes = Enum.GetValues(typeof(PersonType))
- .Cast<PersonType>()
- .Select(p => new
- {
- ID = p,
- Name = p.ToString()
- });
- person.Type = new SelectList(personTypes, "ID", "Name");
- return PartialView(person);
- }
在model綁定過(guò)程中,我去掉了id和Type屬性,因?yàn)樵诎崖?lián)系人添加到數(shù)據(jù)庫(kù)以前并不會(huì)存在id屬性,而去掉Type屬性是因?yàn)樵赩iewModel中它的類(lèi)型是SelectList,但在BLL層中ContactPerson對(duì)象中卻是枚舉類(lèi)型,如果ModelState的IsValid屬性為T(mén)rue(注:既驗(yàn)證通過(guò)),則ViewModel的屬性會(huì)和ContactPerson對(duì)象的屬性進(jìn)行匹配,如果IsValid不為T(mén)rue,數(shù)據(jù)會(huì)回傳到View中顯示驗(yàn)證失敗的相關(guān)信息。
上面代碼中我們注意到了Request.Form[“Type”]這個(gè)string類(lèi)型的ParseEnum<T>擴(kuò)展方法,這也是為什么我去掉Type屬性,只有這樣它才會(huì)被轉(zhuǎn)換為適當(dāng)?shù)念?lèi)型。擴(kuò)展方法的原型(在我的Google Analytics 文中)如下:
- public static T ParseEnum<T>(this string token)
- {
- return (T)Enum.Parse(typeof(T), token);
- }
- edit
這個(gè)action方法也是如此,除了對(duì)DateOfBirth進(jìn)行編輯那部分:
- <tr>
- <td class="LabelCell"><%= Html.LabelFor(m => m.DateOfBirth)%> </td>
- <td><%= Html.EditorFor(m => m.DateOfBirth)%>
- <%= Html.ValidationMessageFor(m => m.DateOfBirth)%></td>
- </tr>
這里我并沒(méi)有使用TextBoxFor<T>擴(kuò)展方法,而是使用了EditorFor<T>方法,默認(rèn)情況下,DateTime類(lèi)型都以hh:mm:ss這樣的方式顯示,但我并不喜歡這種格式,所以我創(chuàng)建了一個(gè)格式化顯示時(shí)間日期的模板,在View/Shared目錄下,我添加了一個(gè)名為EditorTemplates(這也是MVC中應(yīng)有命名方式,因?yàn)镸VC會(huì)自動(dòng)搜索這個(gè)位置)并在此目錄下添加一個(gè)名為DateTime的Partial View,這也是MVC的慣例,而DateTime.ascx的代碼如下:
- <%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<System.DateTime?>" %>
- <%= Html.TextBox("", Model.HasValue ? Model.Value.ToShortDateString() : string.Empty) %>
雖然只有短短兩行代碼,但是可以讓時(shí)間日期如果為空時(shí),什么都不顯示,而如果時(shí)間存在,則以ShortDate的格式顯示。
總結(jié)
本篇文章研究了ASP.Net MVC 2.0中利用DataAnnotations來(lái)進(jìn)行驗(yàn)證,現(xiàn)在這已經(jīng)是.net framework的一部分。文中還簡(jiǎn)單的接觸了新版本中的一些特性,包括強(qiáng)類(lèi)型的HTML Helper以及模板。本篇文章的代碼使用Visual Studio 2010 RC1創(chuàng)建的,所以代碼不能在VWD和Visual Studio的環(huán)境中調(diào)試。
【編輯推薦】