ASP.NET MVC with Entity Framework and CSS一書翻譯系列文章之第二章:利用模型類創建視圖、控制器和數據庫


  在這一章中,我們將直接進入項目,並且為產品和分類添加一些基本的模型類。我們將在Entity Framework的代碼優先模式下,利用這些模型類創建一個數據庫。我們還將學習如何在代碼中創建數據庫上下文類、指定數據庫連接字符串以及創建一個數據庫。最后,我們還將添加視圖和控制器來管理和顯式產品和分類數據。

  注意:如果你想按照本章的代碼編寫示例,你必須完成第一章或者直接從www.apress.com下載第一章的源代碼。

2.1 添加模型類

   Entity Framework的代碼優先模式允許我們從模型類創建數據庫。我們將創建表示產品和分類的兩個模型類來開始本章的學習。我們還將在產品和分類之間添加0或1對多的關系,表示一個產品可以屬於一個分類或不屬於任何分類,一個分類可以包含多個產品。

  右擊Models文件夾,然后從菜單中選擇【添加】->【類】,創建一個名為“Product”的新類,然后在該類中添加如下代碼:

 1 namespace BabyStore.Models
 2 {
 3     public class Product
 4     {
 5         public int ID { get; set; }
 6         public string Name { get; set; }
 7         public string Description { get; set; }
 8         public decimal Price { get; set; }
 9         public int? CategoryID { get; set; }
10         public virtual Category Category { get; set; }
11     }
12 }

  在文件的上方移除所有不必要的using語句。

  提示:要移除不不必要的using語句,我們只需將光標懸停在using語句上,點擊出現的黃色燈泡,然后選擇“刪除不必要的using”選項即可。

  Product類包含以下屬性:

  • ID—用於表示產品(product)在數據庫中的主鍵。
  • Name—產品(product)的名稱。
  • Description—關於產品的文本描述。
  • Price—表示產品的價格。
  • CategoryID—表示指定給產品(product)的分類(category)ID。在數據庫中通常被設置為外鍵。我們還將該屬性的類型設置為可空整型(int?),用於描述這樣一個事實:某個產品可能不屬於任何分類。這可以避免在對分類執行刪除操作時,該分類下的所有產品也被刪除的情況發生。默認情況下,Entity Framework對可空類型外鍵啟用級聯刪除,也就是說,如果CategoryID不是可空的,當一個分類被刪除時,所有與該分類相關聯的產品也會被刪除。
  • Category—導航屬性。導航屬性包含與該實體相關的其他實體,在這種情況下,這個屬性將包含該產品所屬的分類實體。如果一個導航數據可以包含多個實體,那么它必須被定義為集合類型。通常使用ICollection類型。導航屬性通常被定義為virtual,以便能夠完成某些特殊的功能,比如延遲加載。

   提示:我們可以在Visual Studio中輸入prop,然后按兩次Tab鍵來自動生成屬性。

  下面,我們在Models文件夾下再新建一個名為“Category”的模型類,然后在該類中添加如下代碼:

 1 using System.Collections.Generic;
 2 
 3 namespace BabyStore.Models
 4 {
 5     public class Category
 6     {
 7         public int ID { get; set; }
 8         public string Name { get; set; }
 9         public virtual ICollection<Product> Products { get; set; }
10     }
11 }

  移除文件上方所有不必要的using語句。Category類包含的屬性如下所示:

  • ID—主鍵。
  • Name—分類的名稱。
  • Products—導航屬性,該屬性包含屬於該分類的所有產品實體。

2.2 添加數據庫上下文

  對於數據模型來說,數據庫上下文是協調Entity Framework功能最主要的類。

  在解決方案資源管理器中,右鍵單擊BabyStore項目,然后創建一個名為“DAL”的文件夾。在該文件夾中創建一個名為“StoreContext.cs”的類,然后在該類中添加如下代碼:

 1 using BabyStore.Models;
 2 using System.Data.Entity;
 3 
 4 namespace BabyStore.DAL
 5 {
 6     public class StoreContext : DbContext
 7     {
 8         public DbSet<Product> Products { get; set; }
 9         public DbSet<Category> Categories { get; set; }
10     }
11 }

  上下文類繼承自System.Data.Entity.DbContext類,通常情況下,一個數據庫對應一個數據庫上下文類,在某些比較復雜的項目中可能對應多個。每一個DbSet類型的屬性被稱之為實體集,通常對應數據庫中的某一個表,比如Products屬性對應數據庫中的Products表。代碼DbSet<Product>告訴Entity Framework使用Product類來表示Products表中的一行數據。

2.3 指定連接字符串

  現在,我們已經有了數據庫上下文和一些模型類,現在需要我們告訴Entity Framework如何連接到數據庫。在Web.config文件的connectionString節點參加一個新的條目:

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|\BabyStore.mdf;Initial Catalog=BabyStore;Integrated Security=True"
5  providerName="System.Data.SqlClient" />
6   </connectionStrings>

  這個條目告訴Entity Framework要連接的是項目App_Data文件夾中的BabyStore.mdf數據庫。我們選擇在這個文件夾中存儲數據庫是因為它能夠跟隨項目一起被復制。AttachDbFilename=|DataDirectory|\BabyStore.mdf;指定在App_Data文件夾中創建數據庫。

  一個可選的指定連接字符串的條目是Data Source=(LocalDB)\MSSQLLocalDB;Initial Catalog=BabyStore.mdf;Integrated Security=True,它指定在用戶文件夾(通常在Windows系統中是C:\Users\User)中創建數據庫。

  在connectionString節點中存在的其他條目是在我們創建工程時自動創建的,之所以會自動創建這個數據庫是因為我們在身份驗證選項中選擇了“個人用戶賬戶”。我們將在本書的后續章節討論這個問題。

  值得注意的是,我們可以不在web.config文件中定義連接字符串,如果這樣做的話,Entity Framework將會使用基於上下文類的默認設置。

  注意:確保使用的是項目根目錄下的Web.config文件,而不是Views文件夾中的Web.config文件。

