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)對數據庫進行更新,還討論了驗證是如何工做的有關問題。

相關文章
相關標籤/搜索