Fork me on GitHub
Model 验证

 

 

上一篇博文 [ASP.NET MVC 小牛之路]15 - Model Binding 中讲了MVC在Model Binding过程中如何根据用户提交HTTP请求数据创建Model对象。在实际的项目中,我们需要对用户提交的信息进行验证。MVC 对验证提供了较好的支持,如可以通过 Model 元数据设置验证规则、用 ModelState 来处理错误信息等。本文将介绍 Model 的各种验证及其使用。虽然 Model 验证使用起来很简单,但为了更深入的理解它,强烈建议大家在阅读本文前先阅读 [ASP.NET MVC 小牛之路]15 - Model Binding

本文目录

示例准备

按照惯例,先创建一个MVC应用程序(基本模板)。创建一个名为 Appointment 的Model,代码如下:

using System; 
using System.ComponentModel.DataAnnotations; 
 
namespace MvcApplication1.Models { 
    public class Appointment {  
        public string ClientName { get; set; } 
        [DataType(DataType.Date)] 
        public DateTime Date { get; set; } 
        public bool TermsAccepted { get; set; } 
    } 
}

再创建一个Controller,添加 MakeBooking Action,如下:

public class HomeController : Controller { 
    public ViewResult MakeBooking() { 
        return View(new Appointment { Date = DateTime.Now }); 
    } 
    [HttpPost] 
    public ViewResult MakeBooking(Appointment appt) { 
        return View("Completed", appt); 
    } 
}

然后为两个版本的MakeBooking Action方法分别添加两个View,一个 MakeBooking.cshtml :

@model MvcApplication1.Models.Appointment 

<h4>Book an Appointment</h4> 
@using (Html.BeginForm()) { 
    <p>Your name: @Html.EditorFor(m => m.ClientName)</p> 
    <p>Appointment Date: @Html.EditorFor(m => m.Date)</p> 
    <p>@Html.EditorFor(m => m.TermsAccepted) I accept the terms & conditions</p>     
    <input type="submit" value="Make Booking" /> 
}

和一个Completed.cshtml:

@model MvcApplication1.Models.Appointment 

<h4>Your appointment is confirmed</h4> 
<p>Your name is: <b>@Html.DisplayFor(m => m.ClientName)</b></p> 
<p>The date of your appointment is: <b>@Html.DisplayFor(m => m.Date)</b></p>

使用 ModelState

ModelState 是 Controller 抽象类的一个属性,它是 MVC 处理完验证时要使用的一核心对象,提供了对验证结果的存、取和判断。所以验证用户提交的数据,最直接的方法是在Action方法中使用 ModelState 对Model对象的属性值自行判断合法性。下面用一个示例来说明。

修改带 Appointment 类型参数的 MakeBooking action 方法,代码如下:

[HttpPost] 
public ViewResult MakeBooking(Appointment appt) { 
    if (string.IsNullOrEmpty(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.IsValid) { 
        return View("Completed", appt); 
    } else { 
        return View(); 
    } 
}

在这我们通过 ModelState 检查被Model Binder赋过值的参数对象,如果对象的属性值不合法则通过 ModelState.AddModelError 方法添加一个错误信息。ModelState.IsValidField 方法用于检查用户提交的值是否能够被Model Binder成功赋值给指定的属性。若都未通过验证,则重新呈现 MakeBooking.cshtml 视图,View 会根据 ModelState 中的错误信息给对应的 input 添加一个 input-validation-error 样式类,该样式类在默认引用的 /Content/Site.css 下的定义为:

.input-validation-error { border: 1px solid #f00; background-color: #fee; }

运行效果和生成的 Html 代码如下:

 

这会就有个疑问了,勾选框和文本框都应用了 input-validation-error 样式类,为什么勾选框就没有效果呢。其实大部分主流浏览器(包括Chrome 和 Firefox)都会忽略单元框上的样式。在前面的博文 [ASP.NET MVC 小牛之路]13 - Helper Method 中我们知道了如何自定义 Helper Method 模板,对于勾选框没有样式的问题,我们就可以通过自定义 Helper Method 模板解决这个问题,在 /Views/Shared/EditorTemplates 文件夹下创建一个 Boolean.cshtml 分部视图,代码如下:

@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)
    }
}

