在前面三篇文章(《ModelValidator》、《ModelValidatorProvider》和《ModelValidatorProviders》)中我們詳細介紹了真正用於Model驗證的ModelValidator以及相關的提供機制,接下來我們來討論一下在這個以ModelValidator為核心的Model驗證系統中,通過Model綁定得到的數據對象的驗證是如何實現的。[[本文已經同步到《How ASP.NET MVC Works?》中]
目錄
一、從ModelState談起
二、實例演示:驗證Model綁定過程中對ModelError的設置
三、驗證消息的呈現
HtmlHelper.ValidationMessage & HtmlHelper<TModel>.ValidationMessageFor
HtmlHelper.ValidationSummary
錯誤消息在EditForModel方法中的呈現
四、 Model綁定與Model驗證
一、從ModelState談起
我們知道Controller對象的ViewData包含有個元素類型為ModelState的集合,用於表示Model的狀態。除了在Model綁定過程通過ValueProvider體工的數據保存在該集合中之外,提供數據的驗證結果也保存其中。
1: [Serializable]
2: public class ModelState
3: {
4: public ModelErrorCollection Errors { get; }
5: public ValueProviderResult Value { get; set;}
6: }
7:
8: [Serializable]
9: public class ModelErrorCollection : Collection<ModelError>
10: {
11: public ModelErrorCollection();
12: public void Add(Exception exception);
13: public void Add(string errorMessage);
14: }
15:
16: [Serializable]
17: public class ModelError
18: {
19: public ModelError(Exception exception);
20: public ModelError(string errorMessage);
21: public ModelError(Exception exception, string errorMessage);
22:
23: public string ErrorMessage { get; }
24: public Exception Exception { get; }
25: }
通過上面的代碼片斷所示,ModelState具有Value和Errors兩個核心屬性,前者表示ValueProvider提供的ValueProviderResult對象,后者表示針對該數據對象的錯誤集合,其類型為ModelErrorCollection。ModelErrorCollection是一個元素類型為ModelError的集合,而一個ModelError對象通過錯誤消息和異常來描述錯誤。
二、實例演示:驗證Model綁定過程中對ModelError的設置
Model驗證可以看成是Model綁定過程的一部分,它在生成目標Action方法參數值的過程中會對提供的數據實施驗證,而在驗證失敗的情況下驗證結果會以ModelError的形式寫入當前Controller的ViewData的ModelState中,現在我們通過一個簡單的實例來證實這一點。我們還是將多次使用的Contact作為Model類型,如下面的代碼片斷所示,類型Contact和Address以及它們的所有屬性應用了上面定義的驗證特性AlwaysFailsAttribute(《ASP.NET MVC以ModelValidator為核心的Model驗證體系: ModelValidatorProviders》),並設置了相應的錯誤信息。
1: [AlwaysFails(ErrorMessage = "Contact")]
2: public class Contact
3: {
4: [AlwaysFails(ErrorMessage = "Contact.Name")]
5: public string Name { get; set; }
6:
7: [AlwaysFails(ErrorMessage = "Contact.PhoneNo")]
8: public string PhoneNo { get; set; }
9:
10: [AlwaysFails(ErrorMessage = "Contact.EmailAddress")]
11: public string EmailAddress { get; set; }
12:
13: [AlwaysFails(ErrorMessage = "Contact.Address")]
14: public Address Address { get; set; }
15: }
16:
17: [AlwaysFails(ErrorMessage = "Address")]
18: public class Address
19: {
20: [AlwaysFails(ErrorMessage = "Address.Province")]
21: public string Province { get; set; }
22:
23: [AlwaysFails(ErrorMessage = "Address.City")]
24: public string City { get; set; }
25:
26: [AlwaysFails(ErrorMessage = "Address.District")]
27: public string District { get; set; }
28:
29: [AlwaysFails(ErrorMessage = "Address.Street")]
30: public string Street { get; set; }
31: }
在通過Visual Studio的ASP.NET MVC項目模板創建的空Web應用中,我們定義了如下一個默認的HomeController。在基於HTTP-GET的Action方法Index中我們創建一個Contact對象並使用默認的View將其呈現出來。應用了HttpPostAttribute特性的Index方法具有一個類型為Contact的參數,在此方法中我們將包含在當前ViewData的所有ModelState的值和錯誤信息呈現出來。
1: public class HomeController : Controller
2: {
3: public ActionResult Index()
4: {
5: Address address = new Address
6: {
7: Province = "江蘇",
8: City = "蘇州",
9: District = "工業園區",
10: Street = "星湖街328號"
11: };
12: Contact contact = new Contact
13: {
14: Name = "張三",
15: PhoneNo = "123456789",
16: EmailAddress = "zhangsan@gmail.com",
17: Address = address
18: };
19:
20: return View(contact);
21: }
22:
23: [HttpPost]
24: public void Index(Contact contact)
25: {
26: foreach (string key in ViewData.ModelState.Keys)
27: {
28: Response.Write(key + "<br/>");
29: ModelState modelState = ViewData.ModelState[key];
30: Response.Write(string.Format(" Value: {0}<br/>", modelState.Value.ConvertTo(typeof(string))));
31: foreach (ModelError error in modelState.Errors)
32: {
33: Response.Write(string.Format(" Error: {0}<br/>", error.ErrorMessage));
34: }
35: }
36: }
37: }
在如下所示的Action方法Index對應的View的定義,這是一個基於Contact的強類型View。在該View中我們將作為Model的整個Contact對象以編輯模式呈現在一個表單之中。由於Contact的Address屬性是一個復雜類型,所以不會出現在調用EditorForModel方法呈現的HTML中,所有還需要調用EditorFor將該屬性顯示呈現出來。
1: @model Contact
2: @using(Html.BeginForm())
3: {
4: @Html.EditorForModel()
5: @Html.EditorFor(m=>m.Address)
6: <input type="submit" value="保存" />
7: }
運行該程序后會現在瀏覽器中呈現一個編輯聯系人信息的表單,直接點擊“保存”按鈕后會在呈現出如下的輸出結果。我們知道輸出的ModelState的值是在Model綁定過程中通過ValueProvider提供的,而伴隨着Model綁定的驗證則會根據驗證的結果對ModelState的ModelError進行設置。
1: Name
2: Value: 張三
3: Error: Contact.Name
4: PhoneNo
5: Value: 123456789
6: Error: Contact.PhoneNo
7: EmailAddress
8: Value: zhangsan@gmail.com
9: Error: Contact.EmailAddress
10: Address.Province
11: Value: 江蘇
12: Error: Address.Province
13: Address.City
14: Value: 蘇州
15: Error: Address.City
16: Address.District
17: Value: 工業園區
18: Error: Address.District
19: Address.Street
20: Value: 星湖街328號
21: Error: Address.Street
這個演示程序還說明了另一個問題。通過前面的介紹我們知道默認用於進行Model驗證的是CompositeModelValidator,而根據我們之前的實例演示的結果,基於CompositeModelValidator的Model驗證並不具有遞歸性(《ASP.NET MVC以ModelValidator為核心的Model驗證體系: ModelValidatorProviders》),也就是針對Contact對象的驗證並不會遞歸地對Address對象的屬性實施驗證。但是從上面的輸出結果可以清楚地看到,遞歸驗證的現象去發生了,我們將后面的內容討論這個問題。
三、驗證消息的呈現
Model的驗證過程伴隨着Model綁定,當ModelBinder從請求中提取相應的數據為目標Action方法綁定參數值后,驗證錯誤信息已經以ModelError的形式保存到相應的ModelState中。而ModelState列表屬於ViewData的一部分,所以可以直接在View中被使用,這對錯誤信息在View中的呈現提供了可能。現在我們就來討論驗證信息在View中的呈現問題。
HtmlHelper.ValidationMessage & HtmlHelper<TModel>.ValidationMessageFor
驗證消息在View中的呈現通過HtmlHelper/HtmlHelper<TModel>來實現。如下面的代碼片斷所示,靜態ValidationExtensions類中為HtmlHelper定義了4個名為ValidationMessage的擴展方法,為HtmlHelper<TModel>定義了一個名為ValidationMessage的擴展方法。
1: public static class ValidationExtensions
2: {
3: //其他成員
4: public static MvcHtmlString ValidationMessage(this HtmlHelper htmlHelper, string modelName);
5: public static MvcHtmlString ValidationMessage(this HtmlHelper htmlHelper, string modelName, IDictionary<string, object> htmlAttributes);
6: public static MvcHtmlString ValidationMessage(this HtmlHelper htmlHelper, string modelName, object htmlAttributes);
7: public static MvcHtmlString ValidationMessage(this HtmlHelper htmlHelper, string modelName, string validationMessage);
8:
9: public static MvcHtmlString ValidationMessageFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>>
10: expression, string validationMessage, object htmlAttributes);
11: }
ViewData的ModelState屬性的類型不是ModelState,而是一個具有字典結構的ModelStateDictionary類型。ValidationMessage方法中表示所謂Model名稱的參數modelName實際山個對應着存在於這個ModelStateDictionary字典中某個ModelState對象的Key。如果沒有通過參數validationMessage顯式指定了驗證消息,那么就會通過modelName找到相應的ModelState對象,從其Errors屬性表示的ModelErrorCollection對象中提取相應的錯誤消息。
由於ModelState可以包含多個ModelError對象,第一個具有非空消息的ModelError會被選擇,而對應的消息將會作為驗證消息呈現出來。如果這樣的ModelError不存在,不會有任何HTML會被呈現。而ValidationMessageFor與ValidationMessage不同之處在於它會通過指定的表達式來提取ValidationMessage方法中的參數modelName。
現在我們對上面演示的實例略加改動來演示驗證消息的呈現。如下面的代碼片斷所示,在應用了HttpPostAttribute特性的Index方法中,我們將作為參數的contact對象在一個名為“ValidationMessage”的View中呈現。
1: public class HomeController : Controller
2: {
3: //其他成員
4: [HttpPost]
5: public ActionResult Index(Contact contact)
6: {
7: return View("ValidationMessage", contact);
8: }
9: }
如下所示的是這個名為ValidationMessage的View的定義,這是一個基於Contact類型的強類型View。在該View中我們調用HtmlHelper<TModel>的ValidationMessage方法所有的驗證消息呈現出來。
1: @model MvcApp.Models.Contact
2: <ul>
3: <li>@Html.ValidationMessage("Name")</li>
4: <li>@Html.ValidationMessage("PhoneNo")</li>
5: <li>@Html.ValidationMessage("EmailAddress")</li>
6: <li>@Html.ValidationMessage("Address.Province")</li>
7: <li>@Html.ValidationMessage("Address.City")</li>
8: <li>@Html.ValidationMessage("Address.District")</li>
9: <li>@Html.ValidationMessage("Address.Street")</li>
10: </ul>
運行該程序后,在聯系人編輯頁面中直接點擊“保存”按鈕,這個名為ValidationMessage的View會以如下圖所示的效果呈現出來。
在ValidationMessage中針對驗證消息的呈現也可以按照如下的方式調用HtmlHelper<TModel〉的擴展方法ValidationMessageFor來實現。
1: @model MvcApp.Models.Contact
2: <ul>
3: <li>@Html.ValidationMessageFor(c=>c.Name)</li>
4: <li>@Html.ValidationMessageFor(c=>c.PhoneNo)</li>
5: <li>@Html.ValidationMessageFor(c=>c.EmailAddress)</li>
6: <li>@Html.ValidationMessageFor(c=>c.Address.Province)</li>
7: <li>@Html.ValidationMessageFor(c=>c.Address.City)</li>
8: <li>@Html.ValidationMessageFor(c=>c.Address.District)</li>
9: <li>@Html.ValidationMessageFor(c=>c.Address.Street)</li>
10: </ul>
通過這兩個呈現出來的驗證消息具有相同的顯示效果 ,其對應的HTML如下面的代碼所示。可以看出呈現出來的驗證顯示體現為一個<span>元素,其樣式(class="field-validation-error")和客戶端驗證屬性(data-valmsg-for="PhoneNo" data-valmsg-replace="true")作了相應設置。
1: <ul>
2: <li>
3: <span class="field-validation-error" data-valmsg-for="Name" data-valmsg-replace="true">Contact.Name</span></li>
4: <li>
5: <span class="field-validation-error" data-valmsg-for="PhoneNo" data-valmsg-replace="true">Contact.PhoneNo</span>
6: </li>
7: <li>
8: <span class="field-validation-error" data-valmsg-for="EmailAddress" data-valmsg-replace="true">Contact.EmailAddress</span>
9: </li>
10: <li>
11: <span class="field-validation-error" data-valmsg-for="Address.Province" data-valmsg-replace="true">Address.Province</span>
12: </li>
13: <li>
14: <span class="field-validation-error" data-valmsg-for="Address.City" data-valmsg-replace="true">Address.City</span>
15: </li>
16: <li>
17: <span class="field-validation-error" data-valmsg-for="Address.District" data-valmsg-replace="true">Address.District</span>
18: </li>
19: <li>
20: <span class="field-validation-error" data-valmsg-for="Address.Street" data-valmsg-replace="true">Address.Street</span>
21: </li>
22: </ul>
HtmlHelper.ValidationSummary
除了通過ValidationMessageFor與ValidationMessage這兩個方法顯示單條驗證消息之外,我們還可以通過調用HtmlHelper的擴展方法ValidationSummary將所有的驗證消息一並顯示出來。如下面的代碼片斷所示,ValidationExtensions定義了一系列ValidationSummary方法重載。布爾類型的參數excludePropertyErrors表示是否需要排除基於屬性的驗證消息,而通過message參數可以為ValidationSummary指定一個作為標題的字符串。
1: public static class ValidationExtensions
2: {
3: //其他成員
4: public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper);
5: public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, bool excludePropertyErrors);
6: public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, string message);
7: public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, bool excludePropertyErrors, string message);
8: public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, string message, IDictionary<string, object> htmlAttributes);
9: public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, string message, object htmlAttributes);
10: public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, bool excludePropertyErrors, string message, IDictionary<string, object> htmlAttributes);
11: public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, bool excludePropertyErrors, string message, object htmlAttributes);
12: }
ModelStateDictionary是一個Key和Value分別為字符串和ModelState的字典,並且允許一個空字符串作為其Key。ValidationSummary方法通過Key是否為空來判斷ModelState包含的ModelError是否是針對屬性。ModelStateDictionary還定義了如下兩個AddModelError方法重載是我們很容易地進行ModelError的設置。在該方法執行過程中,如果具有相同Key的ModelState對象存在,那么被添加的ModelError將會直接添加到它的Errors集合中;否則會添加一個新的ModelState並將添加的ModelError包含其中。
1: [Serializable]
2: public class ModelStateDictionary : IDictionary<string, ModelState>, ICollection<KeyValuePair<string, ModelState>>,
3: IEnumerable<KeyValuePair<string, ModelState>>, IEnumerable
4: {
5: //其他成員
6: public void AddModelError(string key, Exception exception);
7: public void AddModelError(string key, string errorMessage);
8: }
在一個通過Visual Studio的ASP.NET MVC項目模板創建的空Web應用中,我們定義了如下一個默認的HomeController。在默認的Action方法Index中我們添加了四個ModelError到當前的ModelState集合中,除了最后一個將一個空字符串作為Key之外,前三個均具有一個明確的Key。最后我們直接將默認的View呈現出來。
1: public class HomeController : Controller
2: {
3: public ActionResult Index()
4: {
5: ModelState.AddModelError("Name", "請輸入姓名");
6: ModelState.AddModelError("PhoneNo", "請輸入電話號碼");
7: ModelState.AddModelError("EmailAddress", "請輸入電子郵箱地址");
8:
9: ModelState.AddModelError("", "系統發生異常,詳細信息請與管理員聯系");
10: return View();
11: }
12: }
如下所示的Action方法Index對應的View的定義,在該View中我們兩次調用HtmlHelper的ValidationSummary方法並且指定了message參數。ValidationSummary方法的參數excludePropertyErrors在兩次調用中分別設置為False和True。
1: @Html.ValidationSummary(false, "excludePropertyErrors: false")
2: @Html.ValidationSummary(true, "excludePropertyErrors: true")
該程序運行之后會在瀏覽器中呈現如下圖所示的效果。我們可以看到當excludePropertyErrors參數被設置為True的時候,ValidationSummary中只會呈現出Key為空字符串的ModelState的錯誤消息。
錯誤消息在EditForModel方法中的呈現
在一個強類型View中調用HtmlHelper<TModel>的擴展方法EditorForModel將整個Model對象以編輯模式呈現出來時,如果某個屬性對應的ModelSate具有相應的錯誤(通過Errors屬性表示的ModelError集合不為空),錯誤消息也會一並呈現出來。當然,如果我們為Model類型定義了相應的模板,那又另當別論。我們同樣可以通過一個簡單的實例來演示錯誤消息在EditForModel方法中的呈現。在一個通過Visual Studio的ASP.NET MVC項目模板創建的空Web應用中,我們定義了如下一個屬性的Contact類型作為View的Model。
1: public class Contact
2: {
3: [DisplayName("姓名")]
4: public string Name { get; set; }
5:
6: [DisplayName("電話號碼")]
7: public string PhoneNo { get; set; }
8:
9: [DisplayName("電子郵箱地址")]
10: public string EmailAddress { get; set; }
11: }
然后我們創建一個具有如下定義的HomeController。在Action方法Index中,我們通過調用當ModelState屬性的AddModelError方法認為地添加三個錯誤消息,對應的ModelState名稱與作為Model的Contact類型的屬性名稱一致。最后我們將創建的Contact對象在默認的View中呈現出來。
1: public class HomeController : Controller
2: {
3: public ActionResult Index()
4: {
5: ModelState.AddModelError("Name", "請輸入姓名");
6: ModelState.AddModelError("PhoneNo", "請輸入電話號碼");
7: ModelState.AddModelError("EmailAddress", "請輸入電子郵箱地址");
8: return View(new Contact());
9: }
10: }
下面的代碼片斷代表了Action方法Index對應的View的定義,該View的Model類型為Contact,我們僅僅簡單地調用HtmlHelper<TModel>的擴展方法EditorForModel將整個Model對象以編輯的模式呈現出來。
1: @model Contact
2: @Html.EditorForModel()
當我們成功運行該程序的時候會在瀏覽器中呈現出如下圖所示的效果,我們可以 看到在每個屬性對應的文本框后面,相應的錯誤消息被顯示出來。(S607)
四、 Model綁定與Model驗證
在前面我們不止一次地提到,Model驗證可以看成是Model綁定的一個中間環節。具體來說,Model驗證最終是通過默認的ModelBinder,即DefaultModelBinder實現的。那么現在有這么一個問題:ModelBinder得到最終的作為目標Action方法的參數對象后,再遞交給ModelValidator實施驗證呢,還是ModelBinder在實施Model綁定的過程中動態地調用ModelValidator對通過ValueProvder提供的數據值實施驗證?
實際上我們上面演示的兩個實例已經回答了個問題。通過上面演示的兩個例子我們知道通過CompositeModelValidator這個默認ModelValidator完成的Model驗證並不是遞歸進行的),但是從整個Model綁定過程來看,Model驗證卻具有遞歸性,所以Model綁定和Model驗證絕對不可能是先后的過程,唯一的可能是DefaultModelBinder在遞歸地進行Model綁定的過程中去調用ModelValidator對提供的數據實施驗證。
同樣以針對Contact類型的Model綁定為例。當DefaultModelBinder通過Model得到一個被初始化的Contact對象之后,會根據Contact類型的Model元數據調用ModelValidator的靜態方法GetModelValidator得到一個CompositeModelValidator對象對Contact對象實施驗證。由於CompositeModelValidator的Model驗證不具有遞歸性,所以只有應用在Contact四個屬性(Name、PhoneNo、Email和Address)及其自身類型上的驗證規則在本輪驗證中有效。
由於Contact的Address屬性是一個復雜類型,所以在針對Contact類型的Model綁定過程中會遞歸地綁定一個Address對象並對Contact對象的Address屬性進行初始化。而在完成對Address對象的綁定之后,又會調用ModelValidator的靜態方法GetModelValidator根據基於Address類型的Model元數據得到一個CompositeModelValidator對初始化的Address對象實施驗證。
Model元數據是一個樹型層次化結構,我們的驗證規則可以應用到每一個節點上。DefaultModelBinder就是在遞歸綁定復雜類型對象的過程中對綁定后的對象實施驗證,從而使各個層次上的驗證規則得以生效。不過CompositeModelValidator只有在所有屬性值都驗證通過的情況下,采用使用應用在類型上的驗證規則對數據對象實施驗證,所以驗證的結果也不能完全反映所有的驗證規則。



