ASP.NET MVC with Entity Framework and CSS一书翻译系列文章之第四章:更高级的数据管理

  在这一章我们将学习如何正确地删除分类信息,如何向数据库填充种子数据,如何使用Code First Migrations基于代码更改来更新数据库,然后学习如何执行带有自定义错误消息的验证。

注意:如果你想按照本章的代码编写示例,你必须完成第三章或者直接从www.apress.com下载第三章的源代码。

4.1 删除作为一个外键的实体

  到目前为止,我们已经完成了向站点添加搜索和过滤的功能,并且我们已经可以向站点添加一些分类和产品信息。下面我们将考虑当尝试删除实体信息时会发生什么事情。

  首先,向站点添加一个名为Test的新分类,然后再添加一个名为Test的产品,并将该产品的分类指定为分类Test。现在,我们使用分类的索引(Index)页面删除Test分类,然后提交删除操作,这时,站点将会抛出一个错误,如图4-1所示。

图4-1:当试着删除一个分类时,发生的一个参照完整性错误

  之所以会发生这个错误,是因为Products表中的CategoryID列是一个外键列,该列引用Categories表中的ID列。当一个分类被删除时,Products表没有发生改变,导致Products表中的categoryID外键列引用Categories表中一个已经被删除的分类ID,这就会导致一个错误发生。

  为了解决这个问题,我们需要修正由基架生成的代码,以将受影响的Products表中的外键列的值设置为null。修改\Controllers\CategoriesController.cs文件中的Delete方法(HttpPost版本)以符合下面列出的粗体代码:

 1 // POST: Categories/Delete/5
 2 [HttpPost, ActionName("Delete")]
 3 [ValidateAntiForgeryToken]
 4 public ActionResult DeleteConfirmed(int id)
 5 {
 6     Category category = db.Categories.Find(id);
 7 
 8     foreach(var p in category.Products)
 9     {
10         p.CategoryID = null;
11     }
12 
13     db.Categories.Remove(category);
14     db.SaveChanges();
15     return RedirectToAction("Index");
16 }

  这段代码添加了一个简单的foreach循环遍历Category实体的Products导航属性,然后将每一个Product对象的CategoryID属性值设置为null。现在,我们再尝试着删除Test分类,该分类将会被删除,并且不会发生错误,同时数据库中的Products表的Test产品的CategoryID类会被设置为null。

4.2 启用Code First Migrations数据库迁移以及向数据库填充种子数据

  目前,我们都是在站点中手动输入数据来创建分类和产品信息。在开发环境中,对于测试一个新的功能,这是一种比较好的方法,但是,如果我们想在其它环境中可靠地、轻松地重建相同的数据应该怎么办呢?这就需要Entity Framework的一个称之为播种(seeding)的特性来发挥作用了。播种(seeding)被用于以编程的方式在数据库中创建实体,并且可以控制特定环境下的输入。

  我们现在就开始学习如何使用称之为Code First Migrations的特性来为数据库填充种子数据。迁移(Migrations)是基于对模型类的代码的修改来更新数据库架构的一种方法。从现在开始,本书从始至终都会使用迁移(Migrations)的方法来更新数据库架构。

  首先,我们需要更新web.config文件中的数据库连字符串,以便创建一个用来测试种子数据是否正确工作的新数据库。更新StoreContext的connectionString属性如下面的代码所示以创建一个名为BabyStore2.mdf的新数据库。

1 <connectionStrings>
2   <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\aspnet-BabyStore-20161229112118.mdf;Initial Catalog=aspnet-BabyStore-20161229112118;Integrated Security=True"
3     providerName="System.Data.SqlClient" />
4   <add name="StoreContext" connectionString="Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\BabyStore2.mdf;Initial Catalog=BabyStore2;Integrated Security=True"
5     providerName="System.Data.SqlClient" />
6 </connectionStrings>

  尽管在本书中connectionString显示为多行,但确保在Visual Studio中保持它为一行代码。

4.2.1 启用Code First Migration

  在主菜单栏中选择【视图】->【其他窗口】->【程序包管理器控制台】打开程序包管理器控制台窗口。在这个窗口中可以输入使用迁移(Migrations)的所有命令。我们要做的第一件事情就是对要执行更新的数据库的上下文启动迁移(Migrations)操作。如果只有一个上下文,则上下文可选。

  在这一章中,我们感兴趣的是产品和分类数据,因此,在程序包管理器控制台中输入下列命令:

