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

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

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

4.1 删除做为一个外键的实体git

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

  首先,向站点添加一个名为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.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)对数据库进行更新,还讨论了验证是如何工做的有关问题。

相关文章
相关标签/搜索