再次运行程序,可以看到勾选框也有了框色的边框,效果如下:

显示验证消息

样式是为了让用户快速地定位到没有正确输入的地方,另外,对用户提交欲提交的数据进行验证完后,还应该对没有通过验证的字段有给予消息提示。验证消息的显示,可以简单的分为两种,一种是Model级的,另一种是属性级的,我们先来看Model级的。

我们在 MakeBooking.cshtml 视图中加入一句 @Html.ValidationSummary() 代码,如下:

...
@using (Html.BeginForm()) { 
    @Html.ValidationSummary()
    <p>Your name: @Html.EditorFor(m => m.ClientName)</p> 
    ...
}

运行效果和生成的验证消息HTML代码分别如下:

 

同样,在 /Content/Site.css 文件中也定义了 validation-summary-errors 样式类,如下:

.validation-summary-errors { font-weight: bold; color: #f00; } 

Html.ValidationSummary() 还有三些重载方法:Html.ValidationSummary(bool)  、 Html.ValidationSummary(string)  和 Html.ValidationSummary(bool, string) 。第一个是当参数为true时,只显示Model级的验证消息(如果 ModelState.AddModelError 方法的第一个参数没有指定属性名称,则为Model级的),第二个是为所有的验证消息显示一个标题,第三个是前两个的结合。

至于属性级的验证消息显示,也很简单,使用方法如下:

@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)
    <p>@Html.ValidationMessageFor(m => m.ClientName)</p>
    <p>Your name: @Html.EditorFor(m => m.ClientName)</p>
    <p>@Html.ValidationMessageFor(m => m.Date)</p>
    <p>Appointment Date: @Html.EditorFor(m => m.Date)</p>
    <p>@Html.ValidationMessageFor(m => m.TermsAccepted)</p>
    <p>@Html.EditorFor(m => m.TermsAccepted) I accept the terms & conditions</p>
    <input type="submit" value="Make Booking" />
}

运行程序,效果如下:

Model Binder 提供的验证

除了在 Action 方法中进行验证,默认的 Model Binder (DefaultModelBinder 类)在对 Model 绑定值时也有验证的处理。下面我们来看看它实现验证的效果。

把 Action 中的 ModelState.AddModelError 方法都删除,删除后如下:

[HttpPost]
public ViewResult MakeBooking(Appointment appt) {
    if (ModelState.IsValid) {
        return View("Completed", appt);
    } else {
        return View();
    } 
}

运行程序,可以看到默认的 Model Binder 实现的验证结果如下:

 

当默认的Model Binder不能够从提交的表单元素的值中创建一个 DateTime 类型的对象时,则会为 Date 字段添加一个错误(字段不能为空)。默认的 Model Binder 为 Model 对象的每个属性提供了一些基本的验证处理。例如,对于值类型,如果Binder未能给它绑定到值,它会把错误信息添加到ModelState中,然后由 Helper Method 为该字段显示相应的错误消息。

默认的Model Binder(DefaultModelBinder 类)提供了一些给Binder添加验证处理的可重写方法。如 OmModelUpdated 和 SetProperty,前者在Binder为Model的所有属性赋值后执行,后者在Binder为属性赋值时执行。当我们通过继承 DefaultModelBinder 来自定义 Model Binder时,则可以重写这些方法来实现一些特殊的验证需求。关于自定义 Model Binder 请阅读本系列的 [ASP.NET MVC 小牛之路]15 - Model Binding 文章。

但对于MVC模式来说,如果把验证的规则放在自定义的 Model Binder 类中似乎并不合适。更多的时候我们会选择使用元数据的方式把验证的规则放在Model类中。

使用元数据定义验证规则