1 Enable-Migrations -ContextTypeName BabyStore.DAL.StoreContext

  如果我们正确地执行完毕命令,Visual Studio应该如图4-2所示。

图4-2:启用Code First Migrations

  启用迁移(Migrations)的同时在项目根目录下也添加了一个名为Migrations的文件夹,在其中包含一个名为Configuration.cs的文件,该文件用于配置迁移(Migrations)。图4-3显示了解决方案资源管理器中的新建的文件夹和文件。

图4-3:当启用迁移(Migrations)的时候所创建的Migrations文件夹和Configuratiion文件

  下一步,我们在程序包管理器控制台中输入以下命令以添加一个称之为InitialDatabase的初始迁移(initial migration)。

1 add-migration InitialDatabase

  该命令在Migrations文件夹下创建了一个文件名格式为<TIMESTAMP>_InitialDatabase.cs的新文件,<TIMESTAMP>表示创建该文件时的时间。Up方法用于创建数据库表,Down方法用于删除它们。在这个新文件中所包含的代码如下所示。我们可以看到Up方法包含重建Categories和Products表的一些代码(伴随着数据类型和键的重建)。

 1 namespace BabyStore.Migrations
 2 {
 3     using System;
 4     using System.Data.Entity.Migrations;
 5 
 6     public partial class InitialDatabase : DbMigration
 7     {
 8         public override void Up()
 9         {
10             CreateTable(
11                 "dbo.Categories",
12                 c => new
13                 {
14                     ID = c.Int(nullable: false, identity: true),
15                     Name = c.String(),
16                 })
17                 .PrimaryKey(t => t.ID);
18 
19             CreateTable(
20                 "dbo.Products",
21                 c => new
22                 {
23                     ID = c.Int(nullable: false, identity: true),
24                     Name = c.String(),
25                     Description = c.String(),
26                     Price = c.Decimal(nullable: false, precision: 18, scale: 2),
27                     CategoryID = c.Int(),
28                 })
29                 .PrimaryKey(t => t.ID)
30                 .ForeignKey("dbo.Categories", t => t.CategoryID)
31                 .Index(t => t.CategoryID);
32 
33         }
34 
35         public override void Down()
36         {
37             DropForeignKey("dbo.Products", "CategoryID", "dbo.Categories");
38             DropIndex("dbo.Products", new[] { "CategoryID" });
39             DropTable("dbo.Products");
40             DropTable("dbo.Categories");
41         }
42     }
43 }

  这段代码不就就会用于创建一个新的数据库,在这之前,我们需要更新Migrations\Configuratiion.cs文件中的Seed()方法以向数据库添加一些测试数据。

