3.2.3 设置模型验证规则
模型验证是确保应用程序所接收的数据适合于绑定到模型,并且在不合适时给用户提供有用的信息,以帮助他们修正其问题的过程,防止在存储库中放置劣质或无意义的数据,并引起问题。该过程的第一部分,检查接收的数据是保持域模型完整性的方式之一。通过拒绝域环境中无意义的数据,可以防止应用程序中出现一些奇怪和意想不到的情况。第二部分,帮助用户修正问题,同样是重要的。如果不给用户提供一些与应用程序进行交互所需要的信息和反馈,他们会感到受挫和困惑,用户可能会因此直接停用这个应用程序。MVC框架对模型验证提供了广泛的支持。
从全局来看,发现逻辑仅是整个验证的很小的一部分。验证首先需要管理用户友好(本地化)的与验证逻辑相关的错误提示消息;当验证失败时,在把这些错误提示消息呈现给用户界面上,当然还要向用户提供从验证失败中恢复的机制。本节将演示如何使用这些基本特性。
一、控制器中验证模型
验证一个模型最直接的方式,是在动作方法中实现。
[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 ( ModelState.IsValid){
//插入appt数据到数据库
......
}
return View(); }
ModelState属性是控制器从它的基类集成而来的。
ModelState.AddModelError方法指定有问题的属性名(ClientName),和一条应该显示给用户的消息(“Please enter your name”)。
ModelState.IsValidField方法检查模型绑定器是否能够对一个属性赋值。对于Date属性就是这么做的,以确保模型绑定器能够解析用户所递交的值。
ModelState.IsValid属性检查是否有错误发生。如果在检查期间调用ModelState.AddModelError方法,或在创建Appointment对象时,模型绑定器遇到了问题,该方法会返回true。
二、用元数据指定验证规则 (模型中验证)
ASP.NET MVC中的一个设计原则是DRY(“Don't Repeat Yourself”)。相同功能或行为的代码只写一遍,然后在应用程序的任何地方都可以引用。这样减少了代码数量,降低使代码出错可能性,并且更容易维护。ASP.NET MVC和Entity Framework Code First中的验证规则设置,就是DRY的一个很好的实践。你可以在模型中通过添加数据注释来进行验证规则声明,然后这个验证规则就可以在应用程序的各个地方使用。元数据模型验证是在模型绑定时检查从HTTP请求接收的数据是否合规以保证数据的有效性,在收到无效数据时给出提示帮助用户纠正错误的数据。
1. 声明主键
要想在Entity Framework声明主键,最简单的方式就是不要声明,直接把属性名称设置为Id(或是类名+“Id”,或是ID),并将该属性指派为int类型即可。EF Code First会自动识别出这个字段就是表格里的主键,并且会加上自动编号的识别规格设置。
当希望使用其他域名当作主键时,就可能遇到一些麻烦。
这个错误的产生原因在于,任何EF里的模型,都被要求一定有主键,所以当EF无法识别出哪个字段是主键时,就会引发这个异常。
解决此问题的方法就是在No属性(Property)上加上一个key属性(Attriute),引用System.ComponentModel.DataAnnotations命名空间。
using System;
using System.ComponentModel.DataAnnotations;
namespace MvcGuestbook.Models
{
public class Guestbook
{
[Key]
public int No { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Message { get; set; }
public DateTime CreatedOn { get; set; } }
}
2. 声明必填字段
Required 注释告诉 EF 某一个特定属性是必需的。在 Title 属性中添加 Required 将强制 EF(和 MVC)确保该属性中包含数据。
[Required] public string Title { get; set; }
MVC 应用程序无需添加其他代码或更改标记,就能执行客户端验证,甚至还能使用属性和注释名称动态生成消息。
Required特性还将使被映射的属性不可为空来影响生成的数据库。请注意,Title 字段已经更改为“not null”。
Required属性对值类型来说不必要,如Datetime、Double、Float,因为值类型不能分配 null 值。但对于String等引用类型的属性来说,就可以根据情况进行设置。
还可以自定义Required验证出错时的提示。
[Required(ErrorMessage="wrong name")]
public string Name { get; set; }}
3. 声明允许NULL字段
声明为DateTime的属性,在数据库表格里的字段设置为NOT NULL。如果需要改变允许为空字段,可以加上一个问号,不需要引用任何命名空间。
using System;
using System.ComponentModel.DataAnnotations;
namespace MvcGuestbook.Models
{
public class Guestbook
{
[Key]
public int No { get; set; }
[Required]
public string Name { get; set; }
public string Emai { get; set; }
[Required]
public string Message { get; set; }
public DateTime? CreatedOn { get; set; }
public Nullable<DateTime> RepliedOn { get; set; }
} }
4. 声明字符串长度
(1)MaxLength和MinLength
我们也经常会在数据库中限定特定字段的字符串长度,以方便日后创建字段索引,可以使用MaxLength属性,引用System.ComponentModel.DataAnnotations。
[MaxLength(10), MinLength(5)] public string BloggerName { get; set; }
也可以在注释中指定ErrorMessage。
[MaxLength(10, ErrorMessage="BloggerName 必须在 10 个字符以下"), MinLength(5)] public string BloggerName { get; set; }
(2)StringLength
[StringLength(50, MinimumLength=1)] public string LastName { get; set; }
5.Compare
两个字段值必须一样,比如可以在要求用户重复输入密码时用。
[DataType(DataType.Password)] public String Password { get; set; } [DataType(DataType.Password)] [Compare("Password",ErrorMessage="密码要一致")] public String RptPassword { get; set; }
6.Range
一个数字(或实现了IComparable的类型)的值必须在指定范围内。
[Range(35,44)] public int Age { get; set; }
小数的情况:
[Range(typeof(decimal), “0.00”, “49.99”)] public decimal Price { get; set; }
7.声明字段默认值
在做数据库规划时,通常会规划一些系统字段,也就是由数据库本身自行指定默认值到这个字段上,创建信息的“创建时间”字段就会常常这样设计。如果CreatedOn字段希望能有默认值,且让.NET程序在新增信息到数据库时不用指定其值的话,那么你应该在该属性上加一个DatabaseGenerated属性,并出传入DatabaseGeneratedOption.Computed参数到DatabaseGenerated属性中,引用System.ComponentModel.DataAnnotations.Schema命名空间。
数据库创建完成后的架构如下图。
图中CreatedOn字段并没有被指派默认值上去,DatabaseGenerated特性,让EF不再追踪这个属性的任何对象变化,你可以在数据库中手动加上CreatedOn字段的默认值。
更改完字段后,记得单击“更新”按钮,才会将本次的更新反映到数据库中。
如果们在.net代码中,将guestbook兑现的CreatedOn属性设置日期为2012/02/02这个时间点。
但事实上CreatedOn字段被写入的信息,仍然是由数据库所指定的默认值。
- DatabaeGeneratedOption.Computed,计算列
- DatabaseGeneratedOption.Identity,标识自动加1
- DatabaseGeneratedOption.None,没有标识,不自动加1
8. 声明特定属性不是数据库中的字段
在EF Code First框架里,只要数据模型中出现公开属性,默认就会在数据库中创建一个对应的字段,但如果在数据模型中的属性,是一个动态计算的属性,我们并不想在数据库中新增对应的字段时,该怎么办?可以加上NotMapped属性(Attribute),引用System.ComponentModel.DataAnotations.Schema。
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MvcGuestbook.Models
{
public class Guestbook
{
[Key]
public int No { get; set; }
[Required]
[MaxLength(5)]
public string Name { get; set; }
[MaxLength(200)]
public string Emai { get; set; }
[Required]
public string Message { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime? CreatedOn { get; set; }
[NotMapped]
public string FamilyName
{
get
{
return this.Name.Substring(0, 1);
}
set
{
this.Name = value.Substring(0, 1) + this.Name.Substring(1);
}
}
}
}
9.正则验证属性
[RegularExpression(@"^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$", ErrorMessage = "邮箱格式不正确")] public string stuEmail { get; set; }
[RegularExpression(@”[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+/.[A-Za-z]{2,4}”)]
public string Email { get; set; } [RegularExpression(@"\d{11}", ErrorMessage = "手机格式不正确")] public string stuPhone { get; set}
如果不熟悉正则表达式的编写,可以百度搜索相应的正则表达式。推荐一个在线检测正则表达式的网站,一个正则表达式学习网站。
常见正则验证规则如下:
- Email,[RegularExpression(@"(\w)+(\.\w+)*@(\w)+((\.\w+)+)", ErrorMessage = "{0}格式不正确")]
- 手机,[RegularExpression("^[1][3578][0-9]{9}$",ErrorMessage="手机号码输入不正确")]
- 密码, [RegularExpression("^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,20}$", ErrorMessage = "密码必须包含数字和字母,且长度在6-20位之间")]
- QQ,[RegularExpression("[1-9][0-9]{4,14}",ErrorMessage="{0}输入不正确")]
[RegularExpression(@"\d{11}", ErrorMessage = "手机格式{0}不正确")] public string stuPhone { get; set}
上面使用{0}占位符,来显示用户的输入,并且形成友好提示。
10.自定义验证属性
前面讲的都是预先设定的数据注解,但是系统自由的数据注解肯定不适合所有的场合,所以有时候我们需要自定义数据注解。自定义数据注解有两种,一种是直接写在模型对象中,这样做的好处是验证时只需要关心一种模型对象的验证逻辑,缺点也是显而易见的,那就是不能重用。还有一种是封装在自定义的数据注解中,创建自己的Data Annotations Validation Attribute,优点是可重用,缺点是需要应对不同类型的模型。
(1)创建自定义的属性验证注解属性
using System.ComponentModel.DataAnnotaitons;
namespace ModelValidation.Infrastructure{
public class MustBeTrueAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
return value is bool && (bool)value;
}
}
}
public class Appointment { [Required(ErrorMessage = "Please enter your name")] [StringLength(50)] public string ClientName { get; set; } [DataType(DataType.Date)] [Required(ErrorMessage = "Please choose a date")] public DateTime AppointmentDate { get; set; } [MustBeTrue(ErrorMessage="You must accept the terms")]
public bool TermsAccepted { get; set; } }
using System.ComponentModel.DataAnnotations;
namespace ModelValidation.Infrastructure{
public class MFAttribute : RegularExpressionAttribute { public MFAttribute(): base("[mf]") { } public override string FormatErrorMessage(string name) { return "性别只能输入m(男)或者f(女)"; } }
}
[MF] public string sex { get; set; }
public class FutureDateAttribute: RequiredAttribute { public override bool IsValid(object value) { return base.IsValid(value) && ((DateTime)value) >DateTime.Now; } }
[DataType(DataType.Date)] [FutureDate(ErrorMessage="Please enter a date in the future")] public DateTime Date { get; set; }
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.AppointmentDate==null) { return true; } else { return !(app.ClientName == "Joe" && app.AppointmentDate.DayOfWeek == DayOfWeek.Monday); } } }
[NoJoeOnMondays] public class Appointment { public int AppointmentId { get; set; } [Required(ErrorMessage = "Please enter your name")] [StringLength(50)] public string ClientName { get; set; }
public string AppointmentDate{get;set;}
public bool TermsAccepted{get;set;}
}
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 (!TermsAccepted) { errors.Add(new ValidationResult("You must accept the terms")); } if (errors.Count == 0 && ClientName == "Joe" && Date.DayOfWeek == DayOfWeek.Monday) { errors.Add(new ValidationResult("Joe cannot book appointments on Mondays")); } return errors; } }
[Remote(“CheckUserName”, “Account”)] public string UserName { get; set; }
public JsonResult CheckUserName(string username) { if (IsUniqueName(userName) && IsForbiddenName(userName)) { return Json(true, JsonRequestBehavior.AllowGet); } else if (!IsUniqueName(userName)) { return Json("用户名不唯一!", JsonRequestBehavior.AllowGet); } else { return Json("用户名包含违禁词!", JsonRequestBehavior.AllowGet); }
}
三、显示验证消息
1.显示属性级验证消息
Html.ValidationMessageFor辅助器为单个模型属性显示验证错误,可以在相应字段旁显示验证错误消息。
<p>留言人邮箱:@Html.TextBoxFor(g=>g.AuthorEmail) @Html.ValidationMessageFor(g=>g.AuthorEmail)</p>
.field-validation-error { color: #f00;} //验证错误是消息文字的颜色 .input-validation-error { //输入文本框验证错误时的边框和背景颜色 border: 1px solid #f00; background-color: #fee; }
如果验证有效则使用的样式是,
.field-validation-valid {
display: none;
}
@using(Html.BeginForm()){ @Html.ValidationSummary()
<p>留言人邮箱:@Html.TextBoxFor(g=>g.AuthorEmail) </p> <p>标题:@Html.TextBoxFor(g=>g.Title)</p> <p>内容:@Html.TextBoxFor(g=>g.Content,new { style = "width:200px;height:100px;" }) </p> <input type="submit" value="发布" /> }
.validation-summary-errors { font-weight: bold; color: #f00; } .validation-summary-valid { display: none; }
四、执行客户端验证
以上的验证技术都是服务器端验证的示例。在Web应用程序中,用户会期望得到即时的验证反馈——对服务器不做任何递交。这成为客户端验证,这通常是用JavaScript实现的。用户输入的数据在被发送给服务器之前就进行验证,给用户提供即时反馈并修正错误的机会。MVC框架支持渐进式客户端验证(Unobrusive Client-Side Validation)。术语“渐进式(Unobtrusive)”意指,在生成的HTML元素上添加验证标签属性来表示验证规则。这些标签属性由包含在MVC框架中的JavaScript库进行解释,框架又转而对jQuery验证库进行配置,由验证库完成实际的验证工作。在实际工作中,可以选择对属性级问题使用客户端验证,而对整个模型则使用服务器端验证。
1.启用客户端验证
(1)Web.config文件中的两个设置
... <appSettings> ... <add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />
...
</appSettings>
为了使客户端验证生效,这两个设置都必须为true。也可以配置基于个别视图的客户端验证,只需在视图的一个razor代码块中设置HtmlHelper.ClientValidationEnabled和HtmlHelper.UnobtrusiveJavaScriptEnabled即可。
(2) 添加NuGet包
添加处理那些注解属性的JavaScript包,并检查用户在表单中输入的数据。在Visual StudioTools->Library Package Manager菜单中选择Package Manager Console,并输入如下命令:
Install-Package jQuery -version 1.10.2 Install-Package jQuery.Validation -version 1.11.1 Install-Package Microsoft.jQuery.Unobtrusive.Validation -version 3.0.0
这些包将文件添加到Scripts文件夹,可以使用script元素将它们添加到布局中。
添加到布局中的script元素的次序是很重要的,必须先添加jQuery库,后面跟随jQuery验证库,最后添加微软渐进式验证库。
(3)使用客户端验证
一旦已经启用了客户端验证,并确保在布局中引用了JavaScript库,就可以执行客户端验证了。最简单的方式是运用前面服务器端验证所使用的元数据注解属性,如Required、Range以及StringLength等。
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; } }
此时在浏览器中呈现的反馈是即时的,不需要形成对服务器的请求。执行验证的JavaScript代码会组织递交表单,直到不再出现验证错误为止。当用户改正错误时,反馈是即时的。这种动态反馈帮助用户提供应用程序所需要的数据,而不必将表单提交到服务并等待响应。
MVC客户端验证的一个好处是,运用于客户端和服务器端的指定验证规则的属性是相同的。这意味着,从不支持JavaScript的浏览器而来的数据会得到同样的验证,而不需要任何额外的努力