2.4 添加控制器和視圖

   現在我們需要添加一些控制器和視圖來管理和顯式我們的產品和分類數據。

2.4.1 添加分類控制器和視圖

1、從Visual Studio菜單中,點擊【生成】->【生成解決方案】來生成解決方案。

2、右擊Controllers文件夾,然后選擇【添加】->【控制器】。

3、在“添加基架”窗體中,選擇“包含視圖的MVC 5 控制器(使用 Entity Framework)”選項,如圖2-1所示。

 圖2-1:使用Entity Framework創建控制器和視圖

 4、點擊“添加”按鈕,然后在“添加控制器”窗口中選擇下列選項:

  • 模型類:Category
  • 數據上下文類:StoreContext
  • 確保生成視圖、引用腳本庫和使用布局頁選項被勾選上
  • 保留控制器名稱設置為CategoriesController(全部詳情參見圖2-2)

圖2-2:添加新分類控制器的選項

5、點擊“添加”按鈕,則會在Controllers文件夾中創建一個CategoriesController類,與之相關的視圖會創建在Views\Categories文件夾中。

2.4.2 檢查CategoriesController類和方法

  新搭建的CategoriesController.cs文件包含對分類執行CRUD(新建、查詢、更新和刪除)操作的多個方法。

  代碼private StoreContext db = new StoreContext();初始化了控制器要使用的一個上下文對象。該對象在控制器的整個生命周期中都可使用,在控制器的Dispose方法被調用時釋放。

  CategoriesController包含下列方法。

  Inex方法用於返回所有分類的列表給Views\Categories\Index.cshtml視圖:

1 // GET: Categories
2 public ActionResult Index()
3 {
4     return View(db.Categories.ToList());
5 }

  Details方法基於id參數從數據庫查詢某個分類信息,就像我們在第1章看到的那樣,系統使用路由系統將URL中的id參數傳遞給該方法的id參數。

 1 // GET: Categories/Details/5
 2 public ActionResult Details(int? id)
 3 {
 4     if (id == null)
 5     {
 6         return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
 7     }
 8     Category category = db.Categories.Find(id);
 9     if (category == null)
10     {
11         return HttpNotFound();
12     }
13     return View(category);
14 }

  Create方法的GET版本只是簡單地返回Create視圖。第一次看到這個方法可能有點陌生,它的含義是返回一個視圖,在該視圖中顯示一個空的HTML表單,用於創建一個新的分類。

1 // GET: Categories/Create
2 public ActionResult Create()
3 {
4     return View();
5 }

  Create方法的另一個版本使用HTTP POST請求。該方法在用戶提交創建(Create)視圖的表單時被調用。它帶有一個Category類型的參數,並將該參數表示的Category對象添加到數據庫中。如果該方法執行成功則返回到索引(Index)視圖,否則,它會重新加載創建(Create)視圖。

 1 // POST: Categories/Create
 2 // 為了防止“過多發布”攻擊,請啟用要綁定到的特定屬性,有關 
 3 // 詳細信息,請參閱 http://go.microsoft.com/fwlink/?LinkId=317598
 4 [HttpPost]
 5 [ValidateAntiForgeryToken]
 6 public ActionResult Create([Bind(Include = "ID,Name")] Category category)
 7 {
 8     if (ModelState.IsValid)
 9     {
10         db.Categories.Add(category);
11         db.SaveChanges();
12         return RedirectToAction("Index");
13     }
14 
15     return View(category);
16 }

  因為該方法是一個HTTP POST,它包含一些額外的代碼:

  • [HttpPost]特性告訴控制器當調用Create動作方法的請求是一個POST請求時,使用該方法,而不使用其他重載的Create方法。
  • [ValidateAntiForgeryToken]確保Token通過HTML表單傳遞,從而驗證請求。這樣做的目的是確保請求確確實實地是來自於我們所期望的表單,以防止跨站請求偽造。簡單來說,跨站請求偽造就是來自於其他站點的表單請求偽裝成我們自己站點的請求,從而執行帶有惡意目的的操作。
  • 參數([Bind(Include = "ID,Name")] Category category)告訴方法在添加一個新的分類時只包含ID和Name屬性。Bind特性使用Include屬性創建了一個安全屬性列表,只有該列表中的屬性允許被修改,從而防止overposting攻擊。但是,就像我們后面討論的那樣,它不像我們所期望的那樣工作。因此,我們需要使用一種不同的方法來處理一些值可能為空的編輯和新建操作。舉一個overposting的例子,考慮下面一種場景,當用戶提交一個產品訂單的時候,該產品的價格也被提交,overposting攻擊會嘗試通過購買一個較低價格的商品來修改已經被提交的產品價格。

  Edit方法的GET版本包含的代碼和Details方法的代碼一樣。該方法根據ID找到一個分類,然后將該分類數據傳遞給視圖。該視圖按一定的格式顯示分類信息,並允許進行編輯。

 1 // GET: Categories/Edit/5
 2 public ActionResult Edit(int? id)
 3 {
 4     if (id == null)
 5     {
 6         return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
 7     }
 8     Category category = db.Categories.Find(id);
 9     if (category == null)
10     {
11         return HttpNotFound();
12     }
13     return View(category);
14 }

  Edit方法的POST版本和Create方法的POST版本很類似。它包含一行額外的代碼用於在將實體保存到數據庫之前檢查該實體是否被修改過,如果方法執行成功,則返回到索引(Index)視圖,否則重新顯示編輯(Edit)視圖。

 1 // POST: Categories/Edit/5
 2 // 為了防止“過多發布”攻擊,請啟用要綁定到的特定屬性,有關 
 3 // 詳細信息,請參閱 http://go.microsoft.com/fwlink/?LinkId=317598
 4 [HttpPost]
 5 [ValidateAntiForgeryToken]
 6 public ActionResult Edit([Bind(Include = "ID,Name")] Category category)
 7 {
 8     if (ModelState.IsValid)
 9     {
10         db.Entry(category).State = EntityState.Modified;
11         db.SaveChanges();
12         return RedirectToAction("Index");
13     }
14     return View(category);
15 }

  Delete方法也有兩個版本。ASP.NET MVC基架將要刪除的詳細信息展示給用戶,在用戶真正提交刪除請求之前進行確認。我們在這先列出GET版本的Delete方法,我們會注意到該方法和Details方法十分類型,使用ID找到一個分類,並將該分類數據傳遞給視圖。

 1 // GET: Categories/Delete/5
 2 public ActionResult Delete(int? id)
 3 {
 4     if (id == null)
 5     {
 6         return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
 7     }
 8     Category category = db.Categories.Find(id);
 9     if (category == null)
10     {
11         return HttpNotFound();
12     }
13     return View(category);
14 }

  Delete方法的POST版本執行一個防偽檢查,它首先利用ID找到一個分類,然后移除它,最后保存對數據庫的修改。

 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     db.Categories.Remove(category);
 8     db.SaveChanges();
 9     return RedirectToAction("Index");
