在前面三篇文章(《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验证
我们知道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验证可以看成是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中的呈现问题。
验证消息在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>
除了通过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的错误消息。
在一个强类型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验证最终是通过默认的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只有在所有属性值都验证通过的情况下,采用使用应用在类型上的验证规则对数据对象实施验证,所以验证的结果也不能完全反映所有的验证规则。