4.2.2 向数据库填充种子数据

  当使用Code First Migrations时,Seed方法用于向数据库添加测试数据。一般情况下,只有当数据库被创建时或者向该方法中添加新的数据时,这些测试数据才会被添加到数据库中。当数据模型被更改时,数据不会丢失。当迁移到生成环境时,我们需要确定哪些数据是初始数据而不是测试数据,以及更加恰当地更新Seed方法。

  为了将新的类别和产品数据添加到数据库中,我们需要更新Migrations\Configurations.cs文件中的Seed方法,Seed方法修改后的代码如下所示:

 1 namespace BabyStore.Migrations
 2 {
 3     using System.Data.Entity.Migrations;
 4     using System.Linq;
 5     using Models;
 6     using System.Collections.Generic;
 7 
 8     internal sealed class Configuration : DbMigrationsConfiguration<BabyStore.DAL.StoreContext>
 9     {
10         public Configuration()
11         {
12             AutomaticMigrationsEnabled = false;
13         }
14 
15         protected override void Seed(BabyStore.DAL.StoreContext context)
16         {
17             var categories = new List<Category>
18             {
19                 new Category { Name = "Clothes" },
20                 new Category { Name = "Play and Toys" },
21                 new Category { Name = "Feeding" },
22                 new Category { Name = "Medicine" },
23                 new Category { Name = "Travel" },
24                 new Category { Name = "Sleeping" }
25             };
26 
27             categories.ForEach(c => context.Categories.AddOrUpdate(p => p.Name, c));
28             context.SaveChanges();
29 
30             var products = new List<Product>
31             {
32                 new Product { Name = "Sleep Suit", Description = "For sleeping or general wear", Price = 4.99M, CategoryID = categories.Single( c => c.Name == "Clothes").ID },
33                 new Product { Name = "Vest", Description = "For sleeping or general wear", Price = 2.99M, CategoryID = categories.Single( c => c.Name == "Clothes").ID },
34                 new Product { Name = "Orange and Yellow Lion", Description = "Makes a squeaking noise", Price = 1.99M, CategoryID = categories.Single( c => c.Name =="Play and Toys").ID },
35                 new Product { Name = "Blue Rabbit", Description = "Baby comforter", Price = 2.99M, CategoryID = categories.Single( c => c.Name == "Play and Toys").ID },
36                 new Product { Name = "3 Pack of Bottles", Description = "For a leak free drink everytime", Price = 24.99M, CategoryID = categories.Single( c => c.Name == "Feeding").ID },
37                 new Product { Name = "3 Pack of Bibs", Description = "Keep your baby dry when feeding", Price = 8.99M, CategoryID = categories.Single( c => c.Name == "Feeding").ID },
38                 new Product { Name = "Powdered Baby Milk", Description = "Nutritional and Tasty", Price = 9.99M, CategoryID = categories.Single( c => c.Name == "Feeding").ID },
39                 new Product { Name = "Pack of 70 Disposable Nappies", Description = "Dry and secure nappies with snug fit", Price = 19.99M, CategoryID = categories.Single( c => c.Name == "Feeding").ID },
40                 new Product { Name = "Colic Medicine", Description = "For helping with baby colic pains", Price = 4.99M, CategoryID = categories.Single( c => c.Name == "Medicine").ID },
41                 new Product { Name = "Reflux Medicine", Description = "Helps to prevent milk regurgitation and sickness", Price  =4.99M, CategoryID = categories.Single( c => c.Name == "Medicine").ID },
42                 new Product { Name = "Black Pram and Pushchair System", Description = "Convert from pram to pushchair, with raincover", Price = 299.99M, CategoryID = categories.Single( c => c.Name == "Travel").ID },
43                 new Product { Name = "Car Seat", Description="For safe car travel", Price = 49.99M, CategoryID = categories.Single( c => c.Name == "Travel").ID },
44                 new Product { Name = "Moses Basket", Description = "Plastic moses basket", Price = 75.99M, CategoryID = categories.Single( c => c.Name == "Sleeping").ID },
45                 new Product { Name = "Crib", Description = "Wooden crib", Price = 35.99M, CategoryID = categories.Single( c => c.Name == "Sleeping").ID },
46                 new Product { Name = "Cot Bed", Description = "Converts from cot into bed for older children", Price = 149.99M, CategoryID = categories.Single( c => c.Name == "Sleeping").ID },
47                 new Product { Name = "Circus Crib Bale", Description = "Contains sheet, duvet and bumper", Price = 29.99M, CategoryID = categories.Single( c => c.Name == "Sleeping").ID },
48                 new Product { Name = "Loved Crib Bale", Description = "Contains sheet, duvet and bumper", Price = 35.99M, CategoryID = categories.Single( c => c.Name == "Sleeping").ID }
49             };
50 
51             products.ForEach(c => context.Products.AddOrUpdate(p => p.Name, c));
52             context.SaveChanges();
53         }
54     }
55 }

  这段代码分别创建了一个分类和产品对象的列表,并将它们保存到数据库中。为了解释其工作原理,我们将分析类别部分的代码。首先,分别创建了一个名为categories的变量和一个分类对象的列表,并将分类对象的列表赋值给categories变量,具体代码如下所示:

var categories = new List<Category>
{
  new Category { Name="Clothes" },
  new Category { Name="Play and Toys" },
  new Category { Name="Feeding" },
  new Category { Name="Medicine" },
  new Category { Name="Travel" },
  new Category { Name="Sleeping" }
};

  下一行代码是categories.ForEach(c => context.Categories.AddOrUpdate(p => p.Name, c));,如果在数据库中不存在同名的分类信息,这行代码将会添加一条分类信息,否则将会更新它。在这个例子中,我们假设分类的名称都是唯一的。

  最后一部分代码是context.SaveChanges();当调用该方法时,会将改动保存到数据库中。在这个文件中,我们调用该方法两次,但这不是必须的,我们可以只调用它一次。但是,如果在写入数据库时发生一个错误,每保存一个实体类型之后都调用它一次,可以帮助我们定位有问题的源代码。

  如果我们碰到这么一个场景,我们需要使用非常相似的数据向数据库添加多个实体对象(比如,两个同名的分类),我们可以逐个添加到上下文中,如下代码所示:

1 context.Categories.Add(new Category { Name = "Clothes" });
2 context.SaveChanges();
3 context.Categories.Add(new Category { Name = "Clothes" });
4 context.SaveChanges();

  再次重申,上述代码没有必要多次调用SaveChanges方法保存改动,但是这样做有利于帮助我们查出有错误的源代码。添加产品信息的代码具有和添加分类信息的代码具有同样的模式,除了我们使用下列代码来基于分类实体生成产品信息中的CategoryID字段:CategoryID = categories.Single(c => c.Name == "Clothes").ID。

4.2.3 使用初始数据库迁移创建数据库

  现在我们准备使用来自于Seed方法中的测试数据来创建一个新的数据库。在程序包管理器控制台中,运行如下命令:

1 update-database

  如果工作正常,我们应该被告知正在应用迁移,并且正在运行Seed方法,如图4-4所示。

图4-4:使用update-database命令成功更新数据库后的输出

  命名为BabyStore2.mdf的新数据库被创建在项目根目录下的App_Data文件夹中。当运行update-database命令时,Migrations\Configuration.cs文件中的Up方法被调用,该方法用于创建数据库中的表。如果要查看该数据库,我们可以打开SQL Server对象资源管理器,然后导航到BabyStore2.mdf数据库。如果我们已经打开了SQL Server对象资源管理器,可能需要点击刷新按钮才能显示出新创建的数据库。右键点击Products表,然后从菜单中选择【查看数据】选项来查看表中数据。图4-5显示了Products表中的数据。这些数据在Seed方法运行时被创建。

图4-5:运行Seed方法所创建的Products表的数据

  提示:如果我们在App_Data文件夹中看不到任何东西,则在解决方案资源管理器中点击“显示所有文件”按钮即可。

  不调试运行站点,我们现在会看到由Seed方法所生存的新的分类(如图4-6)和产品(如图4-7)信息。

图4-6:分类索引(Index)页所显示的由Seed方法生成的数据

图4-7:产品索引(Index)页所显示的由Seed方法生成的数据

4.3 添加数据验证以及格式化模型类之间的约束

  截止到目前为止,在这个站点中所录入的数据或以某种格式显示的数据(比如,货币)都没有进行验证。举个例子,我们完全可以输入一个名称为23的分类名称。又或者,一个用户可以不输入分类名称,提交后也会保存到数据库中,但是在显示分类的索引(Index)页面时,应用程序会抛出一个错误。

  删除我们刚刚创建的分类名称为23的分类。如果我们也创建了一个分类名称为空的分类,我们在SQL Server对象资源管理器中也把此分类删除。

  在第2章,我们学习了如何使用MetaDataType类来注解一个已经存在的类,而不是直接在该类中使用注解。但是,在本书剩余章节,为了简单起见,我们直接在类本身进行修改。

4.3.1 向Category类添加验证和格式

  我们打算向分类信息添加如下验证和格式:

  • 名称字段不能为空
  • 名称字段只能接受字母
  • 名称字段的长度在3-50个字母之间

  为了完成上述目的,我们必须修改Models\Category.cs文件,代码如下所示:

 1 using System.Collections.Generic;
 2 using System.ComponentModel.DataAnnotations;
 3 
 4 namespace BabyStore.Models
 5 {
 6     public class Category
 7     {
 8         public int ID { get; set; }
 9 
10         [Required]
11         [StringLength(50, MinimumLength = 3)]
12         [RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]
13         public string Name { get; set; }
14         public virtual ICollection<Product> Products { get; set; }
15     }
16 }

  [Required]特性使得该属性是必须的,也就是说,它不能为null或空。而[StringLength(50, MinimumLength = 3)]特性指示在这个字段中输入的字符串的长度必须再3到50个字符之间。最后一个特性[RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]使用一个正则表达式指示该字段只能包含字母和空格,还必须以大写字母开头。简而言之,这个表达式的意思就是说只能以大写字母开头,后跟多个字母或空格。在本书中,我们不会涵盖正则表达式的知识,因为在网上有多个指导手册可用。如果我们想深入了解正则表达式的相关知识,可以试着使用站点https://regex101.com/

  下一步,我们不调试启动站点,然后点击分类链接。图4-8显示结果页面。我们会看到没有显示分类索引(Index)页面,而是显示了一个错误信息,该错误信息提示我们StoreContext上下文的模型在数据库创建完成后已发生更改。

