原文: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 > )    {    <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 ==  && 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..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(, MinimumLength = , 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="&amp;#39;Date&amp;#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

Model Validation in Asp.net MVC的更多相关文章

  1. Model Validation in ASP.NET Web API

    Model Validation in ASP.NET Web API 原文:http://www.asp.net/web-api/overview/formats-and-model-binding ...

  2. [转]Creating an Entity Framework Data Model for an ASP.NET MVC Application (1 of 10)

    本文转自:http://www.asp.net/mvc/overview/older-versions/getting-started-with-ef-5-using-mvc-4/creating-a ...

  3. Model Validation in ASP.NET Web API By Mike Wasson|July 20, 2012 268 of 294 people found this helpful

    using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using ...

  4. manually Invoking Model Binding / Model Binding /Pro asp.net mvc 5

    限制绑定器 数据源

  5. ASP.NET MVC模型绑定的6个建议(转载)

    ASP.NET MVC模型绑定的6个建议 发表于2011-08-03 10:25| 来源博客园| 31 条评论| 作者冠军 validationasp.netmvc.netasp 摘要:ASP.NET ...

  6. [转]ASP.NET MVC 2: Model Validation

    本文转自:http://weblogs.asp.net/scottgu/archive/2010/01/15/asp-net-mvc-2-model-validation.aspx?CommentPo ...

  7. 转载:Unobtrusive JavaScript in ASP.NET MVC 3 隐式的脚本在MVC3

    Unobtrusive JavaScript 是什么? <!--以下是常规Javascript下写出来的Ajax--> <div id="test"> &l ...

  8. [引]ASP.NET MVC 4 Content Map

    本文转自:http://msdn.microsoft.com/en-us/library/gg416514(v=vs.108).aspx The Model-View-Controller (MVC) ...

  9. ASP.NET MVC Framework

    ASP.NET MVC Framework是微软在ASP.NET中所添加的一组类库,这组类库可以使用Model-View-Controller的设计模式来开发ASP.NET的应用程序.它与现有的ASP ...

随机推荐

  1. docker 现实---联网多台物理主机,容器桥到物理网络(三)

    docker 默认桥接卡docker0 只有当这个单元中的所有容器桥接卡.例如,在主机虚拟网络适配器容器看通常称为veth***  和docker只要把这些卡桥接在一起,例如下面的附图: waterm ...

  2. Ini文件帮助类

    .ini文件是什么 .ini 文件是Initialization File的缩写,就是初始化文件.在Windows系统中,其是配置文件所采用的存储格式(主要是system.ini,win.ini,sy ...

  3. 64位操作系统下用Microsoft.Jet.OLEDB.4.0出现未注册错误

    在WIN7 64位下用Microsoft.Jet.OLEDB.4.0方法访问数据库Access,出现未注册错误 如果是要建立64位的应用程序 1.Microsoft Access Database E ...

  4. UVA714- Copying Books(最大最小化)

    意甲冠军:k手稿的部分成m部分,使每一个和最小 思路:典型最大值最小化问题,使用贪心+二分. 贪心的是每次尽量将元素往右边划分,二分查找最小的x满足m个连续的子序列和S(i)都不超过x. 由于输出的原 ...

  5. 有关windows在调试ODOO8.0有些问题

    继Ubuntu建筑物8.0调试环境后,,尝试windows设置开发环境. 最后的调试和开发,或将返回Linux环境,由于前一段时间手贱,改变分区表,该grub搞哈.哎!后来重建mbr,手动检索分区表( ...

  6. HDOJ 1753 明朝A+B

     http://acm.hdu.edu.cn/showproblem.php? pid=1753 大明A+B Time Limit: 3000/1000 MS (Java/Others)    M ...

  7. 【Unity 3D】学习笔记29:游戏的例子——简单的小制作地图

    无论学习.只看不练是坏科学. 因此,要总结回想这怎么生产MMROPG小地图的游戏.于MMROPG游戏类,在游戏世界中行走时导致各地,通常在屏幕的右上角,将有一个区域,以显示当前的游戏场景微缩.在游戏世 ...

  8. Vim 命令 【转】

    高级一些的编辑器,都会包含宏功能,vim当然不能缺少了,在vim中使用宏是非常方便的: :qx     开始记录宏,并将结果存入寄存器xq     退出记录模式@x     播放记录在x寄存器中的宏命 ...

  9. Display Database Image using MS SQL Server 2008 Reporting Services

    原文 Display Database Image using MS SQL Server 2008 Reporting Services With the new release of MS SQL ...

  10. apk当安装程序将文件复制到手机自带的指定文件夹

    项目已获得,今天.apk文件以获得另一个非调试手机,发现一个问题. 由于涂料.所以绘图数据的点存储在一个.txt文字档.把它用usb传到指定目录下的,可是明显不科学,由于用户下载了你的.apk文件,你 ...