10 }

  由於產品實體包含了一個對分類實體的外鍵引用,自動生成的Delete方法不能正確的工作。我們將在第4章學習如何修正該問題。

  注意:有多個原因導致ASP.NET采取了不允許GET請求更新數據庫的方式,也有許多關於這樣做的安全性的評論和爭論。但是,這樣做的最主要的原因是搜索引擎爬行器會爬行我們站點中的所有公開的超鏈接,如果這些鏈接中包含未認證即可刪除記錄的鏈接,那么,可能會導致數據庫中的相關數據被刪除。稍后我們會給編輯分類添加安全機制,因此,這將變成一個有爭議的問題。

2.4.3 檢查分類視圖

  與分類相關的視圖可在\Views\Categories文件夾中找到。每一個CRUD動作(詳情、創建、編輯和刪除)都有一個視圖,索引(Index)視圖用於顯示所有分類的一個列表。

2.4.3.1 分類的索引(Index)視圖

  自動生成的Categories\Views\Index.cshtml視圖文件如下所示:

 1 @model IEnumerable<BabyStore.Models.Category>
 2 
 3 @{
 4     ViewBag.Title = "Index";
 5 }
 6 
 7 <h2>Index</h2>
 8 
 9 <p>
10     @Html.ActionLink("Create New", "Create")
11 </p>
12 <table class="table">
13     <tr>
14         <th>
15             @Html.DisplayNameFor(model => model.Name)
16         </th>
17         <th></th>
18     </tr>
19 
20     @foreach (var item in Model)
21     {
22         <tr>
23             <td>
24                 @Html.DisplayFor(modelItem => item.Name)
25             </td>
26             <td>
27                 @Html.ActionLink("Edit", "Edit", new { id = item.ID }) |
28                 @Html.ActionLink("Details", "Details", new { id = item.ID }) |
29                 @Html.ActionLink("Delete", "Delete", new { id = item.ID })
30             </td>
31         </tr>
32     }
33 
34 </table>

  在這個視圖中的各個條目如下所示:

  • @model IEnumerable<BabyStore.Models.Category>是該視圖所基於的模型。CategoriesController控制器類的Index方法向索引(Index)視圖傳遞了一個分類列表。在這個例子中,視圖需要向用戶顯示分類信息列表,因此模型指定為實現了IEnumerable接口的分類集合。
  • 當前頁的title屬性使用下列代碼進行了設置:
1 @{
2     ViewBag.Title = "Index";
3 }
  • @Html.ActionLink("Create New", "Create")創建了一個文本為“Create New”的、鏈接到創建(Create)視圖的超鏈接。這是一個使用HTML輔助器的例子,ASP.NET MVC通過使用這些輔助器來渲染各種不同的數據驅動的HTML元素。
  • @Html.DisplayNameFor(model => model.Name)顯示模型中指定屬性的值。在這個例子中,它顯示了分類的Name屬性的的值。
  • 然后代碼循環遍歷模型中包含的每一個分類,並顯示每個分類的Name屬性所包含的值,后面緊跟三個鏈接:Edit、Details和Delete(如圖2-3)。ActionLink方法的第三個參數用於向鏈接打開的視圖提供分類的id。
 1 @foreach (var item in Model)
 2 {
 3     <tr>
 4         <td>
 5             @Html.DisplayFor(modelItem => item.Name)
 6         </td>
 7         <td>
 8             @Html.ActionLink("Edit", "Edit", new { id = item.ID }) |
 9             @Html.ActionLink("Details", "Details", new { id = item.ID }) |
10             @Html.ActionLink("Delete", "Delete", new { id = item.ID })
11         </td>
12     </tr>
13 }

圖2-3:分類索引(Index)視圖生成的HTML頁面(包括示例數據)

  譯者注:示例數據的添加我們后面會講到,這兒有點超前。