图4-8:当模型和数据库不同步时的信息显示

  造成这一问题的原因是因为Category类现在已发生改变,它需要将此改变应用到数据库中。我们添加到名称列的三个特性中的两个特性需要应用到数据库中。这将确保该列不能为空,并且对该列应用maxLength属性。正则表达式和最小长度不适用于数据库。

  为了解决这个问题,打开程序包管理器控制台,使用下列命令添加一个新的迁移(Migration):

1 add-migration CategoryNameValidation

  新的迁移文件将会被创建,该文件所包含的代码用于更新Categories表中的Name列。

 1 namespace BabyStore.Migrations
 2 {
 3     using System;
 4     using System.Data.Entity.Migrations;
 5 
 6     public partial class CategoryNameValidation : DbMigration
 7     {
 8         public override void Up()
 9         {
10             AlterColumn("dbo.Categories", "Name", c => c.String(nullable: false, maxLength: 50));
11         }
12 
13         public override void Down()
14         {
15             AlterColumn("dbo.Categories", "Name", c => c.String());
16         }
17     }
18 }

  为了将改动应用于数据库,在程序包管理器控制台中运行下列命令:

1 update-database

  现在,数据库中的Categories表中的Name列将会被更新,如图4-9所示。注意那个允许为空的复选框现在没有被勾选,并且数据类型变成了nvarchar(50)。

图4-9:更新Name列的Categories表和T-SQL脚本

  现在再次启动站点,点击主页上的分类链接,然后点击Create New链接。现在试着添加一个名称为空的分类,这个时候站点会通知我们这是不允许的,如图4-10所示。

图4-10:当尝试添加一个空的分类时会显示一个错误信息

  现在我们再尝试创建一个名为Clothes 2的分类。图4-11显示另一个信息提示。

