Model Validation in Asp.net MVC

原文: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("[email protected]+\\..+");

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,布布扣,bubuko.com

时间: 2024-08-05 15:21:00

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

[转]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-an-entity-framework-data-model-for-an-asp-net-mvc-application Creating an Entity Framework Data Model for an ASP.NET MVC Application (1 of 10) By      

&lt;转&gt;ASP.NET学习笔记之MVC 3 数据验证 Model Validation 详解

MVC 3 数据验证 Model Validation 详解 在MVC 3中 数据验证,已经应用的非常普遍,我们在web form时代需要在View端通过js来验证每个需要验证的控件值,并且这种验证的可用性很低.但是来到了MVC 新时代,我们可以通过MVC提供的数据验证Attribute来进行我们的数据验证.并且MVC 提供了客户端和服务器端 双层的验证,只有我们禁用了客户端js以后,也会执行服务端验证,所以大大提高了我们的开发进度.今天我们就一起以一个初学者的身份来进入数据验证的殿堂. 首先,

ASP.NET MVC 入门介绍

参考文章 ASP.NET MVC Overview. 1. MVC模式 MVC模式是一种软件架构模式.它把软件系统分为三个部分:模型(Model),视图(View)和控制器(Controller).MVC模式最早由Trygve Reenskaug在1974年提出,是施乐帕罗奥多研究中心(Xerox PARC)在20世纪80年代为程序语言Smalltalk发 明的一种软件设计模式.MVC模式的目的是实现一种动态的程序设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能.除此之外

ASP.NET MVC 5 - 查询Details和Delete方法

原文:ASP.NET MVC 5 - 查询Details和Delete方法 在这部分教程中,接下来我们将讨论自动生成的Details和Delete方法. 查询Details和Delete方法 打开Movie控制器并查看Details方法. public ActionResult Details(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Movie movie

ASP.NET MVC 5 - 给电影表和模型添加新字段

原文:ASP.NET MVC 5 - 给电影表和模型添加新字段 在本节中,您将使用Entity Framework Code First来实现模型类上的操作.从而使得这些操作和变更,可以应用到数据库中. 默认情况下,就像您在之前的教程中所作的那样,使用 Entity Framework Code First自动创建一个数据库,Code First为数据库所添加的表,将帮助您跟踪数据库是否和从它生成的模型类是同步的.如果他们不是同步的,Entity Framework将抛出一个错误.这非常方便的在

ASP.NET MVC 5 -从控制器访问数据模型

原文:ASP.NET MVC 5 -从控制器访问数据模型 在本节中,您将创建一个新的MoviesController类,并在这个Controller类里编写代码来取得电影数据,并使用视图模板将数据展示在浏览器里. 在开始下一步前,先Build一下应用程序(生成应用程序)(确保应用程序编译没有问题) 在解决方案上,用鼠标右键单击Controller文件夹,点击新增,再选择Controller. 在Scaffold新增对话框,选择MVC 5  Controller with views, using

[转] ASP.NET MVC 中你必须知道的 13 个扩展点

ScottGu 在其 最新的博文 中推荐了 Simone Chiaretta 的文章 13 ASP.NET MVC extensibility points you have to know,该文章为我们简单介绍了  ASP.NET MVC  中的 13 个扩展点.Keyvan Nayyeri(与Simone合著了 Beginning ASP.NET MVC 1.0 一书)又陆续发表了一些文章,对这13个扩展点分别进行深入的讨论.我将在以后的随笔中对这些文章逐一进行翻译,希望能对大家有所帮助.

[转]ASP.NET MVC 5 -从控制器访问数据模型

在本节中,您将创建一个新的MoviesController类,并在这个Controller类里编写代码来取得电影数据,并使用视图模板将数据展示在浏览器里. 在开始下一步前,先Build一下应用程序(生成应用程序)(确保应用程序编译没有问题) 在解决方案上,用鼠标右键单击Controller文件夹,点击新增,再选择Controller. 在Scaffold新增对话框,选择MVC 5  Controller with views, using Entity Framework, 点击新增. · 控制

[转]ASP.NET MVC 5 - 查询Details和Delete方法

在这部分教程中,接下来我们将讨论自动生成的Details和Delete方法. 查询Details和Delete方法 打开Movie控制器并查看Details方法. public ActionResult Details(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Movie movie = db.Movies.Find(id); if (movie == nu