在这一小节,我们将会给Movie模型添加验证规则,这样当用户试图新加或者编辑一部电影的数据时将会确保这些数据是符合验证逻辑的。
保持事请DRY
ASP.NET MVC的代码设计原则之一就是DRY(Don't Repeat Yourself,不用重复自身)。ASP.NET MVC鼓励在一个地方定义功能或者行为,然后在程序中需要的地方引用它们。这会降低需要编写的代码数量,降低代码出错率并且使代码易于维护。
ASP.NET MVC和Entity Framework所支持的逻辑验证方式正是DRY原则的典型实践。你可以在某一个地方(一般是在model中)以声明的方式指定验证规则,这些规则就会在程序的任何地方执行。
下面让我们来看看如何在我们的程序中使用这种验证方式。
在Movie模型上添加验证规则
我们从在Movie类上添加一些验证规则开始。
打开Movie.cs文件。在文件的顶部添加一个using语句引入System.ComponentModel.DataAnnotations命名空间:
using System.ComponentModel.DataAnnotations;
注意到命名空间中并不包含System.Web这样的字样。DataAnnotations提供了一组内置的验证特性(validation attributes),你可以以声明式编程的方式(注:.net所提供的特性Attribute是一种声明式编程的典型实践,比如常用的如果想将一个类序列化需要给类声明上加上[Serializable]特性,声明式编程可以大大提高代码的可读性)将这些特性应该用到任何的类或者属性上。
现在来修改一下Movie类,给它加上内置的Required,StringLength和Range验证特性。使用如下的代码:
public class Movie
{ public int ID { get; set; } [Required] public string Title { get; set; } [DataType(DataType.Date)] public DateTime ReleaseDate { get; set; } [Required] public string Genre { get; set; } [Range(1, 100)]
[DataType(DataType.Currency)] public decimal Price { get; set; } [StringLength(5)] public string Rating { get; set; } }
运行程序,在程序运行时将会再次看到如下错误
The model backing the 'MovieDBContext' context has changed since the database was created. Consider using Code First Migrations to update the database (http://go.microsoft.com/fwlink/?LinkId=238269).
我们仍然需要使用migrations来更新数据库结构。生成项目,打开Package Manager Console窗口输入下面的命令:
add-migration AddDataAnnotationsMig
update-database
命令执行完毕后,Visual Studio会打开一个类文件,类文件的名字是第一个命令指定的(AddDataAnnotationMig),这个类从DbMIgration派生,从类中的Up方法可以看到为数据库增加约束的代码。Title和Genre字段的值不允许为null了,Rating字段要求长度不超过5.
验证特性指定了它们所应用到的model的属性应该遵守的规则。Required特性之处某个属性必须有一个值;在这里例子里,Movie的Title,ReleaseDate,Genre和Price属性必须有一个值。Range特性约定了一个指定的范伟。StringLength特性让我们可以为一个string类型的属性设置最大长度,也可以设置最小长度。内部类型(比如decimal,int,float,DateTIme)默认就是Required,所以不必加上Required特性。(注:所谓内部类型,原文是Intrinsic Types,我想指的应该是值类型)。
Code First保证了我们再一个model上附加的验证规则在保存到数据库之前一定会执行。例如,下面的代码,当SaveChanges方法被调用的时候,会抛出一个异常,因为好几个Required的Movie的属性没有值并且Pirce属性的值是0,但是Range特性知名了允许的值是1到100.
MovieDBContext db = new MovieDBContext(); Movie movie = new Movie(); movie.Title = "Gone with the Wind"; movie.Price = 0.0M; db.Movies.Add(movie); db.SaveChanges(); // <= Will throw server side validation exception
.Net Framework自动执行的验证规则可以使我们的应用程序更加健壮。同样也确保了我们不会忘记验证一些事情,也确保了不会应为我们的疏忽而让脏数据存储到数据库中。
下面是修改后的Movies.cs文件内容。
using System; using System.Data.Entity; using System.ComponentModel.DataAnnotations; namespace MvcMovie.Models
{ public class Movie
{ public int ID { get; set; } [Required] public string Title { get; set; } [DataType(DataType.Date)] public DateTime ReleaseDate { get; set; } [Required] public string Genre { get; set; } [Range(1, 100)] [DataType(DataType.Currency)] public decimal Price { get; set; } [StringLength(5)] public string Rating { get; set; } } public class MovieDBContext : DbContext { public DbSet<Movie> Movies { get; set; } } }
ASP.NET MVC中的验证错误UI
重新运行程序导航到/Movies.
点击Create New超链接来添加一部新电影。使用一些非法值填写表单中的数据然后单击Create按钮。
注意一下表单是如何自动使用加粗的红色文字来高亮显示有非法数据的文本框的,并且这些错误信息是紧挨着各自的文本框显示的。数据验证即在客户端进行(在客户端使用javascript验证)也在服务端进行。
一个显著的好处是我们并不需要修改MoviesController中的代码或者修改Create.cshtml文件来让验证信息呈现在界面上。我们早先创建的controller和view
会自动使用我们通过特性指定在Movie类的属性上的验证规则。
你可能注意到了属性Title和Genre的验证规则要在我们提交表单的时候(通过点击Create按钮)才会触发,或者是我们再文本框里输入了值,但是有删除了的时候回触发。对于那些初始时是空的值(比如在Create视图中的某些字段),如果他们只有Required验证特性,你可以指定下列的触发验证的方式:
- Tab into the field. (获得焦点)
- Enter some text. (输入一些文字)
- Tab out. (失去焦点)
- Tab back into the field.(再次获得焦点)
- Remove the text. (删除内容)
- Tab out.(失去焦点)
上面的操作序列将会触发Required验证,并不需要点击提交按钮。不输入任何内容只是点击提交按钮将会触发客户端验证,表单数据不会被发送到服务器除非客户端验证全部通过。你可以通过在HTTPPost的方法里加断点或者使用Fiddler工具或者使用IE9提供的开发者工具来验证这一点。
Create视图和Create方法中的验证是如何发生的
你可能好奇,我们并没有修改之前在controller和view中生成的代码,但是在浏览器中确可以显示验证错误的信息。下面的代码是MovieController中Create方法的代码,他们和在前面章节中创建时的内容一模一样。
// // GET: /Movies/Create public ActionResult Create() { return View(); } // // POST: /Movies/Create [HttpPost] public ActionResult Create(Movie movie) { if (ModelState.IsValid) { db.Movies.Add(movie); db.SaveChanges(); return RedirectToAction("Index"); } return View(movie); }
第一个HttpGet版本的Create方法用于展示初始的Create页面。第二个HttpPost版本的Create方法用于处理post回来的表单。第二个Create方法调用了ModelState.IsValid来检查传回的Movie数据是否通过验证。调用这个属性会执行所有应用在该对象上的验证特性,如果有验证错误,Create方法会再次展示表单数据,如果没有验证错误,方法会将新数据添加到数据库中。在我们使用的示例中,表单数据只有通过了客户端的验证,才会被发送到服务器,如果我们禁用了浏览器的javascript,客户端验证就失效了,这时候只能通过Create方法在服务器端通过调用ModelState.IsValid来检查传入的数据是否正确。
我们可以再HttpPost版本的Create方法中设置一个端点类验证这个方法是否被调用,如果我们禁用浏览器javascript脚本,包含错误数据的表单就会提交到服务器,断点就会被命中。即使没有了Javascript,我们仍然会执行所有的验证。下面的图片展示了如何在IE中禁用Javascript。
下面的图片展示了如何在FireFox浏览器中禁用Javascript
下面的图片展示了如何在chrome浏览器中禁用Javascript
下面是在前面文章中生成的Create.cshtml视图模板中的代码。这个视图文件被用来展示初始的表单页面和如果发生验证错误时展示错误信息。
@model MvcMovie.Models.Movie @{ ViewBag.Title = "Create"; } <h2>Create</h2> @using (Html.BeginForm()) { @Html.ValidationSummary(true) <fieldset> <legend>Movie</legend> <div class="editor-label"> @Html.LabelFor(model => model.Title) </div> <div class="editor-field"> @Html.EditorFor(model=> model.Title) @Html.ValidationMessageFor(model =>
model.Title) </div> <div class="editor-label"> @Html.LabelFor(model => model.ReleaseDate) </div> <div class="editor-field"> @Html.EditorFor(model=> model.ReleaseDate) @Html.ValidationMessageFor(model =>
model.ReleaseDate) </div> <div class="editor-label"> @Html.LabelFor(model => model.Genre) </div> <div class="editor-field"> @Html.EditorFor(model=> model.Genre) @Html.ValidationMessageFor(model =>
model.Genre) </div> <div class="editor-label"> @Html.LabelFor(model => model.Price) </div> <div class="editor-field"> @Html.EditorFor(model=> model.Price) @Html.ValidationMessageFor(model =>
model.Price) </div> <div class="editor-label"> @Html.LabelFor(model => model.Rating) </div> <div class="editor-field"> @Html.EditorFor(model=> model.Rating) @Html.ValidationMessageFor(model =>
model.Rating) </div> <p> <input type="submit" value="Create" /> </p> </fieldset> } <div> @Html.ActionLink("Back to List", "Index") </div> @section Scripts { @Scripts.Render("~/bundles/jqueryval") }
注意代码使用Html.EditorFor辅助方法来为Movie的属性生成<input>标签。紧挨着是Html.ValidationMessageFor的调用。这两个方法需要controller传递给view的model对象。他们会自动查看应用在model对象上的验证特性并且在适当的时候显示错误信息。
这种验证方式的好处是controller和view都不需要知道任何关于验证的事情,不管是验证规则还是错误信息。验证规则和错误信息只需要在model中指定即可。同样的验证规则会自动应用到Edit视图和其他任何你可能需要编辑数据的视图模板中。
如果你想要修改验证逻辑,那么只需要在一个地方进行就该就行了,就是在model中为某些属性更换验证特性。你不需要担心因为验证在程序的不同部分
执行而产生不一致性。所有的验证逻辑都在一个地方定义,但是可以在任意地方使用。这就使得代码非常整洁,并且易于维护和扩展。也就是说你完完全全遵守了DRY原则。
为Movie模型添加格式化信息
打开Movie.cs文件查看一下里面的内容。System.ComponentModel.DataAnnotations命名空间不仅提供了一组内置的验证特性,也提供了格式化特性。我们已经为ReleaseDate和Price属性添加了一个DataType枚举值,下面的代码展示了附加了适当的DisplayFormat特性的ReleaseDate和Price属性。
[DataType(DataType.Date)] public DateTime ReleaseDate { get; set; } [DataType(DataType.Currency)] public decimal Price { get; set; }
DataType特性并不是验证特性,这个特性告诉浏览器如何来渲染HTML内容,在上面的示例中,DataType.Date特性把movie的ReleaseDate展示成了日期,而没有展示出具体的时间点。下面的DataType特性并不会验证数据的格式。
[DataType(DataType.EmailAddress)]
[DataType(DataType.PhoneNumber)]
[DataType(DataType.Url)]
上面列出的几个特性仅仅是告诉浏览器将数据格式化成那种类型(例如为url渲染成<a>为电子邮箱渲染成<a href=mailto:xxx>,如果要验证数据格式的话,使用RegularExpression特性)。
还有一种替代DataType特性的方法,你可以显式指定一个DataFormatString值,下面代码展示了使用DataFormatString的ReleaseDate属性,你可以使用这个特性来指明你不想把时间作为ReleaseDate的一部分。
[DisplayFormat(DataFormatString = "{0:d}")] public DateTime ReleaseDate { get; set; }
下面的代码把Price属性格式化为货币。
[DisplayFormat(DataFormatString = "{0:c}")] public decimal Price { get; set; }
完整的Movie类的代码如下
public class Movie { public int ID { get; set; } [Required] public string Title { get; set; } [DataType(DataType.Date)] public DateTime ReleaseDate { get; set; } [Required] public string Genre { get; set; } [Range(1,100)] [DataType(DataType.Currency)] public decimal Price { get; set; } [StringLength(5)] public string Rating { get; set; } }
运行程序浏览Movies,ReleaseDate和Price都很完美的格式化了。下面的图片显示了简体中文文化环境下的ReleaseDate和Price的值。(最终ReleaseDate和Price的值会被格式化成什么样子取决于计算机的设置,如果计算机的时钟、区域和语言设置,如果设置为美国,那么就会显示$而不是¥)
在下一部分,我们会回顾一下代码,然后为自动生成的Details和Delete方法做一些改进。