图4-11:尝试在分类名称中输入数字时所显示的提示信息

  就像我们所看到的那样,图4-11所显示的消息是用户不友好的。幸运的是,ASP.NET MVC允许我们重写错误信息的文本,只需要在特性中输入一个额外的参数即可。为了添加更加友好的错误提示,修改\Models\Category.cs文件为下列所示的代码:

 1 using System.Collections.Generic;
 2 using System.ComponentModel.DataAnnotations;
 3 
 4 namespace BabyStore.Models
 5 {
 6     public class Category
 7     {
 8         public int ID { get; set; }
 9 
10         [Required(ErrorMessage = "分类名称不能为空!")]
11         [StringLength(50, MinimumLength = 3, ErrorMessage = "请确保输入的分类名称的长度在3~50个字符之间!")]
12         [RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$", ErrorMessage = "请确保输入的分类名字以大写字母开头,后面只能是字母或空白!")]
13         public string Name { get; set; }
14         public virtual ICollection<Product> Products { get; set; }
15     }
16 }

  现在不调试启动站点,再次创建一个名为Clothes 2的分类,这次所显示的错误信息更加有意义,如图4-12所示。

图4-12:自定义错误验证信息

4.3.2 向Product类添加格式和验证

  Product类所包含的属性需要更加复杂的特性,比如格式化货币、多行显示等。修改Models\Product.cs文件为下列所示的代码:

 1 using System.ComponentModel.DataAnnotations;
 2 
 3 namespace BabyStore.Models
 4 {
 5     public partial class Product
 6     {
 7         public int ID { get; set; }
 8         [Required(ErrorMessage = "产品名称不能为空!")]
 9         [StringLength(50, MinimumLength = 3, ErrorMessage = "请确保产品名称的长度在3-50个字符之间!")]
10         [RegularExpression(@"^[a-zA-Z0-9'-'\s]*$", ErrorMessage = "请确保产品名称只能为字母或数字!")]
11         public string Name { get; set; }
12         [Required(ErrorMessage = "产品描述不能为空!")]
13         [StringLength(200, MinimumLength = 10, ErrorMessage = "请确保输入的产品描述信息的长度在10-200字符之间!")]
14         [RegularExpression(@"^[,;a-zA-Z0-9'-'\s]*$", ErrorMessage = "请确保产品描述信息只能是字母或数字!")]
15         [DataType(DataType.MultilineText)]
16         public string Description { get; set; }
17         [Required(ErrorMessage = "价格不能为空!")]
18         [Range(0.10, 10000, ErrorMessage = "请输入0.10-10000.00之间的价格!")]
19         [DataType(DataType.Currency)]
20         [DisplayFormat(DataFormatString = "{0:c}")]
21         public decimal Price { get; set; }
22         public int? CategoryID { get; set; }
23         public virtual Category Category { get; set; }
24     }
25 }

  这儿有多个我们之前没有见过的新特性。代码[DataType(DataType.MultilineText]用于告诉UI当在编辑视图或创建视图中,使用多行文本来显示描述信息输入框。

  [DataType(DataType.Currency)]用于给UI提示应该如何限制输入的格式。DataType的一些类型目前还不被大多数浏览器所实现。

  [DisplayFormat(DataFormatString = "{0:c})"]指示Price属性应该被显示为货币格式,比如£1,234.56(依赖于本地服务器的货币设置)。一般情况下,这些属性都会工作,并且将价格显示为货币。为了完整性,我们把它们都包含其中。

  现在,重新生成解决方案,然后在程序包管理器控制台中依次运行下列命令,以向数据库添加范围和null设置。

1 add-migration ProductValidation
2 update-database

  不调试启动站点,然后点击产品链接。图4-13显示了产品的列表,并且产品的价格现在已经被格式化为货币形式,这都归功于我们使用的数据注解功能。

图4-13:产品索引(Index)页中的价格字段现在被格式化为货币形式

  注意:使用代码[DisplayFormat(DataFormatString = "{0:c}", ApplyFormatInEditMode = true)]可以让我们在编辑视图中将价格格式化为货币形式,但是,我们不推荐这样做,因为,当我们在编辑是输入的价格格式为£9,999.99,但是当我们试着提交表单时,价格将不会通过验证,因为£不是一个数字。

  点击Details链接,我们将会看到价格现在也被格式化为货币形式。为了看到对Product类所做的修改带来的全部影响,我们需要创建和编辑一个产品。图4-14显示的是当创建一个新产品信息时,所输入的数据全部不符合规则的效果。

图4-14:使用无效数据创建一个产品信息时所显示的自定义错误信息

  译者注:原书第78页剩余部分所描述的问题好像不正确,有大神能看明白原作者意图的请指出,在此非常感谢!因此译者对剩余部分进行了修改,感兴趣的读者可以参考原书。

  我们将验证应用到默认视图中是一大改进,但是,依然存在一些小问题,比如,价格可以输入多位小数,而我们希望最多只能输入两位小数,为了解决这个问题,我们使用下列方法。

  我们向Product类中添加一个正则表达注解,更新后的Price属性如下所示:

1 [Required(ErrorMessage = "价格不能为空!")]
2 [Range(0.10, 10000, ErrorMessage = "请输入0.10-10000.00之间的价格!")]
3 [DataType(DataType.Currency)]
4 [DisplayFormat(DataFormatString = "{0:c}")]
5 [RegularExpression("[0-9]+(\\.[0-9][0-9]?)?", ErrorMessage = "价格必须是不超过两位小数的数字!")]
6 public decimal Price { get; set; }

  这个正则表达式允许一个数值后面跟一个或两个小数,它允许的数值格式为1、1.1或1.10,但不能是1.之后没有任何数字的格式。图4-15显示了其验证效果。

图4-15:对Product的Price属性应用两位小数验证的效果

4.3.3 验证是如何工作的

  当我们第一次创建项目的时候,基架为我们自动安装了两个NuGet包:Microsoft.jQuery.Unobtrusive和jQuery.Validation.ASP.NET MVC,它们使用jQuery执行客户端验证,当用户从一个输入框跳到另一个输入框时,客户端验证即被触发,也就是说用户不需要提交表单即可收到验证的错误信息。

  当客户端验证通过后,提交表单也会触发服务端的验证。一些JavaScript代码可以绕过客户端验证,因此,服务端验证是最后一道防线。

  为了看到服务端验证的效果,我们移除\Views\Products\Create.cshtml文件中的下列代码(该代码位于该文件的末尾):

1 @section Scripts {
2     @Scripts.Render("~/bundles/jqueryval")
3 }

  移除的这段代码主要用户客户端验证,以便我们现在只进行服务端验证。现在启动站点,并尝试创建一个空白名称、空白描述以及价格为1.234的产品,然后点击Create按钮。该页面将会显示与之前一样的验证信息,但是,这次是服务端执行的验证,如图4-16所示。

图4-16:提交表单之后的服务端验证

  将先前移除的下列代码再重新添加到\Views\Products\Create.cshtml文件中:

1 @section Scripts {
2     @Scripts.Render("~/bundles/jqueryval")
3 }

  编辑和创建视图文件不仅包含着对每个输入文本框生成验证信息的代码,还包含一个生成整个表单摘要的代码,该摘要主要用于生成有关模型的错误信息,和输入信息无关。\Views\Products\Create.cshtml文件中的代码我们在下面列出,高亮显示的代码行是和验证有关的代码。

 1 @model BabyStore.Models.Product
 2 
 3 @{
 4     ViewBag.Title = "Create";
 5 }
 6 
 7 <h2>Create</h2>
 8 
 9 
10 @using (Html.BeginForm()) 
11 {
12     @Html.AntiForgeryToken()
13     
14     <div class="form-horizontal">
15         <h4>Product</h4>
16         <hr />
17         @Html.ValidationSummary(true, "", new { @class = "text-danger" })
18         <div class="form-group">
19             @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
20             <div class="col-md-10">
21                 @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
22                 @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
23             </div>
24         </div>
25 
26         <div class="form-group">
27             @Html.LabelFor(model => model.Description, htmlAttributes: new { @class = "control-label col-md-2" })
28             <div class="col-md-10">
29                 @Html.EditorFor(model => model.Description, new { htmlAttributes = new { @class = "form-control" } })
30                 @Html.ValidationMessageFor(model => model.Description, "", new { @class = "text-danger" })
31             </div>
32         </div>
33 
34         <div class="form-group">
35             @Html.LabelFor(model => model.Price, htmlAttributes: new { @class = "control-label col-md-2" })
36             <div class="col-md-10">
37                 @Html.EditorFor(model => model.Price, new { htmlAttributes = new { @class = "form-control" } })
38                 @Html.ValidationMessageFor(model => model.Price, "", new { @class = "text-danger" })
39             </div>
40         </div>
41 
42         <div class="form-group">
43             @Html.LabelFor(model => model.CategoryID, "CategoryID", htmlAttributes: new { @class = "control-label col-md-2" })
44             <div class="col-md-10">
45                 @Html.DropDownList("CategoryID", null, htmlAttributes: new { @class = "form-control" })
46                 @Html.ValidationMessageFor(model => model.CategoryID, "", new { @class = "text-danger" })
47             </div>
48         </div>
49 
50         <div class="form-group">
51             <div class="col-md-offset-2 col-md-10">
52                 <input type="submit" value="Create" class="btn btn-default" />
53             </div>
54         </div>
55     </div>
56 }
57 
58 <div>
59     @Html.ActionLink("Back to List", "Index")
60 </div>
61 
62 @section Scripts {
63     @Scripts.Render("~/bundles/jqueryval")
64 }

  代码@Html.ValidationSummary(true, "", new { @class = "text-danger" })负责显示对于模型的整体验证的摘要信息,而其它几行代码,比如@Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger"})用于显示每一个单独属性的验证信息(在这个例子中,是Name属性)。Bootstrap的CSS类text-danger用于将信息显示为红色。

4.4 小节

  在这一章中,我们学习了如何修改Delete方法,以便可以删除一个有外键约束的实体信息,然后我们学习了如何启用Code First Migrations功能,以便我们可以通过代码的修改来更新数据库架构。我们还学习了如何使用迁移(Migration)对数据库添加种子数据以及如何创建一个新数据库。最后,我们学习了如何向一个类添加格式和验证规则,然后使用迁移(Migration)对数据库进行更新,还讨论了验证是如何工作的有关问题。

posted @ 2017-01-12 09:49  编码之道  阅读(1152)  评论(3编辑  收藏  举报