MVC 框架支持使用元数据来表示Model验证的规则。相对于在 Action 方法中的验证,使用元数据的好处在于能使某个Model的验证规则应用于整个应用程序。DefaultModelBinder 在绑定Model时,会检查该Model上提供了验证规则的特性元数据。你可以看到下面对 Appointment model 应用的验证规则特性:

public class Appointment { 
    [Required] 
    public string ClientName { get; set; } 

    [DataType(DataType.Date)] 
    [Required(ErrorMessage="Please enter a date")]
  public DateTime Date { get; set; } 

    [Range(typeof(bool), "true", "true", ErrorMessage = "You must accept the terms")]
  public bool TermsAccepted { get; set; } 
}

下面列出了MVC内置的验证特性:

 

 所有的用于验证的特性都可以像下这样指定错误消息:

[Required(ErrorMessage="Please enter a date")]

如果没有指定错误消息,MVC会像前一节的例子那样使用默认的消息。

自定义验证特性类

继承 ValidationAttribute

MVC 内置的用于验证特性是一些常用的,当这些特性不能满足我们的需求时,我们可以通过继承 ValidationAttribute 类自定义一个特性。例如,在上面的 Appointmen 中用的是 Range 特性来保证 TermsAccepted 的值必须为 true,这看起来很怪,我们可以为此自定义一个特性。

添加一个 Infrastructure 文件夹,在该文件夹中添加一个名为 MustBeTrueAttribute 的类,代码如下:

public class MustBeTrueAttribute : ValidationAttribute { 
    public override bool IsValid(object value) { 
        return value is bool && (bool)value; 
    } 
}

这个特性类重写了基类的 IsValid 方法,Model Binder 将使用这个特性类来验证应用了该特性的属性的值。这个类的验证逻辑很简单,即如果是 true 值则通过验证。然后我们在 Appointment model中对 TermsAccepted 属性应用该特性,如下:

...
[MustBeTrue(ErrorMessage="You must accept the terms")] 
public bool TermsAccepted { get; set; }
...

这样看起来比使用Range更简洁易读。运行效果如下:

继承内置的特性类

每个内置的特性类都是继承自 ValidationAttribute 类,都有一个可以被重写的 IsValid 方法,所以我们也可以通过继承内置的特性类来自定义。为此,我们再举个例子。

在 Infrastructure 文件下添加一个名为 FutureDateAttribute 的类,代码如下:

public class FutureDateAttribute : RequiredAttribute { 
    public override bool IsValid(object value) { 
        return base.IsValid(value) && ((DateTime)value) > DateTime.Now; 
    } 
}

将此特性应用到 Appointment model的 Date 属性上,如下:

[FutureDate(ErrorMessage="Please enter a date in the future")] 
public DateTime Date { get; set; }

这样我们就可以实现 Date 属性值必须大于当前时间的验证。

自定义 Model 级验证特性

上面我们创建的自定义验证特性都是应用在属性上的,这就限制了验证的规则只能和当前这个属性相关。如果 Model 中的多个属性准定了一个验证规则。例如,Joe这个人星期一这天不能预约,这个验证规与 ClientName 和 Date 两个属性相关,所以需要定义一个 Model 级的验证特性,下面演示如何定义 Model 级的验证特性。

在 Infrastructure 文件下添加一个名为 NoJoeOnMondaysAttribute 的类,代码如下:

public class NoJoeOnMondaysAttribute : ValidationAttribute { 

    public NoJoeOnMondaysAttribute() { 
        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) { 
        return true; 
        } else { 
        return !(app.ClientName == "Joe" && app.Date.DayOfWeek == DayOfWeek.Monday); 
        }
    } 
}

把这个特性应用在 Appointment model上,如下:

[NoJoeOnMondays] 
public class Appointment { 
    ...
} 

右键浏览 MakeBooking 视图,效果如下:

Model 的自验证