.4.3.2 分類的詳情(Details)視圖

  由基架生成的Views\Categories\Details.cshtml視圖文件如下所示:

 1 @model BabyStore.Models.Category
 2 
 3 @{
 4     ViewBag.Title = "Details";
 5 }
 6 
 7 <h2>Details</h2>
 8 
 9 <div>
10     <h4>Category</h4>
11     <hr />
12     <dl class="dl-horizontal">
13         <dt>
14             @Html.DisplayNameFor(model => model.Name)
15         </dt>
16 
17         <dd>
18             @Html.DisplayFor(model => model.Name)
19         </dd>
20 
21     </dl>
22 </div>
23 <p>
24     @Html.ActionLink("Edit", "Edit", new { id = Model.ID }) |
25     @Html.ActionLink("Back to List", "Index")
26 </p>

  這段代碼比索引(Index)視圖簡單,它只顯示單一實體。文件的第一行代碼所指定的模型是一個單一實體而不是集合。

  @model BabyStore.Models.Category

  在該視圖中使用了與索引(Index)視圖中一樣的HTML輔助器,但這次不需要使用循環,因為只有一個單一實體,如圖2-4所示。

圖2-4:分類詳細(Details)視圖生成的HTML頁面

2.4.3.3 分類的創建(Create)視圖

  創建(Create)視圖顯示了一個空表單以允許我們創建一個分類。為了生成一個HTML表單,該視圖實現了一些在索引(Index)視圖和詳情(Details)視圖中沒有包括的新特性。這個表單使用POST請求提交,該請求會被POST版本的Create方法處理。這個視圖自動生成的代碼如下所示:

 1 @model BabyStore.Models.Category
 2 
 3 @{
 4     ViewBag.Title = "Create";
 5 }
 6 
 7 <h2>Create</h2>
 8 
 9 @using (Html.BeginForm())
10 {
11     @Html.AntiForgeryToken()
12 
13     <div class="form-horizontal">
14         <h4>Category</h4>
15         <hr />
16         @Html.ValidationSummary(true, "", new { @class = "text-danger" })
17         <div class="form-group">
18             @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
19             <div class="col-md-10">
20                 @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
21                 @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
22             </div>
23         </div>
24 
25         <div class="form-group">
26             <div class="col-md-offset-2 col-md-10">
27                 <input type="submit" value="Create" class="btn btn-default" />
28             </div>
29         </div>
30     </div>
31 }
32 
33 <div>
34     @Html.ActionLink("Back to List", "Index")
35 </div>
36 
37 @section Scripts {
38     @Scripts.Render("~/bundles/jqueryval")
39 }

  圖2-5顯示了所生成的HTML頁面。下面是上述代碼中的一些關鍵點:

  • 第一個新特性使用的代碼是@Using(Html.BeginForm()),該代碼行告訴視圖將這個using語句中的所有代碼都包裹在HTML表單中。
  • @Html.AntiForgeryToken()生成一個anti-forgery token,被匹配的POST版本的Create方法用於檢查(使用[ValidateAntiForgeryToken]特性)。
  • @Html.ValidationSummary(true, "", new { @class = "text-danger" })是另一個輔助器,用於顯示一個錯誤摘要(導致表單無效的任何原因)。第一個參數告訴摘要排除任何屬性錯誤,只顯示模型級別的錯誤,第三個參數new { @class = "text-danger" }用於使用Bootstrap的text-danger CSS類樣式化錯誤信息(這是一個紅色的文本,更多關於Bootstrap和CSS的知識包含在本書后續章節)。
  • @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })創建了一個新的HTML label元素,該label元素與隨后的HTML輸入控件(分類的Name屬性)相關聯。
  • @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })是另一個HTML輔助器方法,可以根據指定屬性的數據格式來顯示正確的HTML輸入元素。在這個例子中,屬性是Name,因此EditorFor方法嘗試着顯示正確的HTML元素類型以允許用戶編輯字符串。在這個情況下,它在HTML表單中創建了一個文本框元素,所以用戶可以輸入分類的名稱。
  • 在這個視圖中的最后一個新HTML輔助器是@Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })。這段代碼對屬性添加了特殊的驗證消息,如果用戶輸入的屬性值違法了在程序中設置的驗證規則,則會觸發一個錯誤。當前,我們還沒有設置規則,但是我們將在第4章學習如何設置它。
  • 在這個視圖中還有一個額外的部分是在索引(Index)視圖和詳情(Details)視圖文件中沒有的,它用於包含驗證所需的JavaScript文件(更多知識點將在第4章講述):
1 @section Scripts {
2     @Scripts.Render("~/bundles/jqueryval")
3 }

圖2-5:分類的創建(Create)視圖所生成的HTML頁面,這個頁面包含一個用於提交新分類信息的HTML表單

2.4.3.4 分類的編輯(Edit)視圖

  分類的編輯視圖顯示了一個允許用戶編輯分類信息的HTML表單,分類信息由CategoriesController控制器類的Edit方法(GET版本)傳遞給該視圖。這個視圖非常類似於創建(Create)視圖。該視圖自動生成的代碼如下所示:

 1 @model BabyStore.Models.Category
 2 
 3 @{
 4     ViewBag.Title = "Edit";
 5 }
 6 
 7 <h2>Edit</h2>
 8 
 9 @using (Html.BeginForm())
