原文:
Model Validation in Asp.net MVC
本文用于记录Pro ASP.NET MVC 3 Framework中阐述的数据验证的方式。
先说服务器端的吧。最简单的一种方式自然是直接在Action方法中来进行了,如下:
[HttpPost]
public ViewResult MakeBooking(Appointment appt)
{
if (String.IsNullOrWhiteSpace(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.IsValidField(
"
ClientName
") && ModelState.IsValidField(
"
Date
") &&
appt.ClientName ==
"
Joe
" && appt.Date.DayOfWeek == DayOfWeek.Monday)
{
ModelState.AddModelError(
"",
"
Joe cannot book appointments on Mondays
");
}
if (ModelState.IsValid)
{
repository.SaveAppointment(appt);
return View(
"
Completed
", appt);
}
else
{
return View();
}
}
补充Appointment类源码如下:
public
class Appointment
{
public
string ClientName {
get;
set; }
[DataType(DataType.Date)]
public DateTime Date {
get;
set; }
public
bool TermsAccepted {
get;
set; }
}
可以看到,Appointment类很POCO,其中Date属性上的DataType属性,不过是标注Date属性值为DateTime的Date部分(去掉Time部分)。再看action内部,将传入的appointment对象属性进行了一个遍历校验。最后,ModelState.AddModelError("", "Joe cannot book appointments on Mondays"); 是标注一个对象模型级别的错误(方法的key参数为空),模型级别错误可以标注多个,它们均将通过@Html.ValidationSummary()显示错误信息。
上述action对应的view为:
@model PageValidation.Models.Appointment
@{
ViewBag.Title =
"
Make A Booking
";
}
<h4>Book an Appointment</h4>
@using (Html.BeginForm())
{
@Html.ValidationSummary();
<p>
Your name: @Html.EditorFor(m => m.ClientName)
@Html.ValidationMessageFor(m => m.ClientName)
</p>
<p>
Appointment Date: @Html.EditorFor(m => m.Date)
@Html.ValidationMessageFor(m => m.Date)
</p>
<p>
@Html.EditorFor(m => m.TermsAccepted) I accept the terms & conditions
@Html.ValidationMessageFor(m => m.TermsAccepted)
</p>
<input type=
"
submit
" value=
"
Make Booking
" />
}
这个时候,运行程序,神马都不填写然后提交时,页面提示如下:
如果不想form中错误提示重复(顶部的summary和顶部的detail),将@Html.ValidationSummary(); 更新为@Html.ValidationSummary(true); 即可。这个时候,顶部Validation Summary部分只会提示model-level错误了,比如上文中的ModelState.AddModelError("", "Joe cannot book appointments on Mondays");。 关于@Html.ValidationSummary()更多细节,请MSDN。
另外,还有一个view的问题是,Firefox和Chrome等一些浏览器上,对checkbox样式的设置不取作用,上图中的效果是通过在checkbox外层包一个div,将checkbox样式转移到div上来实现的。具体为:在项目Views\Shared\EditorTemplates目录下,建立一个Boolean.cshtml文件以覆盖asp.net mvc默认的行为。文件内容如下:
@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)
}
}
服务器端验证第2种方式是通过Model Binder了。我们继承DefaultModelBinder来写一个Appointment需要的类:
public
class ValidatingModelBinder : DefaultModelBinder
{
protected
override
void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor,
object value)
{
//
make sure we call the base implementation
base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
//
perform our property-level validation
switch (propertyDescriptor.Name)
{
case
"
ClientName
":
if (
string.IsNullOrEmpty((
string)value))
{
bindingContext.ModelState.AddModelError(
"
ClientName
",
"
Please enter your name
");
}
break;
case
"
Date
":
if (bindingContext.ModelState.IsValidField(
"
Date
") && DateTime.Now > ((DateTime)value))
{
bindingContext.ModelState.AddModelError(
"
Date
",
"
Please enter a date in the future
");
}
break;
case
"
TermsAccepted
":
if (!((
bool)value))
{
bindingContext.ModelState.AddModelError(
"
TermsAccepted
",
"
You must accept the terms
");
}
break;
}
}
protected
override
void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
//
make sure we call the base implementation
base.OnModelUpdated(controllerContext, bindingContext);
Appointment model = bindingContext.Model
as Appointment;
//
apply our model-level validation
if (model !=
null && bindingContext.ModelState.IsValidField(
"
ClientName
") && bindingContext.ModelState.IsValidField(
"
Date
")
&& model.ClientName ==
"
Joe
" && model.Date.DayOfWeek == DayOfWeek.Monday)
{
bindingContext.ModelState.AddModelError(
"",
"
Joe cannot book appointments on Mondays
");
}
}
}
其中,OnModelUpdated方法是当给model所有属性赋值时触发,SetProperty方式是当单个属性变化时即触发。接下来要做的,就是在global的Application_Start方法中注册了:
ModelBinders.Binders.Add(
typeof(Appointment),
new ValidatingModelBinder());
然后,MakeBooking action就可以解脱出来,只需要如下几行代码:
if (ModelState.IsValid)
{
repository.SaveAppointment(appt);
return View(
"
Completed
", appt);
}
else
{
return View();
}
此时,效果和第1种方式完全一样。
第3种方式是通过MetaData了。Asp.net MVC内置了5个meta data验证属性:Compare、Range、RegularExpression、Required、StringLength。基于这5个属性的一些限制,为了更适切Appointment类,自定义几个验证属性如下:
futureDate验证属性:
public
class FutureDateValidatorAttribute : ValidationAttribute
{
public
override
bool IsValid(
object value)
{
var isDate = value
is DateTime;
if(isDate)
{
var date = Convert.ToDateTime(value);
if (date <= DateTime.Now)
{
return
false;
}
}
return
true;
}
}
MustBeTrue验证属性:
public
class MustBeTrueAttribute : ValidationAttribute
{
public
override
bool IsValid(
object value)
{
return value
is
bool && (
bool)value;
}
}
Appointment验证属性:
public
class AppointmentValidatorAttribute : ValidationAttribute
{
public AppointmentValidatorAttribute()
{
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)
{
//
we don't have a model of the right type to validate, or we don't have
//
the values for the ClientName and Date properties we require
return
true;
}
else
{
return !(app.ClientName ==
"
Joe
" && app.Date.DayOfWeek == DayOfWeek.Monday);
}
}
}
再来定义Appointment类:
[AppointmentValidator]
public
class Appointment
{
[Required(ErrorMessage =
"
Please enter your name
")]
public
string ClientName {
get;
set; }
[DataType(DataType.Date)]
[FutureDateValidator(ErrorMessage =
"
You must enter a date in the future
")]
public DateTime Date {
get;
set; }
//
[Range(typeof(bool), "true", "true", ErrorMessage = "You must accept the terms")]
[MustBeTrue(ErrorMessage =
"
You must accept the terms
")]
public
bool TermsAccepted {
get;
set; }
}
第4种方式:通过实现IValidatableObject接口,定义自验证model。还是Appointment类,如下:
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
",
new
string[] {
"
ClientName
" }));
}
if (DateTime.Now > Date)
{
errors.Add(
new ValidationResult(
"
Please enter a date in the future
",
new
string[] {
"
Date
" }));
}
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
",
new
string[] {
"
TermsAccepted
" }));
}
return errors;
}
}
可以看到,它的核心不过是:将类对象验证内容移入到Valiate方法中。
第5种方式,通过继承ModelValidationProvider,创建自定义ValidationProvider. 如下:
public
class CustomValidationProvider : ModelValidatorProvider
{
public
override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context)
{
if (metadata.ContainerType ==
typeof(Appointment))
{
return
new ModelValidator[] {
new AppointmentPropertyValidator(metadata, context)
};
}
else
if (metadata.ModelType ==
typeof(Appointment))
{
return
new ModelValidator[] {
new AppointmentValidator(metadata, context)
};
}
return Enumerable.Empty<ModelValidator>();
}
}
AppointmentPropertyValidator代码如下:
public
class AppointmentPropertyValidator : ModelValidator
{
public AppointmentPropertyValidator(ModelMetadata metadata, ControllerContext context)
:
base(metadata, context)
{
}
public
override IEnumerable<ModelValidationResult> Validate(
object container)
{
Appointment appt = container
as Appointment;
if (appt !=
null)
{
switch (Metadata.PropertyName)
{
case
"
ClientName
":
if (
string.IsNullOrEmpty(appt.ClientName))
{
return
new ModelValidationResult[]
{
new ModelValidationResult
{
//
MemberName = "ClientName",
Message =
"
Please enter your name
"
}
};
}
break;
case
"
Date
":
if (appt.Date ==
null || DateTime.Now > appt.Date)
{
return
new ModelValidationResult[]
{
new ModelValidationResult
{
MemberName =
"",
Message =
"
Please enter a date in the future
"
}
};
}
break;
case
"
TermsAccepted
":
if (!appt.TermsAccepted)
{
return
new ModelValidationResult[]
{
new ModelValidationResult
{
MemberName =
"",
Message =
"
You must accept the terms
"
}
};
}
break;
}
}
return Enumerable.Empty<ModelValidationResult>();
}
}
注意,上文代码中MemberName不能填写,获取赋值为空,否则error提交到ModelState时,key值会重叠,比如ClientName会成为ClientName.ClientName。AppointmentValidator代码如下:
public
class AppointmentValidator : ModelValidator
{
public AppointmentValidator(ModelMetadata metadata, ControllerContext context)
:
base(metadata, context)
{
}
public
override IEnumerable<ModelValidationResult> Validate(
object container)
{
Appointment appt = (Appointment)Metadata.Model;
if (appt.ClientName ==
"
Joe
" && appt.Date.DayOfWeek == DayOfWeek.Monday)
{
return
new ModelValidationResult[]
{
new ModelValidationResult
{
MemberName =
"",
Message =
"
Joe cannot book appointments on Mondays
"
}
};
}
return Enumerable.Empty<ModelValidationResult>();
}
}
做完这些工作,然后就是注册启用CustomerValidationProvider了。在Application_Start中加入:
ModelValidatorProviders.Providers.Add(new CustomValidationProvider());
就完毕了。
关于CustomerValidationProvider这种方式,作者建议仅用于复杂场合。如:需要从db中动态加载validation rule,或者实现自己的一些验证框架时才使用。这里有一个案例:http://www.codeproject.com/Articles/463900/Creating-a-custom-ModelValidatorProvider-in-ASP-NE
好吧,再看浏览器端的验证。
第1步先启用客户端验证:
<add key=
"
ClientValidationEnabled
" value=
"
true
"/>
<add key=
"
UnobtrusiveJavaScriptEnabled
" value=
"
true
"/>
或者在Application_Start中增加:
HtmlHelper.ClientValidationEnabled =
true;
HtmlHelper.UnobtrusiveJavaScriptEnabled =
true;
还有,view当中确保没有:
HtmlHelper.ClientValidationEnabled =
false;
默认情况下,它是true。如果要禁用,上述3个区域任意一个设置为false即可。
第2步,view中加载4个必须文件:
<link href=
"
@Url.Content(
"~/Content/Site.css
"
)
" rel=
"
stylesheet
" type=
"
text/css
" />
<script src=
"
@Url.Content(
"~/Scripts/jquery-
1.5.
1.min.js
"
)
" type=
"
text/javascript
"></script>
<script src=
"
@Url.Content(
"~/Scripts/jquery.validate.min.js
"
)
" type=
"
text/javascript
"></script>
<script src=
"
@Url.Content(
"~/Scripts/jquery.validate.unobtrusive.min.js
"
)
" type=
"
text/javascript
"></script>
第3步,据说最简单的方式是利用meta data属性:
[AppointmentValidator]
public
class Appointment
{
[Required(ErrorMessage =
"
Please enter your name
")]
[StringLength(
10, MinimumLength =
3, ErrorMessage =
"
Please enter a string of whose length is between 3 and 10
")]
[EmailAddress]
public
string ClientName {
get;
set; }
[DataType(DataType.Date)]
[FutureDateValidator(ErrorMessage = "You must enter a date in the future")]
public DateTime Date {
get;
set; }
[MustBeTrue(ErrorMessage =
"
You must accept the terms
")]
public
bool TermsAccepted {
get;
set; }
}
其中EmailAddress是新实现的一个可供客户端验证用的metadata属性。如下:
public
class EmailAddressAttribute : ValidationAttribute, IClientValidatable
{
private
static
readonly Regex emailRegex =
new Regex(
"
.+@.+\\..+
");
public EmailAddressAttribute()
{
ErrorMessage =
"
Enter a valid email address
";
}
public
override
bool IsValid(
object value)
{
return !
string.IsNullOrEmpty((
string) value) &&
emailRegex.IsMatch((
string) value);
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
return
new List<ModelClientValidationRule>
{
new ModelClientValidationRule
{
ValidationType =
"
email
",
ErrorMessage =
this.ErrorMessage
},
//
new ModelClientValidationRule
//
{
//
ValidationType = "required",
//
ErrorMessage = this.ErrorMessage
//
}
};
}
}
它实现了一个IClientValidatable 接口,所以能够直接在客户端交互。
关于它的实现原理,它不过是在server端将view上需要验证的全部信息都render并且隐藏在页面,然后基于jQuery的validation组件来交互。 看一下html片段:
<
p
>
Your name:
<
input
data-val
="true"
data-val-email
="Enter a valid email address"
data-val-length
="Please enter a string of whose length is between 3 and 10"
data-val-length-max
="10"
data-val-length-min
="3"
data-val-required
="Please enter your name"
id
="ClientName"
name
="ClientName"
type
="text"
value
=""
/>
<
span
class
="field-validation-valid"
data-valmsg-for
="ClientName"
data-valmsg-replace
="true"
></
span
>
</
p
>
<
p
>
Appointment Date:
<
input
class
="text-box single-line"
data-val
="true"
data-val-remote
="&#39;Date&#39; is invalid."
data-val-remote-additionalfields
="*.Date"
data-val-remote-url
="/Appointment/ValidateDate"
data-val-required
="The Date field is required."
id
="Date"
name
="Date"
type
="text"
value
="2012/10/16"
/>
<
span
class
="field-validation-valid"
data-valmsg-for
="Date"
data-valmsg-replace
="true"
></
span
>
</
p
>
所以,在客户端,其实你可以脱离mvc框架自己来写。如:
$(document).ready(
function () {
$('form').validate({
errorLabelContainer: '#validtionSummary',
wrapper: 'li',
rules: {
ClientName: {
required:
true,
}
},
messages: {
ClientName: "Please enter your name"
}
});
});
同时,在view中render时,你也可以按照自己的方式来做。如将原有的ClientName显示方式换为:
<p>
Your name: @Html.TextBoxFor(m => m.ClientName,
new { data_val =
"
true
",
data_val_email =
"
Enter a valid email address
",
data_val_required =
"
Please enter your name
"})
@Html.ValidationMessageFor(m => m.ClientName)
</p>
因为-在C#中是非法变量名字符,所以用_替代,同时asp.net mvc生成html时会将它替换为-。
最后一个问题是,当客户端验证需要使用服务器端资源时,怎么办? 这时就要使用到Remote Validation了。首先,自然是得后端有一个ajax调用的action了:
public JsonResult ValidateDate(
string Date)
{
DateTime parsedDate;
if (!DateTime.TryParse(Date,
out parsedDate))
{
return Json(
"
Please enter a valid date (mm/dd/yyyy)
", JsonRequestBehavior.AllowGet);
}
else
if (DateTime.Now > parsedDate)
{
return Json(
"
Please enter a date in the future
", JsonRequestBehavior.AllowGet);
}
else
{
return Json(
true, JsonRequestBehavior.AllowGet);
}
}
然后,在Appointment类的Date属性上加个Remote特性:
[DataType(DataType.Date)]
//
[FutureDateValidator(ErrorMessage = "You must enter a date in the future")]
[Remote(
"
ValidateDate
"
,
"
Appointment
"
)]
public DateTime Date {
get;
set; }
至此,它就完成了。当你输入date结束后,就会调用ValidateDate(string Date)方法。 我在想,这里Appointment得是真正的ViewModel了,要不然就太别扭了。因为它实际上是调用了controller的action方法了。
全部源码download