另一个验证技术是 Model 的自验证,即在 Model 类内部编写验证逻辑方法,通过实现 IValidatableObject 接口来告诉 MVC 该某个 Model 是否为自验证的 Model。

下面我们让 Appointment model 实现 IValidatableObject 接口使它包含自验证功能:

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")); 
        }
        if (DateTime.Now > Date) { 
            errors.Add(new ValidationResult("Please enter a date in the future")); 
        }
        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")); 
        } 
        return errors; 
    }
}

IValidatableObject 接口只定义了一个方法,Validate。该方法的返回值是一个 ValidationResult 类型的集合,每个 ValidationResult 对象代表一个验证错误。如果一个 Model 实现了 IValidatableObject 接口,MVC 会在 Model Binder 为 Model 的每个属性赋值后调用Validate方法。相对于在 action 方法中的验证,这种 Model 自验证更为灵活,而且把验证逻辑放在对应的Model中,保证了代码的一致性,方便维护。最后来看来运行结果:

使用客户端验证

客户端验证在Web.config中有两个开关,默认都是启用的,如下:

... 
<appSettings> 
    <add key="ClientValidationEnabled" value="true"/> 
    <add key="UnobtrusiveJavaScriptEnabled" value="true"/> 
</appSettings> 
...

要启用客户端验证,这两个值都需要设为true。你也可以在单个的View中通过设置HtmlHelper.ClientValidationEnabled 和 HtmlHelper.UnobtrusiveJavaScriptEnabled的值来开启或关闭客户端验证。启用时还需要包含三个JS引用:

  • /Scripts/ jquery-1.7.1.min.js
  • /Scripts/ jquery.validate.min.js
  • /Scripts/ jquery.validate.unobtrusive.min.js

添加这些引用最简单的方法是使用MVC 4新加的一个叫捆绑的功能(将在后续博文中介绍),如下在 /Views/Shared/_Layout.cshtml 文件中下面的代码和引用以上三个文件是一样的:

<body> 
    @RenderBody() 
    @Scripts.Render("~/bundles/jquery") 
    @Scripts.Render("~/bundles/jqueryval") 
    @RenderSection("scripts", required: false) 
</body> 

当我们启用客户端验证后,要使用起来,最简单的方便是对Model应用验证特性,如Required、Range等。为了演示,我们修改 Appointment 类如下:

public class Appointment {
    [Required]
    [StringLength(10, MinimumLength = 3)]
    public string ClientName { get; set; }
[DataType(DataType.Date)]
public DateTime Date { get; set; }
public bool TermsAccepted { get; set; } }

这样做就可以了,运行程序,在 name 字段输入框随便输入一个字符,则即刻出现错误消息,如下所示:

这里的验证规则是通过后台指定的。但并不是所有后台使用的验证都有对应的客户端验证,例如 action 中的验证、应用Model级的验证特性和Model的自验证都是没有客户端验证的。

客户端验证如何工作

使用 MVC 提供的客户端验证的好处之一是不用写 JavaScript 代码。它的工作方法类似于 [ASP.NET MVC 小牛之路]14 - Unobtrusive Ajax 文章中的 Unobtrusive Ajax,MVC 通过生成 HTML 属性来表示验证规则。如果没有启用客户端验证,@Html.EditorFor(m => m.ClientName) 生成的 HTML 代码是:

<input class="text-box single-line" id="ClientName" name="ClientName" type="text" value="" /> 

启用客户端验证生成的 HTML 代码是:

<input class="text-box single-line" data-val="true" 
    data-val-length="The field ClientName must be a string with a minimum length of 3 and a maximum length of 10." 
data-val-length-max
="10" data-val-length-min="3" data-val-required="The ClientName field is required."

id
="ClientName" name="ClientName" type="text" value="" />

引入的两个客户端验证的 jQurey 库根据 data-val 的属性值来判断HTML元素是否需要验证,而验证规则是被名称为 data-val-<name>  的属性指定的,<name> 代表的是规则名(如data-val-length-max),然后根据这些个属性的值来实现具体的验证规则。

MVC 客户端验证的另一个好处是,用户可以即时的看到验证消息,更快地得到反馈。当然,如果用户禁用了JavaScript, MVC 就会走后台验证。

你也可以不使用 MVC 特性来实现客户端验证,如果你愿意花时间研究一下 jquery.validate.js ,也可以很方便地实现客户端验证。

使用 Remote 验证

最后要介绍的一种验证是使用 Remote 验证。这种验证实际上就是通过 Ajax 实现的,只是被MVC封装好了,用起来简单多了,也不需要写 JavaScript 代码。下面通过具体的例子说明 Remote 验证的用法。

在 HomeController 中添加一个用于 Remote 验证的 Action 方法,代码如下:

public JsonResult ValidateDate(string Date) {
    DateTime parsedDate;
    if (!DateTime.TryParse(Date, out parsedDate)) {
        return Json("Please enter a valid date (yyyy/mm/dd)", JsonRequestBehavior.AllowGet);
    }
    else if (DateTime.Now > parsedDate) {
        return Json("Please enter a date in the future", JsonRequestBehavior.AllowGet);
    }
    else {
        return Json(true, JsonRequestBehavior.AllowGet);
    }
}

用于 Remote 验证的Action 方法必须返回一个 JsonResult 类型的结果,至于 Json 方法为什么要指定第二个参数为 JsonRequestBehavior.AllowGet 请看 [ASP.NET MVC 小牛之路]14 - Unobtrusive Ajax 文章。

然后在 Appointment model 的 Date 属性上应用 Remote 特性,需要指定实施验证规则的 Action 方法名和 Controller 名,如下:

public class Appointment {public string ClientName { get; set; }
[DataType(DataType.Date)] [Remote(
"ValidateDate", "Home")] public DateTime Date { get; set; }
public bool TermsAccepted { get; set; } }

运行程序,效果如下:

效果上和客户端验证差不多,但验证的处理是在 Controller 中的 Action 中发生的。应用 Remote 特性的字段,每次改变它的值都会调用一次后台,所以从某种意义上来说,我们应该尽量避免使用这种验证,除了那种不得不与后台交互的验证,如检查一个用户名是否已经存在。

 


参考:《Pro ASP.NET MVC 4 4th Edition》

 

作者:Liam Wang

编程沉思-做一款小巧而好用的截图软件

 
效果图,为了减少图片大小,比例进行了相应的缩减。
第一节:介绍一下我的作品
图片左上角的exe就是我的截图软件,双击打开软件。
1.等待两秒,截图软件启动的时候首先隐藏自己并截取完整的屏幕,2秒后替代当前屏幕。
2.左键单击开始选取截图区域,再次单击结束截图区域,会有一个从起点到终点的矩形提示选取的区域。
3.对当前选取区域不满意可以ALT+A重新选取截图区域,方法同2.
4.截图完成后弹出操作菜单,依照用途选择标记类型,共支持矩形、椭圆、直线三种标记,在操作菜单上点选按钮。
5.在选取截图区域内单击开始标记,再次单击结束标记,见效果图。
6.做完这些操作用户可以选择如何保存截图以及推出,共支持三种方式:什么都不做的直接退出方式;保存至剪贴板并退出;保存至剪贴板和bmp文件并退出。
第二节:作品回顾
----这里只介绍分析过程,代码给出链接,文中不再贴代码。
准备工作:频繁的画线和贴图,如何避免屏幕闪烁,当然要用双缓冲咯,选定截取区域以前在整个对话框的背景上画线,选取截图区域以后仅仅在截图区域上画线。
朋友们,刚刚掌握双缓冲就迫不及待的开始了,我真的太天真了。
实战第一步:该如何启动我的软件?
这是我遇到的第一个问题,截图软件不能像一般软件那样启动,会导致覆盖一部分屏幕的,在反复启动关闭我使用的截图软件以及找博客学习后得出结论:截屏软件是一个没有标题栏、按钮和边框的对话框,刚启动的时候是隐藏的,启动一段时间后用自己覆盖了桌面。
实战第二步:动手做全屏对话框的朋友才会懂,windows并没有提供现成的应用程序全屏函数,只能自己定义对话框的宽、高、起点,问题随之而来:宽=屏幕像素宽且高=屏幕像素高且起点为(0,0)时对话框展现在屏幕上会有边框,浅浅的但绝对可以认出来的边框,Oh fuck,想破头皮才想来了灵机一动,对话框设计的比屏幕大一些,大概4-6个像素,并且偏移量从(-1,-1)开始,嗯,完美了,边框仍然存在但是存在屏幕外的,Who care!
实战第三步:启动的时候隐藏对话框,2秒后显示对话框,这个没啥难度,一个定时器就解决掉。
实战第四步:截取屏幕并显示到当前对话框上,这个过程做起来挺难,回味起来真没意思,创建一个显示器的DC,这就是所有的技术关键了,剩下的工作就是GDI的基础应用,从源DCBitBlt位图到源DC关联的位图。这里我选择创建一张基础图片作为背景,背景图片作为以后位图操作的源图片但本身不做任何修改。
实战第五步:消息响应与截取,当我们的截图软件到最前端以后是不希望指定按键和鼠标操作影响我们截图的,在消息预处理中截取消息,仅放过我们指定的鼠标操作以及快捷键,我采用鼠标左键点击开始截图和结束截图,全局快捷键重新开始截图。
实战第六步:截图操作处理,鼠标点击一下,开始截图,会有一个矩形框连接起始点和鼠标当前位置,当截图完成后该矩形框固定。我的准备工作发挥光和热了,背景图片画到缓冲画布上,同时在缓冲画布上画上矩形,完成绘画后将缓冲图形贴到对话框上。
实战第七步:做一个按钮菜单,我用了一排简陋的按钮来替代菜单功能:获取截图区域的右下角坐标,进行相应调整,确保一整排按钮都可以显示在桌面上。
实战第八步:保存到剪贴板和位图文件,这其实是两个功能啦,挤挤放一起吧,剪贴板就是一系列API的调用:获取剪贴板,清空剪贴板,粘贴到剪贴板,关闭剪贴板。啥?没了!真没了。保存到位图相对复杂点,尤其是第一次做的同学们,可能对要保存的各种结构体很头大,做吧,有不少文章介绍的。
实战第九步:用户打标记:使用截图软件的重头戏就是在截取的图片上做一些高亮显示的框框、圆形、直线,这是必须有的,仍然要采用双缓冲来贴图。用户提意见了:我们要求回退功能,哪次打标记失误,我们要求回退操作。笑一笑,这个也有,我采用一种比较笨拙的方式实现的回退,定义一个包含4张位图的数组,每次操作记录在数组里相应的位图上,要回退时就废弃掉现在的图片并把之前的图片贴回来,这样实现的效果是最多支持3次完整回退,第4次回退就会把用户所有的标记都撤销了。后来学到了一种技巧,还没有应用,有兴趣的同学试一试,定义一个记录操作的结构体,每次操作都记录在结构体里并追加到记录链表,在撤销操作的时候取屏幕相反颜色的画笔再次执行标记动作就可以还原画笔的痕迹。
第三节:小小的感想
越简单的软件容错率和差异度都要求更高的精度,做一个让人满意的小软件需要用心去做,完工以后还要多加打磨。
编程的积淀容不得任何水分,初学时候我就读了C++primer,读了C专家,读了C和指针,读了C++程序设计语言......到头来还是笨手笨脚,那时敢与人斗辩,现在只想把这孤独的路途走的快一点,要学的要想的要做的,莫张皇、莫求人、边学边记,做出点东西拿与大家点评。
做的不好的地方请大家多批评。
 
 
 
posted on 2013-11-22 16:57  HackerVirus  阅读(1187)  评论(0编辑  收藏  举报