10 {
11     @Html.AntiForgeryToken()
12 
13     <div class="form-horizontal">
14         <h4>Category</h4>
15         <hr />
16         @Html.ValidationSummary(true, "", new { @class = "text-danger" })
17         @Html.HiddenFor(model => model.ID)
18 
19         <div class="form-group">
20             @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
21             <div class="col-md-10">
22                 @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
23                 @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
24             </div>
25         </div>
26 
27         <div class="form-group">
28             <div class="col-md-offset-2 col-md-10">
29                 <input type="submit" value="Save" class="btn btn-default" />
30             </div>
31         </div>
32     </div>
33 }
34 
35 <div>
36     @Html.ActionLink("Back to List", "Index")
37 </div>
38 
39 @section Scripts {
40     @Scripts.Render("~/bundles/jqueryval")
41 }

  圖2-6顯示了所生成的編輯頁面。在這個視圖中唯一的一個新的HTML輔助器方法是@Html.HiddenFor(model => model.ID)。該語句創建了一個包含分類ID的隱藏的HTML輸入元素,用於CategoriesController控制器類的Edit方法(POST版本)的Bind元素:public ActionResult Edit([Bind(Include = "ID,Name")] Category category)。

圖2-6:分類的編輯(Edit)視圖所生成的HTML頁面,分類的當前名稱被預先填充在輸入框中。

2.4.3.5 分類的刪除(Delete)視圖

   刪除(Delete)視圖和詳情(Details)視圖很類似,而且也包含一個HTML表單,用於提交到CategoriesController控制器類的Delete方法(POST版本)。除了前面我們所檢查的視圖所包含的內容之外,該視圖沒有包含其他任何新的特性。自動生成的代碼如下所示,該視圖所生成的HTML如圖2-7所示:

 1 @model BabyStore.Models.Category
 2 
 3 @{
 4     ViewBag.Title = "Delete";
 5 }
 6 
 7 <h2>Delete</h2>
 8 
 9 <h3>Are you sure you want to delete this?</h3>
10 <div>
11     <h4>Category</h4>
12     <hr />
13     <dl class="dl-horizontal">
14         <dt>
15             @Html.DisplayNameFor(model => model.Name)
16         </dt>
17 
18         <dd>
19             @Html.DisplayFor(model => model.Name)
20         </dd>
21 
22     </dl>
23 
24     @using (Html.BeginForm())
25     {
26         @Html.AntiForgeryToken()
27 
28         <div class="form-actions no-color">
29             <input type="submit" value="Delete" class="btn btn-default" /> |
30             @Html.ActionLink("Back to List", "Index")
31         </div>
32     }
33 </div>

2.4.4 添加產品控制器和視圖

1、右擊Controllers文件夾,然后選擇【添加】->【控制器】。

2、在“添加基架”窗體中,選擇“包含視圖的 MVC 5 控制器(使用 Entity Framework)”選項(如圖2-1)。

3、點擊“添加”按鈕,然后在“添加控制器”窗口中選擇下列選項:

  • 模型類:Product
  • 數據上下文類:StoreContext
  • 確保生成視圖、引用腳本庫和使用布局頁選項被勾選上
  • 保留控制器名稱設置為ProductsController(全部詳情參見圖2-8)

圖2-8:添加新產品控制器的選項

4、點擊“添加”按鈕,則會在Controllers文件夾中創建一個ProductsController類,與之相關的視圖會創建在Views\Products文件夾中。

2.4.4.1 檢查產品控制器和視圖

  ProductsController控制器類和與之相關的視圖和前面所講述的CategoriesController類非常相似,我們就不再詳細描述了。然而,該視圖包含了一個非常重要的新功能,應用程序需要提供一種方式關聯產品和分類,在該視圖中所采用的方式是使用select元素顯示一個下拉列表,以便用戶在創建產品和編輯產品時可以選擇產品所對應的分類。

  在控制器中,實現這個功能的代碼可以在Edit方法(GET版本和POST版本)和Create方法(POST版本)中找到:

ViewBag.CategoryID = new SelectList(db.Categories, "ID", "Name", product.CategoryID);

  在Create方法的GET版本中,與之相類似的代碼如下所示:

ViewBag.CategoryID = new SelectList(db.Categories, "ID", "Name");

  這段代碼將一個條目賦值給了ViewBage的CategoryID屬性。這個條目是一個SelectList對象,該對象包括數據庫中的所有分類,每一個條目使用Name屬性作為要顯示的文本,使用ID屬性作為其值。可選的第四個參數決定在select列表中預先選定的條目。舉個例子,如果第四個參數product.CategoryID設置為2,那么視圖中的分類下拉列表中將會預先選定分類Toys。圖2-9顯示了它在視圖中的樣子。

  在視圖中顯示HTML的select元素使用下列HTML輔助器方法:@Html.DropDownList("CategoryID", null, htmlAttributes: new { @class = "form-control" })。

  這段代碼基於ViewBag.CategoryID屬性生成一個HTML元素,並將該元素的CSS class屬性設置為form-control。在DropDownList輔助器方法中,如果第一個字符串參數的值匹配ViewBag屬性的名字,它將會自動使用這個值,而不會再指定一個到ViewBag的引用。

圖2-9:使用product.CategoryID參數在下拉列表中預先選定的元素值

2.5 使用新的產品和分類視圖

  我們不期望用戶在瀏覽器地址欄中手動輸入URL來導航到我們新建的視圖,因此,我們需要更新主站點的導航欄:

1、打開Views\Shared\_Layout.cshtml文件。

2、在代碼<li>@Html.ActionLink("聯系方式", "Contact", "Home")</li>的下面添加分類和產品的索引(Index)頁面:

 1 <div class="navbar-collapse collapse">
 2     <ul class="nav navbar-nav">
 3         <li>@Html.ActionLink("主頁", "Index", "Home")</li>
 4         <li>@Html.ActionLink("關於", "About", "Home")</li>
 5         <li>@Html.ActionLink("聯系方式", "Contact", "Home")</li>
 6         <li>@Html.ActionLink("分類", "Index", "Categories")</li>
 7         <li>@Html.ActionLink("產品", "Index", "Products")</li>
 8     </ul>
 9     @Html.Partial("_LoginPartial")
10 </div>

3、點擊【調試】->【開始執行(不調試)】,Web站點將會啟動。點擊“分類”鏈接的時候將會發生兩件事:

  • 分類的索引(Index)視圖將會出現。可能該視圖不包含任何數據,因為我們還沒有在數據庫中添加數據(如圖2-10)。
  • Entity Framework會使用Code First模式,基於我們的模型類創建BabyStore數據庫。為了查看該數據庫,在Visual Studio中打開SQL Server對象資源管理器。如果SQL Server對象資源管理器沒有出現,則從主菜單中點擊【視圖】->【SQL Server對象資源管理器】。

圖2-10:分類的索引(Index)頁面

2.5.1 檢查新創建的BabyStore數據庫

  要看出數據庫中的新列,在SQL Server對象資源管理器中展開以下節點:SQL Server>(localdb)\MSSQLLocalDB>數據庫>BabyStore>表>dbo.Categories>列。同時也將dbo.Products>列展開,如圖2-11。

圖2-11:SQL Server對象資源管理器中顯示的最初的BabyStore數據庫,分類和產品表的列依次展開,並顯示出每個列的數據類型

  在數據庫中列出的每個列都和模型類的屬性相匹配。表名被默認設置為復數形式。Categories表包含ID和Name列。Products表包含ID、Name、Description、Price列以及一個外鍵列CategoryID。每個列的數據類型和模型類中的屬性的數據類型相匹配。

  要想更加詳細地查看每個表的詳情,右擊表,然后在菜單中選擇【視圖設計器】。使用視圖設計器,我們可以更加詳細地查看表中的每個列,以及外鍵。在圖2-12中,我們可以看到Products表的設計以及T-SQL部分,T-SQL部分的第8行代碼用於設置外鍵約束,表面該外鍵列引用Categories表中的ID列。

圖2-12:帶有外鍵約束的Products表的設計器

2.5.2 使用視圖添加一些數據

  為了測試新視圖的功能,我們點擊分類的索引(Index)視圖中的Create New鏈接來添加一些數據。添加三個分類:Clothes、Toys和Feeding。當我們完成這些操作后,分類的索引(Index)頁面如圖2-13所示。

圖2-13:帶有三個測試分類數據的分類索引(Index)頁面

  新添加的分類信息已經被添加到數據庫中,因為當用戶點擊分類的創建(Create)頁面中的Create按鈕時,CategoriesController類中的Create方法(POST版本)被調用,然后將新分類信息保存到數據庫中。

  點擊產品鏈接,然后再點擊Create New鏈接來添加一些產品數據。添加的產品數據如表2-1所示。

表2-1:添加到站點的產品信息


  每一次點擊產品的創建(Create)頁面的Create按鈕,ProductsController類的Create方法(POST版本)都會被調用,從而保存產品數據到數據庫中。

  一旦完成添加,產品的索引(Index)頁面應如圖2-14所示。數據現在也別保存到數據庫中,如圖2-15。要查看數據庫中的數據,可以在SQL Server對象資源管理器中右鍵單擊dbo.Products表,然后從菜單中選擇【查看數據】菜單項。

圖2-14:帶有產品數據的產品索引(Index)頁面

圖2-15:查看數據庫中的Products表數據

注意:在視圖中自動生成的默認的表頭和標簽對於用戶的使用不太友好,因此作者在隨后的截圖中對他們進行了修正。如果我們想自己進行修正,可以參照第二章的源碼,該源碼可以從www.Apress.com下載。作者沒有在本書中對代碼進行修正,因為它們太過重復和繁瑣。

2.5.3 使用數據注解的方法更改分類和產品的Name屬性的顯示

  產品的索引(Index)頁面包含兩個名為Name的標題,如圖2-16所示。這是因為在視圖中使用了@Html.DisplayNameFor(model => model.Category.Name)代碼來顯示分類的Name屬性,緊接着也使用了該方法來顯示產品的Name屬性。在產品的詳情(Details)頁面也有同樣的問題,這造成了用戶使用的困惑。

圖5-16:產品的索引(Index)頁面中的兩個Name表頭

  為了解決這個問題,我們可以使用一個稱之為數據注解的ASP.NET特性在分類和產品的模型類的Name屬性上添加一個Display特性。

  在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         [Display(Name = "Category Name")] 11         public string Name { get; set; }
12         public virtual ICollection<Product> Products { get; set; }
13     }
14 }

  [Display(Name = "Category Name")]告訴MVC框架在顯示屬性名稱標簽的時候,不使用Name而是使用Category Name。

  按同樣的方式修改Models\Product.cs文件中的代碼:

 1 using System.ComponentModel.DataAnnotations;  2 
 3 namespace BabyStore.Models
 4 {
 5     public class Product
 6     {
 7         public int ID { get; set; }
 8         [Display(Name = "Product Name")]  9         public string Name { get; set; }
10         public string Description { get; set; }
11         public decimal Price { get; set; }
12         public int? CategoryID { get; set; }
13         public virtual Category Category { get; set; }
14     }
15 }

  從菜單欄中選擇【生成解決方案】,然后再選擇【調試】->【開始執行(不調試)】菜單項來啟動應用程序。點擊產品鏈接打開產品索引(Index)頁面,我們會看到兩個Name標題現在變成了Category Name和Product Name,如圖2-17所示。

圖2-17:產品索引(Index)頁面顯示的數據注解的顯示名稱

  在類中使用數據注解可以使得我們的代碼更易維護,因為對於屬性名稱的顯示只需要在一個地方進行控制即可。我們也可以在視圖中修改顯示名稱,但這涉及到兩個文件,因此變得難以維護,此外,對於我們使用該屬性創建的視圖,未來都需要被更新。

2.5.3.1 使用MetaDataType將數據注解分割為另一個文件

  一些開發人員喜歡模型類盡可能地簡單明了,因此,不願意對模型類添加數據注解。這可以使用MetaDataType類來完成這種需求。

  在Models文件夾中添加一個名為ProductMetaData.cs的文件,然后修改其代碼如下所示:

 1 using System.ComponentModel.DataAnnotations;
 2 
 3 namespace BabyStore.Models
 4 {
 5     [MetadataType(typeof(ProductMetaData))]
 6     public partial class Product
 7     {
 8     }
 9 
10     public class ProductMetaData
11     {
12         [Display(Name = "Product Name")]
13         public string Name;
14     }
15 }

  現在將Product類聲明成了一個分部類,這意味着將該類分割為了多個文件。數據注解[MetadataType(typeof(ProductMetaData))]用於告訴.NET要將來自於ProductMetaData類的元數據應用於Product類。

  將Product類修改成原來的狀態,但是將它聲明為一個分部類,以便與聲明在ProductMetaData.cs文件中的Product類聲明進行合並。

 1 namespace BabyStore.Models
 2 {
 3     public partial class Product
 4     {
 5         public int ID { get; set; }
 6         public string Name { get; set; }
 7         public string Description { get; set; }
 8         public decimal Price { get; set; }
 9         public int? CategoryID { get; set; }
10         public virtual Category Category { get; set; }
11     }
12 }

  修正后的代碼所產生的結果將和圖2-17顯示的效果一樣。然而,使用這種編碼方法,除了將Product類聲明為一個分部類之外,沒有對Product類進行任何修改。當我們使用一些自動生成的類,而我們又不想對這些類進行修改時,這是一種非常有用的策略,比如,當我們使用Entity Framework的DataBase First模式時。在本書中,我們沒有涵蓋Entity Framework的Database First相關知識,但是我們將講述另外一種場景:對一個已經存在的數據庫使用Code First。

2.6 簡單查詢:按照字母順序對分類進行排序

  在分類的索引(Index)視圖中所顯示的分類目前是按照ID來進行排序的,我們可以修改成按照字母順序對分類的名稱進行排序。

  實現這個功能十分簡單,打開Controllers\CategoriesController.cs文件,然后將Index方法修改成如下代碼:

1 // GET: Categories
2 public ActionResult Index()
3 {
4     return View(db.Categories.OrderBy(c => c.Name).ToList());
5 }

  點擊【調試】->【開始執行(不調試)】啟動應用程序,然后點擊分類鏈接。現在分類將會按照字母順序對產品的名稱進行排序,如圖2-18所示。

圖2-18:按照字母順序對分類名稱進行排序

  這段代碼使用LINQ方法語法(method syntax)來指定對哪個列進行排序。lambda表達式用於指定要排序的列是Name列。這段代碼將返回一個排序過的分類列表給視圖顯示。LINQ表示Language-Integrated Query,它是內置在.NET框架中的一個查詢語言。使用LINQ方法語法(method syntax)意味着所創建的查詢可以使用點“.”快速地將多個方法進行鏈接。一個可替換方法語法(method syntax)的方式是使用查詢語法(query syntax),我們將在第3章給出一個編寫的一個比較復雜的查詢例子。方法語法(method syntax)在外觀上更像SQL的語法,對於比較復雜的查詢可以讓我們更容易理解。但是,對於稍短的查詢它顯得就比較冗長。

  lambda表達式是匿名方法,這些方法可用於創建委托。簡單來說,lambda表達式可以讓我們創建一個表達式,該表達式的lambda操作符(=>)左邊的值是輸入參數,右邊的是要計算的表達式和返回值。考慮上面我們輸入的lambda表達式,它帶有一個Category類型的輸入參數,然后返回分類的Name屬性。因此,簡單的說,它表述的意思就是按照分類的Name屬性排序。

  在本書中,我們沒有詳細講述LINQ或lambda表達式。如果你想對其了解的更多,我們建議你讀下Andrew Troelsen編寫的一本非常優秀的圖書:Pro C#(中文圖書名為:精通C#)。

2.7 按照類別過濾產品:使用導航屬性和Include搜索相關實體

  我們已經看到如何創建一個非常基本的頁面用於顯示不同實體的列表。現在,我們將添加一些有用的功能從而讓這些列表進行交互。我們將使用分類列表中被選擇的值來過濾產品列表。為了達到此目的,我們需要修改下列代碼:

  • 修改ProductsController的Index方法,使它能夠接收一個參數,該參數表示選擇的分類,並且返回一個屬於該分類的產品列表。
  • 將分類索引(Index)頁中的文本列表修改成超鏈接列表,該超鏈接的目標是ProductsController的Index方法。

  第一處改動是ProductsController的Index方法,如下所示:

1 public ActionResult Index(string category)
2 {
3     var products = db.Products.Include(p => p.Category);
4     if (!string.IsNullOrEmpty(category)) 5  { 6         products = products.Where(p => p.Category.Name == category); 7  } 8     return View(products.ToList());
9 }

  這段代碼給Index方法添加了一個名為category的字符串參數。if語句用於檢查category參數是否為空,如果該參數不為空,將會使用Product類的導航屬性Category來對產品進行過濾,代碼為:products = products.Where(p => p.Category.Name == category);。

  下面一行代碼所展示的Include方法是一個使用預先加載(eager loading)的例子:

1 var products = db.Products.Include(p => p.Category);

  它告訴Entity Framework去執行一個單一查詢,檢索出所有的產品和這些產品相關的分類信息。預先加載(eager loading)會導致一個SQL連接查詢,它一次檢索出所需的所有數據。我們可以忽略Include方法,這時,Entity Framework將會使用延遲加載(lazy loading),這將涉及多個查詢而不是一個單一的連接查詢。

  選擇使用那一種加載方法有一些性能上的差異。預先加載對數據庫進行一次查詢就可以獲得結果,但是,如果使用比較復雜的連接語句會導致性能下降。延遲加載會對數據庫進行多次查詢才能獲得所需數據。在這兒,我們使用預先加載是因為連接語句比較簡單,同時,我們需要加載相關的分類信息以便對齊進行搜索。

  products變量使用Where方法匹配產品的分類名稱與傳遞進來的category參數值一致的那些產品。這可能有點小題大做,同時,我們可能也會迷惑為什么我們不使用CategoryID而是使用字符串來給Index方法傳值。關鍵原因在於,當我們使用路由時,使用分類名稱更加有意義。我們將在本書后續章節講述相關知識。

  這是一個非常好的例子,它展示了導航屬性是非常有用和強大的。由於使用了Product類中的導航屬性,我們可以使用較少的代碼來搜索兩個相關的實體。如果我們想根據分類名稱來匹配產品,但是不使用導航屬性的話,我們必須在ProductsController中加載分類實體,但是按照慣例,ProductsController只對產品進行管理。

  為了演示新方法達到的效果,我們啟動站點,然后導航到產品的索引(Index)頁面。現在我們在URL后面最佳代碼?category=clothes。現在,產品列表將會被匹配的項目過濾,如圖2-19。

圖2-19:使用URL地址欄按照clothes分類過濾產品

  在查詢字符串中的任何參數都會自動匹配目標方法的參數。因此,在這個例子中,URL中的?category=clothes匹配ProductsController中的Index方法的category參數,此時,該參數的值為clothes。

  注意:對於剛剛開始使用Entity Framework的編碼人員來說,最常見的錯誤是在錯誤的地方使用了ToList()方法。在一個方法中,LINQ通常用於創建一個查詢,但是不執行這個查詢!查詢只在ToList()方法被調用時才被執行。初級編碼人員經常在他們方法的開始就使用ToList()方法,這通常導致比較多的記錄(通常是所有的記錄)從數據庫中檢索出來,其中有些記錄並不是所需要的,從而對性能產生影響。所有這些記錄都被保存在內存中,並作為內存中的列表進行處理,這通常是不可取的,它會使站點的想能大幅降低。作為選擇,我們甚至都不用調用ToList()方法,當加載視圖時,查詢會被執行。這個話題被稱之為延遲執行,延遲執行歸功於查詢只有在ToList()方法被調用時才會執行。

  為了完成根據分類過濾產品的功能,我們需要修改分類的索引(Index)頁面中的產品列表,將其更改為鏈接到ProductsController的Index方法的超鏈接。

  為了將分類更改為超鏈接,我們修改Views\Categories\Index.cshtml文件,將其文件中的@Html.DisplayFor(modelItem => item.Name)修改為:

 1 @foreach (var item in Model)
 2 {
 3     <tr>
 4         <td>
 5             @*@Html.DisplayFor(modelItem => item.Name)*@  6             @Html.ActionLink(item.Name, "Index", "Products", new { category = item.Name }, null)  7         </td>
 8         <td>
 9             @Html.ActionLink("Edit", "Edit", new { id = item.ID }) |
10             @Html.ActionLink("Details", "Details", new { id = item.ID }) |
11             @Html.ActionLink("Delete", "Delete", new { id = item.ID })
12         </td>
13     </tr>
14 }

  這段代碼使用HTML的ActionLink輔助器方法生成一個超鏈接,該鏈接的顯示文本是分類的名稱,目標是ProductsController的Index方法。第四個參數是路由參數,用於將分類的Name屬性值賦值給category參數,該參數會跟隨URL傳遞給目標方法參數。使用這種方法和我們使用手動方式的效果一樣,都會在URL的后面追加category=caegoryname樣式的參數。

  通過上述的修改,分類的索引(Index)頁面現在包含了鏈接到ProductsController的Index方法的超鏈接,如圖2-20所示。

  點擊每一個鏈接,現在都會打開產品的索引(Index)頁面,在該頁面中只顯示了與該分類相關的產品信息。

圖2-20:帶有超鏈接的、用於過濾產品的分類索引(Index)頁面。標紅的部分是clothes鏈接生成的URL。

2.8 小節

   在這一章中,我們學習了如何創建模型類以及如何根據模型類生成數據庫。我們還學習了如何指定數據庫連接字符串,以及如何創建數據庫上下文。這些類以及我們的模型類可用於創建控制器和視圖,我們還創建和填充了數據庫。

  數據庫創建完畢之后,我們還學習了如何檢查它以及如何修改視圖以修正與基架相關的問題。從這開始,本章余下的部分主要講述了如何使用分類過濾產品,如何使用導航屬性以及如何從視圖鏈接到不同的動作方法。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM