【譯著】第9章 SportsStore:管理 — 《精通ASP.NET MVC 3框架》


C H A P T E R 9
■ ■ ■

SportsStore: Administration
SportsStore:管理

In this final chapter on building the SportsStore application, we will give the site administrator a way of managing the product catalog. We will add support for creating, editing, and removing items from the product repository, as well as for uploading and displaying images alongside products in the catalog. And, since these are administrative functions, we’ll show you how to use authentication and filters to secure access to controllers and action methods, and to prompt users for credentials when needed.
在這個建立SportsStore應用程序的最后一章中,將為網站管理員提供一個管理產品分類的方法。我們將添加對產品存儲庫條目的創建、編輯、和刪除、以及上傳分類中產品的圖片並在產品旁邊顯示圖片的支持功能。另外,由於這些是管理功能,我們將演示如何使用認證和過濾來實現對控制器和動作方法的安全訪問,並在需要時提示用戶提供憑據。

Adding Catalog Management
添加分類管理

The convention for managing collections of items is to present the user with two types of pages: a list page and an edit page, as shown in Figure 9-1.
管理條目集合的慣例是向用戶顯示兩種形式的頁面:一個列表頁面和一個編輯頁面,如圖9-1所示。

圖9-1

Figure 9-1. Sketch of a CRUD UI for the product catalog
圖9-1. 用於產品分類的CRUD界面略圖

Together, these pages allow a user to create, read, update, and delete items in the collection. As noted in Chapter 7, collectively, these actions are known as CRUD. Developers need to implement CRUD so often that Visual Studio tries to help by offering to generate MVC controllers that have action methods for CRUD operations and view templates that support them.
這些頁面合起來便是讓用戶創建、讀取、更新、和刪除集合中的條目。正如第7章所說明的,合起來,這些動作稱為CRUD。開發人員往往需要實現CRUD,Visual Studio試圖對此提供幫助,它讓你能夠生成CRUD控制器,這種控制器含有進行CRUD操作的動作方法,另外還提供了支持這些操作的視圖模板。

Creating a CRUD Controller
創建一個CRUD控制器

We will create a new controller to handle our administration functions. Right-click the Controllers folder of the SportsStore.WebUI project and select Add † Controller from the pop-up menu. Set the name of the controller to AdminController and select Controller with empty read/write actions from the Template drop-down list, as shown in Figure 9-2.
我們將創建一個新控制器來處理這些管理功能。右擊SportsStore.WebUI項目的Controllers文件夾,並從彈出菜單中選擇“添加控制器”。將該控制器名設為AdminController,並在“模板”下拉列表中選擇“Controller with empty read/write actions(帶有空讀/寫動作的控制器)”,如圖9-2所示。

圖9-2

Figure 9-2. Creating a controller using the Add Controller dialog box
圖9-2. 用添加控制器對話框創建一個控制器

Click the Add button to create the controller. You can see the code that the template produces in Listing 9-1.
點擊“添加”按鈕以創建這個控制器。你可以在清單9-1中看到該模板產生的代碼。

Listing 9-1. The Visual Studio CRUD Template
清單9-1. Visual Studio的CRUD模板

using System.Web.Mvc;
namespace SportsStore.WebUI.Controllers { public class AdminController : Controller {
public ActionResult Index() { return View(); } public ActionResult Details(int id) { return View();} public ActionResult Create() { return View();}
[HttpPost] public ActionResult Create(FormCollection collection) { try { // TODO: Add insert logic here // TODO: 這里添加插入邏輯 return RedirectToAction("Index"); } catch { return View(); } }
public ActionResult Edit(int id) { return View();}
[HttpPost] public ActionResult Edit(int id, FormCollection collection) { try { // TODO: Add update logic here // TODO: 這里添加更新邏輯 return RedirectToAction("Index"); } catch { return View(); } }
public ActionResult Delete(int id) { return View();}
[HttpPost] public ActionResult Delete(int id, FormCollection collection) { try { // TODO: Add delete logic here // TODO: 這里添加刪除邏輯 return RedirectToAction("Index"); } catch { return View(); } } } }

This is Visual Studio’s default CRUD template. However, we aren’t going to use it for our SportsStore application because it isn’t ideal for our purposes. We want to demonstrate how to build up the controller and explain each step as we go. So, remove all of the methods in the controller and edit the code so that it matches Listing 9-2.
這是Visual Studio默認的CRUD模板。然而,我們不打算把它用於我們的SportsStore應用程序,因為它對我們的目標不很理想。我們希望演示如何建立這種控制器,並對我們所做的每一個步驟進行解釋。因此,刪掉此控制器中的所有動作方法,並編輯代碼使之與清單9-2吻合。

Listing 9-2. Starting Over with the AdminController Class
清單9-2. AdminController類的大致開始

using System.Web.Mvc;
using SportsStore.Domain.Abstract; 
namespace SportsStore.WebUI.Controllers {
public class AdminController : Controller { private IProductRepository repository;
public AdminController(IProductRepository repo) { repository = repo; } } }

Rendering a Grid of Products in the Repository
渲染存儲庫中的產品網格

To support the list page shown in Figure 9-1, we need to add an action method that will display all of the products in the repository. Following the MVC Framework conventions, we’ll call this method Index. Add the action method to the controller, as shown in Listing 9-3.
為了支持圖9-1中的列表頁面,我們需要添加一個動作方法,它將顯示存儲庫中的所有產品。根據MVC框架的約定,我們稱這個方法為Index。把這個動作方法添加到控制器,如清單9-3所示。

Listing 9-3. The Index Action Method
清單9-3. Index動作方法

using System.Web.Mvc;
using SportsStore.Domain.Abstract; 
namespace SportsStore.WebUI.Controllers {
public class AdminController : Controller { private IProductRepository repository;
public AdminController(IProductRepository repo) { repository = repo; }
public ViewResult Index() { return View(repository.Products); } } }

UNIT TEST: THE INDEX ACTION
單元測試:INDEX動作

The behavior that we care about for the Index method is that it correctly returns the Product objects that are in the repository. We can test this by creating a mock repository implementation and comparing the test data with the data returned by the action method. Here is the unit test:
對Index方法,我們所關心的行為是,它正確地返回了存儲庫中的Product對象。對此進行測試的思路是,創建一個模仿存儲庫的實現,並把該動作方法返回的數據與測試數據進行比較。以下是該單元測試:

[TestMethod]
public void Index_Contains_All_Products() {
    // Arrange - create the mock repository
    // 布置 — 創建模仿存儲庫
    Mock<IProductRepository> mock = new Mock<IProductRepository>();
    mock.Setup(m => m.Products).Returns(new Product[] {
        new Product {ProductID = 1, Name = "P1"},
        new Product {ProductID = 2, Name = "P2"},
        new Product {ProductID = 3, Name = "P3"},
    }.AsQueryable());
// Arrange - create a controller // 布置 — 創建控制器 AdminController target = new AdminController(mock.Object);
// Action // 動作 Product[] result = ((IEnumerable<Product>)target.Index().ViewData.Model).ToArray();
// Assert // 斷言 Assert.AreEqual(result.Length, 3); Assert.AreEqual("P1", result[0].Name); Assert.AreEqual("P2", result[1].Name); Assert.AreEqual("P3", result[2].Name); }

Creating a New Layout
創建一個新的布局

We are going to create a new Razor layout to use with the SportsStore administration views. This will be a simple layout that provides a single point where we can apply changes to all of the administration views.
我們打算創建一個新的Razor布局,以用於SportsStore的管理視圖。這是一個簡單的布局,它提供了一個單一的點,我們可以運用這個點,把它變成所有的管理視圖。

To create the layout, right-click the Views/Shared folder in the SportsStore.WebUI project and select Add → New Item. Select the MVC 3 Layout Page (Razor) template and set the name to _AdminLayout.cshtml, as shown in Figure 9-3. Click the Add button to create the new file.
為了創建這個布局,右擊SportsStore.WebUI項目的Views/Shared文件夾,並選擇“添加”→“新項目”。選擇“MVC 3 Layout Page (Razor)(MVC 3布局頁(Razor))”模板,並設置其名字為_AdminLayout.cshtml,如圖9-3所示。點擊“添加”按鈕以創建這個新文件。

圖9-3

Figure 9-3. Creating a new Razor layout
圖9-3. 創建一個新的Razor布局

The convention is to start the layout name with an underscore (_). Razor is also used by another Microsoft technology called WebMatrix, which uses the underscore to prevent layout pages from being served to browsers. MVC doesn’t need this protection, but the convention for naming layouts is carried over to MVC applications anyway.
約定是用一個下划線字符(_)作為布局名。微軟的另一個叫做WebMatrix的技術也使用Razor,它利用下划線來阻止瀏覽器請求布局頁面。MVC不需要這種防護,但這一約定被延續到了MVC應用程序。

We want to create a reference to a CSS file in the layout, as shown in Listing 9-4.
在這個布局中,我們要創建一個對CSS文件的引用,如清單9-4所示。

Listing 9-4. The _AdminLayout.cshtml File
清單9-4. _AdminLayout.cshtml文件

<!DOCTYPE html> 
<html> <head> <title>@ViewBag.Title</title> <link href="@Url.Content("~/Content/Admin.css")" rel="stylesheet" type="text/css" /> </head> <body> <div> @RenderBody() </div> </body> </html>

The addition (shown in bold) is a reference to a CSS file called Admin.css in the Content folder. To create the Admin.css file, right-click the Content folder, select Add † New Item, select the Style Sheet template, and set the name to Admin.css, as shown in Figure 9-4.
添加的內容(黑體)引用了Content文件夾中一個名為Admin.css的CSS文件。要創建這個Admin.css文件,右擊Content文件夾,選擇“添加新項目”,選擇“樣式表”模板,將名字設置為Admin.css,如圖9-4所示。

圖9-4

Figure 9-4. Creating the Admin.css file
圖9-4. 創建Admin.css文件

Replace the contents of the Admin.css file with the styles shown in Listing 9-5.
用清單9-5所示的樣式替換Admin.css文件的內容。

Listing 9-5. The CSS Styles for the Admin Views
清單9-5. 用於Admin視圖的CSS樣式

BODY, TD { font-family: Segoe UI, Verdana }
H1 { padding: .5em; padding-top: 0; font-weight: bold;
   font-size: 1.5em; border-bottom: 2px solid gray; }
DIV#content { padding: .9em; }
TABLE.Grid TD, TABLE.Grid TH { border-bottom: 1px dotted gray; text-align:left; }
TABLE.Grid { border-collapse: collapse; width:100%; }
TABLE.Grid TH.NumericCol, Table.Grid TD.NumericCol {
  text-align: right; padding-right: 1em; }
FORM {margin-bottom: 0px; }
DIV.Message { background: gray; color:White; padding: .2em; margin-top:.25em; }
.field-validation-error { color: red; display: block; }
.field-validation-valid { display: none; }
.input-validation-error { border: 1px solid red; background-color: #ffeeee; }
.validation-summary-errors { font-weight: bold; color: red; }
.validation-summary-valid { display: none; }

Implementing the List View
實現List視圖

Now that we have created the new layout, we can add a view to the project for the Index action method of the Admin controller. Right-click inside the Index method and select Add View from the pop-up menu. Set the name of the view to Index, as shown in Figure 9-5.
現在,已經創建了一個新布局,我們可以把一個用於Admin控制器的Index動作方法的視圖添加到項目中。在Index方法中右擊,並從彈出菜單選擇“添加視圖”。將視圖名設為Index,如圖9-5所示。

圖9-5

Figure 9-5. Creating the Index view
圖9-5. 創建Index視圖

We are going to use a scaffold view, which is where Visual Studio looks at the class we select for a strongly typed view and creates a view containing markup tailored for that model type. To do this, select Product from the list of model classes and List for the scaffold template, as shown in Figure 9-5.
我們打算使用一個支架(scaffold)視圖,在這個視圖中,Visual Studio會考查我們對強類型視圖所選擇的類,並且創建的視圖包含了對這個模型類型量身定制的標記。為此,從模型列表中選擇Product,並在支架模板中選擇List,如圖9-5所示。

■ Note When using the List scaffold, Visual Studio assumes you are working with an IEnumerable sequence of the model view type, so you can just select the singular form of the class from the list.
注:當使用List支架時,Visual Studio假設你要使用的是一個IEnumaerable的模型視圖類型序列,因此,你只能從列表中選擇一個類。

We want to apply our newly created layout, so check the option to use a layout for the view and select the _AdminLayout.cshtml file from the Views/Shared folder. Click the Add button to create the view. The scaffold view that Visual Studio creates is shown in Listing 9-6.
我們要運用新創建的布局,因此為此視圖選中“使用布局”復選框,並選擇Views/Shared文件夾中的_AdminLayout.cshtml文件。點擊“添加”按鈕創建這個視圖。Visual Studio所創建的這個支架視圖如清單9-6所示。

Listing 9-6. The Scaffold for List Views
清單9-6. List視圖的支架

@model IEnumerable<SportsStore.Domain.Entities.Product> 
@{ ViewBag.Title = "Index"; Layout = "~/Views/Shared/_AdminLayout.cshtml"; }
<h2>Index</h2> <p> @Html.ActionLink("Create New", "Create") </p> <table> <tr> <th></th> <th>Name</th> <th>Description</th> <th>Price</th> <th>Category</th> </tr>
@foreach (var item in Model) { <tr> <td> @Html.ActionLink("Edit", "Edit", new { id=item.ProductID }) | @Html.ActionLink("Details", "Details", new { id=item.ProductID }) | @Html.ActionLink("Delete", "Delete", new { id=item.ProductID }) </td> <td>@item.Name</td> <td>@item.Description</td> <td>@String.Format("{0:F}", item.Price)</td> <td>@item.Category</td> </tr> } </table>

You can see how this view is rendered by requesting the Admin/Index URL from the application, as shown in Figure 9-6.
通過請求應用程序的Admin/Index地址,你可以看到該視圖是如何渲染的,如圖9-6所示。

圖9-6

Figure 9-6. Rendering the scaffold List view
圖9-6. 渲染支架List視圖

The scaffold view does a pretty good job of setting things up for us. We have columns for each of the properties in the Product class and links for other CRUD operations that refer to action methods in the same controller. That said, the markup is a little verbose. Also, we want something that ties in with the CSS we created earlier. Edit your Index.cshtml file to match Listing 9-7.
支架視圖為我們做了很好的設置工作。我們有了Product類中每個屬性的列、有了進行CRUD操作的鏈接,它們指向同一控制器中的動作方法。這個標記有點冗長。而且我們希望有些東西與我們先前創建的CSS聯系起來。編輯這個Index.cshtml文件使之與清單9-7吻合。

Listing 9-7. Modifying the Index.cshtml View
清單9-7. 修改Index.cshtml視圖

@model IEnumerable<SportsStore.Domain.Entities.Product> 
@{ ViewBag.Title = "Admin: All Products"; Layout = "~/Views/Shared/_AdminLayout.cshtml"; }
<h1>All Products</h1> <table class="Grid"> <tr> <th>ID</th> <th>Name</th> <th class="NumericCol">Price</th> <th>Actions</th> </tr> @foreach (var item in Model) { <tr> <td>@item.ProductID</td> <td>@Html.ActionLink(item.Name, "Edit", new { item.ProductID })</td> <td class="NumericCol">@item.Price.ToString("c")</td> <td> @using (Html.BeginForm("Delete", "Admin")) { @Html.Hidden("ProductID", item.ProductID) <input type="submit" value="Delete"/> } </td> </tr> } </table> <p>@Html.ActionLink("Add a new product", "Create")</p>

This view presents the information in a more compact form, omitting some of the properties from the Product class and using a different approach to lay out the links to specific products. You can see how this view renders in Figure 9-7.
這個視圖以一種更緊湊的形式表現相關信息,忽略了Product類的一些屬性,並用一種不同的辦法展示了指向產品的鏈接。你可以從圖9-7看到這個視圖是如何渲染的。

圖9-7

Figure 9-7. Rendering the modified Index view
圖9-7. 渲染修改后的Index視圖

Now we have a nice list page. The administrator can see the products in the catalog, and there are links or buttons to add, delete, and inspect items. In the following sections, we’ll add the functionality to support each of these features.
現在,我們有了一個很好的列表頁面。管理員可以看到分類中的產品,並有了進行添加、刪除、以及查看條目的鏈接或按鈕。在以下章節中,我們將添加對每個特性進行支持的功能。

Editing Products
編輯產品

To provide create and update features, we will add a product-editing page similar to the one shown in Figure 9-1. There are two halves to this job:
為了提供創建和更新特性,我們將添加一個產品編輯頁面,它類似於圖9-1。此工作有兩個部分:

  • Display a page that will allow the administrator to change values for the properties of a product.
    顯示一個允許管理員修改產品屬性值的頁面。
  • Add an action method that can process those changes when they are submitted.
    添加一個在遞交時能夠處理這些修改的動作方法。

Creating the Edit Action Method
創建Edit動作方法

Listing 9-8 shows the Edit method we have added to the AdminController class. This is the action method we specified in the calls to the Html.ActionLink helper method in the Index view.
清單9-8顯示了我們已經添加到AdminController類中的Edit方法。這是我們在Index視圖的調用Html.ActionLink輔助器方法中所指定的動作方法。

Listing 9-8. The Edit Method
清單9-8. Edit方法

public ViewResult Edit(int productId) {
    Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId);
    return View(product);
}

This simple method finds the product with the ID that corresponds to the productId parameter and passes it as a view model object.
這個簡單的方法找到與productId參數對應的ID的產品,並把它作為一個視圖模型對象進行傳遞。

UNIT TEST: THE EDIT ACTION METHOD
單元測試:EDIT動作方法

We want to test for two behaviors in the Edit action method. The first is that we get the product we ask for when we provide a valid ID value. Obviously, we want to make sure that we are editing the product we expected. The second behavior is that we don’t get any product at all when we request an ID value that is not in the repository. Here are the test methods:
我們想要測試Edit動作方法中的兩個行為。第一個是當我們提供一個有效的ID值時獲取我們所查找的產品。顯然,我們希望確保我們編輯的是我們預期的產品。第二個行為是當我們請求一個不在存儲庫中的ID值時,我們根本得不到任何產品。以下是測試方法:

[TestMethod]
public void Can_Edit_Product() {
    // Arrange - create the mock repository
    // 布置 — 創建模仿存儲庫
    Mock<IProductRepository> mock = new Mock<IProductRepository>();
    mock.Setup(m => m.Products).Returns(new Product[] {
        new Product {ProductID = 1, Name = "P1"},
        new Product {ProductID = 2, Name = "P2"},
        new Product {ProductID = 3, Name = "P3"},
    }.AsQueryable());
// Arrange - create the controller // 布置 — 創建控制器 AdminController target = new AdminController(mock.Object);
// Act // 動作 Product p1 = target.Edit(1).ViewData.Model as Product; Product p2 = target.Edit(2).ViewData.Model as Product; Product p3 = target.Edit(3).ViewData.Model as Product;
// Assert // 斷言 Assert.AreEqual(1, p1.ProductID); Assert.AreEqual(2, p2.ProductID); Assert.AreEqual(3, p3.ProductID); }
[TestMethod] public void Cannot_Edit_Nonexistent_Product() { // Arrange - create the mock repository Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m => m.Products).Returns(new Product[] { new Product {ProductID = 1, Name = "P1"}, new Product {ProductID = 2, Name = "P2"}, new Product {ProductID = 3, Name = "P3"}, }.AsQueryable()); // Arrange - create the controller AdminController target = new AdminController(mock.Object); // Act Product result = (Product)target.Edit(4).ViewData.Model; // Assert Assert.IsNull(result); }

Creating the Edit View
創建Edit視圖

Now that we have an action method, we can create a view for it to render. Right-click in the Edit action method and select Add View. Leave the view name as Edit, check the option for a strongly typed view, and ensure that the Product class is selected as the model class, as shown in Figure 9-8.
現在我們有了一個動作方法,我們可以為它創建一個視圖以便渲染。右擊Edit動作方法並選擇“添加視圖”。保留視圖名為Edit,選中“強類型視圖”復選框,並確保選擇了Product類作為模型類,如圖9-8所示。

圖9-8

Figure 9-8. Creating the Edit view
圖9-8. 創建Edit視圖

There is a scaffold view for the Edit CRUD operation, which you can select if you are interested in seeing what Visual Studio creates. We will use our own markup again, so we have selected Empty from the list of scaffold options. Don’t forget to check the option to apply a layout to the view and select _AdminLayout.cshtml as the view to use. Click the Add button to create the view, which will be placed in the Views/Admin folder. Edit the view so that the content matches Listing 9-9.
有一個用於Edit的CRUD操作的支架視圖,如果你有興趣要看看Visual Studio會創建什么,你可以選擇它。我們仍要采用我們自己的標記,因此,我們在支架選項中選擇Empty。不要忘記對此視圖選中“運用布局”復選框,並選擇_AdminLayout.cshtml用於該視圖。點擊“添加”創建這個視圖,它將被放置在Views/Admin文件夾中。編輯該視圖使其內容與清單9-9吻合。

Listing 9-9. The Edit View
清單9-9. Edit視圖

@model SportsStore.Domain.Entities.Product
@{ ViewBag.Title = "Admin: Edit " + @Model.Name; Layout = "~/Views/Shared/_AdminLayout.cshtml"; }
<h1>Edit @Model.Name</h1>
@using (Html.BeginForm()) { @Html.EditorForModel() <input type="submit" value="Save" /> @Html.ActionLink("Cancel and return to List", "Index") }

Instead of writing out markup for each of the labels and inputs by hand, we have called the Html.EditorForModel helper method. This method asks the MVC Framework to create the editing interface for us, which it does by inspecting the model type—in this case, the Product class.
代替手工地為每個標簽和輸入項編寫標記,我們調用了Html.EditorForModel輔助器方法。這個方法要求MVC框架為我們創建編輯接口,這是通過探測其模型類型來實現的 — 即,Product類。

To see the page that is generated from the Edit view, run the application and navigate to /Admin/Index. Click one of the product names, and you will see the page shown in Figure 9-9.
要看看這個Edit視圖所生成的頁面,運行應用程序並導航到/Admin/Index。點擊一個產品名,於是你將看到如圖9-9所示的頁面。

圖9-9

Figure 9-9. The page generated using the EditorForModel helper method
圖9-9. 用EditorForModel輔助器方法生成的頁面

Let’s be honest—the EditorForModel method is convenient, but it doesn’t produce the most attractive results. In addition, we don’t want the administrator to be able to see or edit the ProductID attribute, and the text box for the Description property is far too small.
我們得承認 — EditorForModel方法很方便,但它並不產生最引人的結果。此外,我們不希望管理員可以看到或編輯ProductID屬性,而且,用於Description的文本框太小了。

We can give the MVC Framework directions about how to create editors for properties by using model metadata,. This allows us to apply attributes to the properties of the new model class to influence the output of the Html.EditorForModel method. Listing 9-10 shows how to use metadata on the Product class in the SportsStore.Domain project.
通過使用模型元數據,我們可以指示MVC框架如何創建屬性的編輯器(屬性編輯器是指HTML頁面上對某屬性進行輸入或編輯的UI — 譯者注)。這允許我們能夠把注解屬性運用於這個新模型類的屬性上,以影響Html.EditorForModel方法的輸出。清單9-10演示了如何在SportsStore.Domain項目中的Product類上使用元數據。

Listing 9-10. Using Model Metadata
清單9-10. 使用模型元數據

using System.ComponentModel.DataAnnotations;
using System.Web.Mvc; 
namespace SportsStore.Domain.Entities { public class Product {
[HiddenInput(DisplayValue=false)] public int ProductID { get; set; }
public string Name { get; set; }
[DataType(DataType.MultilineText)] public string Description { get; set; }
public decimal Price { get; set; } public string Category { get; set; } } }

The HiddenInput attribute tells the MVC Framework to render the property as a hidden form element, and the DataType attribute allows us to specify how a value is presented and edited. In this case, we have selected the MultilineText option. The HiddenInput attribute is part of the System.Web.Mvc namespace, which means that we must add a reference to the System.Web.Mvc assembly in the SportsStore.Domain project. The other attributes are contained in the System.ComponentModel.DataAnnotations namespace, whose containing assembly is included in an MVC application project by default.
HiddenInput屬性告訴MVC框架,將該屬性渲染為隱藏的表單元素,而DataType屬性讓我們指示如何顯示或編輯一個值。這里,我們選擇了MultilineText選項。HiddenInput屬於System.Web.Mvc命名空間,因此,我們必須在SportsStore.Domain項目中添加對System.Web.Mvc程序集的引用。其它屬性包含在System.ComponentModel.DataAnnotations命名空間中,默認地,該命名空間的程序集已經包含在MVC應用程序項目中了。

Figure 9-10 shows the Edit page once the metadata has been applied. You can no longer see or edit the ProductId property, and you have a multiline text box for entering the description. However, the UI still looks pretty poor.
圖9-10再次顯示了已經運用了元數據的Edit頁面。你不再能看到或編輯ProductId屬性了,而且,你有了一個輸入description的多行文本框。然而,這個UI看上去還是很差。

圖9-10

Figure 9-10. The effect of applying metadata
圖9-10. 運用元數據的效果

We can make some simple improvements using CSS. When the MVC Framework creates the input fields for each property, it assigns different CSS classes to them. When you look at the source for the page shown in Figure 9-10, you can see that the textarea element that has been created for the product description has been assigned the "text-box-multi-line" CSS class:
我們可以用CSS作一些簡單的改善。當MVC框架為每個屬性創建input字段時,它給這些input賦予不同的CSS的class值。當你查看圖9-10頁面的源代碼時,你可以看到為產品的description創建的文本框元素被賦予了“text-box-multi-line”CSS的class值:

...
<div class="editor-field">
<textarea class="text-box multi-line" id="Description" name="Description">...description text...</textarea>
...

To improve the appearance of the Edit view, add the styles shown in Listing 9-11 to the Admin.css file in the Content folder of the SportsStore.WebUI project.
為了改善Edit視圖的外觀,把清單9-11所示的樣式添加到SportsStore.WebUI 項目的Content文件夾中的Admin.css文件。

Listing 9-11. CSS Styles for the Editor Elements
清單9-11. 用於編輯器元素的樣式

.editor-field { margin-bottom: .8em; }
.editor-label { font-weight: bold; }
.editor-label:after { content: ":" }
.text-box { width: 25em; }
.multi-line { height: 5em; font-family: Segoe UI, Verdana; }

Figure 9-11 shows the effect these styles have on the Edit view.
圖9-11顯示了把這些樣式運用於Edit視圖的效果。

圖9-11

Figure 9-11. Applying CSS to the editor elements
圖9-11. 將CSS運用於編輯器元素

The rendered view is still pretty basic, but it is functional and will do for our administration needs.
所渲染的視圖仍然是很基本的,但它的功能具備了我們的管理需要。

As you saw in this example, the page a template view helper like EditorForModel creates won’t always meet your requirements. We’ll discuss using and customizing template view helpers in detail in Chapter 16.
正如你在這個例子中看到的,像EditorForModel這樣的模板視圖輔助器所創建的頁面並不總能滿足我們的需求。我們將在第16章詳細討論模板視圖輔助器的使用和定制。

Updating the Product Repository
更新產品存儲庫

Before we can process edits, we need to enhance the product repository so that we can save changes. First, we will add a new method to the IProductRepository interface, as shown in Listing 9-12.
在能夠處理編輯之前,我們需要增強產品存儲庫,以使對所作的修改能夠進行保存。首先,我們要對IProductRepository接口添加一個新的方法,如清單9-12所示。

Listing 9-12. Adding a Method to the Repository Interface
清單9-12. 把一個方法添加到存儲庫接口

using System.Linq;
using SportsStore.Domain.Entities; 
namespace SportsStore.Domain.Abstract {
public interface IProductRepository { IQueryable<Product> Products { get; }
void SaveProduct(Product product); } }

We can then add this method to our Entity Framework implementation of the repository, the EFProductRepository class, as shown in Listing 9-13.
然后我們可以把這個方法添加到存儲庫的實體框架實現上,EFProductRepository類,如清單9-13所示。

Listing 9-13. Implementing the SaveProduct Method
清單9-13. 實現SaveProduct方法

using System.Linq;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities; 
namespace SportsStore.Domain.Concrete {
public class EFProductRepository : IProductRepository { private EFDbContext context = new EFDbContext();
public IQueryable<Product> Products { get { return context.Products; } }
public void SaveProduct(Product product) { if (product.ProductID == 0) { context.Products.Add(product); } context.SaveChanges(); } } }

The implementation of the SaveChanges method adds a product to the repository if the ProductID is 0; otherwise, it applies any changes to the existing product.
這個SaveChanges方法的實現在ProductID為0時把一個產品加入存儲庫,否則,它把任何修改運用於這個已經存在的產品。

Handling Edit POST Requests
處理Edit的POST請求

At this point, we are ready to implement an overload of the Edit action method that will handle POST requests when the administrator clicks the Save button. The new method is shown in Listing 9-14.
到了這里,我們已經做好了實現一個重載的Edit動作方法的准備,它在管理員點擊Save按鈕時處理POST請求。這個新方法如清單9-14所示。

Listing 9-14. Adding the POST-Handling Edit Action Method
清單9-14. 添加處理POST的Edit動作方法

[HttpPost]
public ActionResult Edit(Product product) {
if (ModelState.IsValid) { repository.SaveProduct(product); TempData["message"] = string.Format("{0} has been saved", product.Name); return RedirectToAction("Index"); } else { // there is something wrong with the data values // 數據值有錯誤 return View(product); } }

We check that the model binder has been able to validate the data submitted to the user. If everything is OK, we save the changes to the repository, and then invoke the Index action method to return the user to the list of products. If there is a problem with the data, we render the Edit view again so that the user can make corrections.
我們核查了模型綁定器已經能夠驗證遞交給用戶的數據。如果所有事情都OK了,我們便把這些修改保存到存儲庫,然后請求Index動作方法,讓用戶返回到產品列表。如果數據有問題,我們再次渲染Edit視圖,以使用戶能夠進行修正。

After we have saved the changes in the repository, we store a message using the Temp Data feature. This is a key/value dictionary, similar to the session data and View Bag features we have used previously. The key difference is that TempData is deleted at the end of the HTTP request.
在存儲庫中保存了這些修改之后,我們用Temp Data(臨時數據)特性存儲了一條消息。這是一個鍵/值字典,它類似於我們之前已經用過的會話數據和View Bag(視圖包)特性。關鍵差別是TempData在HTTP請求結束時被刪掉了。

Notice that we return the ActionResult type from the Edit method. We’ve been using the ViewResult type until now. ViewResult is derived from ActionResult, and it is used when you want the framework to render a view. However, other types of ActionResults are available, and one of them is returned by the RedirectToAction method. We use that in the Edit action method to invoke the Index action method.
注意,Edit方法返回的是ActionResult類型。到目前為止,我們一直在用ViewResult類型。ViewResult派生於ActionResult,而且它是在你希望框架去渲染一個視圖時使用的。然而,其它類型的ActionResults也是可用的,RedirectToAction方法所返回的是其中之一(意即,ActionResults的類型有好幾種,RedirectToAction的返回類型是ActionResult類型的一種 — 譯者注)。我們在Edit動作方法中用它去請求Index動作方法。

We can’t use ViewBag in this situation because the user is being redirected. ViewBag passes data between the controller and view, and it can’t hold data for longer than the current HTTP request. We could have used the session data feature, but then the message would be persistent until we explicitly removed it, which we would rather not have to do. So, the Temp Data feature is the perfect fit. The data is restricted to a single user’s session (so that users don’t see each other’s TempData) and will persist until we have read it. We will read the data in the view rendered by the action method to which we have redirected the user.
這種情況下我們不能使用ViewBag,因為用戶被重定向了。ViewBag在控制器與視圖之間傳遞數據,但它保持數據的時間不能比當前HTTP請求還長(注意,重定向意味着用戶是跨請求的,故ViewBag不能用於重定向情況下控制與視圖之間的數據傳遞 — 譯者注)。也許我們可以使用會話數據特性,但在另一方面,消息會是持久的,直到我們明確地刪除它為止,那我們還不如不用它。因此,Temp Data特性是十分合適的。其數據被限制到一個單一用戶的會話(於是用戶不會看到相互的TempData),並且將保持到我們已經讀取了它為止。在動作方法渲染的視圖中,我們把這些數據讀給已經被重定向的用戶。

UNIT TEST: EDIT SUBMISSIONS
單元測試:EDIT遞交

For the POST-processing Edit action method, we need to make sure that valid updates to the Product object that the model binder has created are passed to the product repository to be saved. We also want to check that invalid updates—where a model error exists—are not passed to the repository. Here are the test methods:
對於處理POST的Edit動作方法,我們需要確保,對模型綁定器創建的Product對象所作的有效更新,被傳遞給產品存儲庫進行了保存。我們還要檢查非法更新 — 存在模型錯誤 — 不會被傳遞給存儲庫。以下是相應的測試方法:

[TestMethod]
public void Can_Save_Valid_Changes() {
    // Arrange - create mock repository
    // 布置 — 創建模仿存儲庫
    Mock<IProductRepository> mock = new Mock<IProductRepository>(); 
// Arrange - create the controller // 布置 — 創建控制器 AdminController target = new AdminController(mock.Object);
// Arrange - create a product // 布置 — 創建一個產品 Product product = new Product {Name = "Test"};
// Act - try to save the product // 動作 — 試着保存這個產品 ActionResult result = target.Edit(product);
// Assert - check that the repository was called // 斷言 — 檢查,調用了存儲庫 mock.Verify(m => m.SaveProduct(product));
// Assert - check the method result type // 斷言 — 檢查方法的結果類型 Assert.IsNotInstanceOfType(result, typeof(ViewResult)); }
[TestMethod] public void Cannot_Save_Invalid_Changes() { // Arrange - create mock repository // 布置 — 創建模仿存儲庫 Mock<IProductRepository> mock = new Mock<IProductRepository>();
// Arrange - create the controller // 布置 — 創建控制器 AdminController target = new AdminController(mock.Object);
// Arrange - create a product // 布置 — 創建一個產品 Product product = new Product { Name = "Test" };
// Arrange - add an error to the model state // 布置 — 把一個錯誤添加到模型狀態 target.ModelState.AddModelError("error", "error");
// Act - try to save the product // 動作 — 試圖保存產品 ActionResult result = target.Edit(product);
// Assert - check that the repository was not called // 斷言 — 存儲庫未被調用 mock.Verify(m => m.SaveProduct(It.IsAny<Product>()), Times.Never());
// Assert - check the method result type // 斷言 — 檢查方法的結果類型 Assert.IsInstanceOfType(result, typeof(ViewResult)); }

Displaying a Confirmation Message
顯示一條確認消息

We are going to deal with the message we stored using TempData in the _AdminLayout.cshtml layout file. By handling the message in the template, we can create messages in any view that uses the template, without needing to create additional Razor blocks. Listing 9-15 shows the change to the file.
我們打算在_AdminLayout.cshtml布局文件中處理用TempData存儲的消息。通過在模板中處理消息,我們可以在任何使用此模板的視圖中創建消息,而不需要創建附加的Razor塊。清單9-15顯示了對此文件的修改。

Listing 9-15. Handling the ViewBag Message in the Layout
清單9-15. 在布局中處理ViewBag消息

<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Admin.css")" rel="stylesheet" type="text/css" />
</head>
<body>
    <div>
        @if (TempData["message"] != null) {
            <div class="Message">@TempData["message"]</div>
        }
        @RenderBody()
    </div>
</body>
</html>

■ Tip The benefit of dealing with the message in the template like this is that users will see it displayed on whatever page is rendered after they have saved a change. At the moment, we return them to the list of products, but we could change the workflow to render some other view, and the users will still see the message (as long as the next view also uses the same layout).
提示:像這樣在模板中處理消息的好處是,在用戶保存了修改之后,可以看到它顯示在任何渲染頁面上。此刻,我們是把消息返回給產品列表,但我們可以改變此工作流去渲染一些其它視圖,而用戶將仍然能夠看到這些消息

We how now have all the elements we need to test editing products. Run the application, navigate to the Admin/Index URL, and make some edits. Click the Save button. You will be returned to the list view, and the TempData message will be displayed, as shown in Figure 9-12.
我們現在有了對編輯產品進行測試的所有元素。運行此應用程序,導航到Admin/Index,作一些編輯。點擊“Save”按鈕。你將被返回到列表視圖,而TempData消息將被顯示出來,如圖9-12所示。

圖9-12

Figure 9-12. Editing a product and seeing the TempData message
圖9-12. 編輯一個產品並看到TempData消息

The message will disappear if you reload the product list screen, because TempData is deleted when it is read. That is very convenient, since we don’t want old messages hanging around.
如果你刷新產品列表屏幕,這條消息會消失,因為TempData在讀取它時被刪除了。這是很方便的,因為,我們不希望還會殘留過時的消息。

Adding Model Validation
添加模型驗證

As is always the case, we need to add validation rules to our model entity. At the moment, the administrator could enter negative prices or blank descriptions, and SportsStore would happily store that data in the database. Listing 9-16 shows how we have applied data annotations attributes to the Product class, just as we did for the ShippingDetails class in the previous chapter.
情況總是這樣,我們需要對我們的模型實體添加驗證規則。此刻,管理員能夠輸入負數價格或空白的產品描述,那么SportsStore也將會愉快地把這些數據存儲到數據庫中(這當然不行,所以要添加驗證規則 — 譯者注)。清單9-16演示了我們把數據注解屬性(Data annotations attributes)運用於Product類,就像我們上一章對ShippingDetails類所做的那樣。

Listing 9-16. Applying Validation Attributes to the Product Class
清單9-16. 將驗證屬性運用於Product類

using System.ComponentModel.DataAnnotations;
using System.Web.Mvc; 
namespace SportsStore.Domain.Entities {
public class Product { [HiddenInput(DisplayValue=false)] public int ProductID { get; set; }
[Required(ErrorMessage = "Please enter a product name")] public string Name { get; set; }
[Required(ErrorMessage = "Please enter a description")] [DataType(DataType.MultilineText)] public string Description { get; set; }
[Required] [Range(0.01, double.MaxValue, ErrorMessage = "Please enter a positive price")] public decimal Price { get; set; }
[Required(ErrorMessage = "Please specify a category")] public string Category { get; set; } } }

■ Note We have reached the point with the Product class where there are more attributes than properties. Don’t worry if you feel that the attributes make the class unreadable. You can move the attributes into a different class and tell MVC where to find them. We’ll show you how to do this in Chapter 16.
注:此時,我們已經讓Product類的注解屬性比類屬性還多了。如果你感覺這些注解屬性影響了類的可讀性,不必擔心。你可以把這些注解移到一個不同的類中去,並告訴MVC到哪里去找它們。我們將在第16章向你演示如何做這種事。

When we used the Html.EditorForModel helper method to create the form elements to edit a Product, the MVC Framework added all the markup and CSS needed to display validation errors inline. Figure 9-13 shows how this appears when you edit a product and enter data that breaks the validation rules we applied in Listing 9-16.
當我們使用Html.EditorForModel輔助器方法來創建form元素以編輯Product時,MVC框架添加了與顯示驗證錯誤內聯的所有標記和CSS。圖9-13演示了,當你編輯一個產品,並且輸入的數據打破了我們在清單9-16中運用的驗證規則時,界面是如何顯示的。

圖9-13

Figure 9-13. Data validation when editing products
圖9-13. 編輯產品時的數據驗證

Enabling Client-Side Validation
啟用客戶端驗證

At present, our data validation is applied only when the administrator submits edits to the server. Most web users expect immediate feedback if there are problems with the data they have entered. This is why web developers often want to perform client-side validation, where the data is checked in the browser using JavaScript. The MVC Framework can perform client-side validation based on the data annotations we applied to the domain model class.
現在,只有當管理員把編輯遞交給服務器時,才會運用我們的數據驗證。大多數web用戶期望,如果他們輸入的數據有問題時,會立即得到反饋。這就是web開發人員經常希望執行客戶端驗證的原因,此時,數據在瀏覽器中用JavaScript進行檢查。MVC框架可以根據我們運用於域模型類的數據注解來執行客戶端驗證。

This feature is enabled by default, but it hasn’t been working because we have not added links to the required JavaScript libraries. The simplest place to add these links is in the _AdminLayout.cshtml file, so that client validation can work on any page that uses this layout. Listing 9-17 shows the changes to the layout. The MVC client-side validation feature is based on the jQuery JavaScript library, which can be deduced from the name of the script files.
這一特性默認是可用的,但它並不會起作用,因為我們還沒有添加對所需的JavaScript庫的連接。添加這些連接最簡單的地方是在_AdminLayout.cshtml文件中,以使客戶端驗證能夠在使用這個布局的任何頁面上起作用。清單9-17顯示了對布局的修改。MVC客戶端驗證特性基於JQuery的JavaScript庫,我們可以根據腳本文件名來推斷JQuery庫。

Listing 9-17. Importing JavaScript Files for Client-Side Validation
清單9-17. 引入用於客戶端驗證的JavaScript文件

<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Admin.css")" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")"
        type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.min.js")"
        type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")"
        type="text/javascript"></script>
</head>
<body>
    <div>
        @if (TempData["message"] != null) {
            <div class="Message">@TempData["message"]</div>
        }
        @RenderBody()
    </div>
</body>
</html>

With these additions, client-side validation will work for our administration views. The appearance of error messages to the user is the same, because the CSS classes that are used by the server validation are also used by the client-side validation. But the response is immediate and doesn’t require a request to be sent to the server.
通過這些添加,客戶端驗證將對我們的管理視圖生效。顯示給用戶的錯誤消息的外觀是相同的,因為服務器驗證所使用的CSS的class也由客戶端驗證所使用。但響應是快速的,而且不需要把請求發送到服務器。

In most situations, client-side validation is a useful feature, but if, for some reason, you don’t want to validate at the client, you need to use the following statements:
在大多數情況下,客戶端驗證是一個有用的特性,但如果出於某種原因,你不希望在客戶端驗證,則需要使用以下語句:

HtmlHelper.ClientValidationEnabled = false;
HtmlHelper.UnobtrusiveJavaScriptEnabled = false;

If you put these statements in a view or in a controller, then client-side validation is disabled only for the current action. You can disable client-side validation for the entire application by using those statements in the Application_Start method of Global.asax or by adding values to the Web.config file, like this:
如果你把這些語句放在一個視圖中或一個控制器中,那末客戶端驗證只對當前動作失效。通過在Global.asax的Application_Start方法中使用這些語句,你可以取消整個應用程序的客戶端驗證,或是把這些值運用於Web.config文件,像這樣:

<configuration>
    <appSettings>
        <add key="ClientValidationEnabled" value="false"/>
        <add key="UnobtrusiveJavaScriptEnabled" value="false"/>
    </appSettings>
</configuration>

Creating New Products
創建新產品

Next, we will implement the Create action method, which is the one specified in the Add a new product link in the product list page. This will allow the administrator to add new items to the product catalog. Adding the ability to create new products will require only one small addition and one small change to our application. This is a great example of the power and flexibility of a well-thought-out MVC application.
下一步,我們將實現Create動作方法,這是在產品列表頁面中“Add a new product(添加新產品)”鏈接所指定的方法。它允許管理員把一個新條目添加到產品分類。添加創建新產品的能力只需要一個小的附件,並對我們的應用程序作一些小的修改即可。這是精心構思MVC應用程序功能和適應性的一個很好的例子。

First, add the Create method, shown in Listing 9-18, to the AdminController class.
首先,把Create方法加到AdminController類,如清單9-18所示。

Listing 9-18. Adding the Create Action Method to the Admin Controller
清單9-18. 將Create動作方法添加到Admin控制器

public ViewResult Create() {
    return View("Edit", new Product());
}

The Create method doesn’t render its default view. Instead, it specifies that the Edit view should be used. It is perfectly acceptable for one action method to use a view that is usually associated with another view. In this case, we inject a new Product object as the view model so that the Edit view is populated with empty fields.
Create方法並不渲染它的默認視圖。而是,它指明應該使用Edit視圖。讓一個動作方法去使用一個通常與另一個視圖關聯的視圖是一件很愜意的事情。在這里,我們注入一個新的Product對象作為視圖模型,以便Edit視圖用空字段進行填充。

This leads us to the modification. We would usually expect a form to postback to the action that rendered it, and this is what the Html.BeginForm assumes by default when it generates an HTML form. However, this doesn’t work for our Create method, because we want the form to be posted back to the Edit action so that we can save the newly created product data. To fix this, we can use an overloaded version of the Html.BeginForm helper method to specify that the target of the form generated in the Edit view is the Edit action method of the Admin controller, as shown in Listing 9-19.
這使我們能夠進行修改。通常,我們期望一個表單回遞給渲染它的動作,而且這正是Html.BeginForm在生成一個HTML表單時所假設的默認情況。然而,我們的Create方法並不是這樣,因為我們希望此表單被回遞給Edit動作,以便我們可以保存這個新創建的產品數據。為了對此進行修正(這里,Create動作方法調用了Edit視圖,當用戶在此視圖的表單中編輯數據,然后進行遞交時,默認會被回遞給Create動作方法,但我們希望被回遞給Edit動作方法,故需要修正 — 譯者注),我們可以用重載的Html.BeginForm輔助器方法來指明:在Edit視圖中生成的表單的目標(始終)是Admin控制器的Edit動作方法,如清單9-19所示。

Listing 9-19. Explicitly Specifying an Action Method and Controller for a Form
清單9-19. 明確地指定表單所用的控制器和動作方法

@model SportsStore.Domain.Entities.Product
@{ ViewBag.Title = "Admin: Edit " + @Model.Name; Layout = "~/Views/Shared/_AdminLayout.cshtml"; }
<h1>Edit @Model.Name</h1> @using (Html.BeginForm("Edit", "Admin")) { @Html.EditorForModel() <input type="submit" value="Save" /> @Html.ActionLink("Cancel and return to List", "Index") }

Now the form will always be posted to the Edit action, regardless of which action rendered it. We can create products by clicking the Add a new product link and filling in the details, as shown in Figure 9-14.
現在,此表單將總是被遞交給Edit動作,而不管渲染它的是哪個動作。通過點擊“Add a new product(添加新產品)”鏈接,並進行詳細填充,我們可以創建產品,如圖9-14所示。

圖9-14

Figure 9-14. Adding a new product to the catalog
圖9-14. 對分類添加一個新產品

Deleting Products
刪除產品

Adding support for deleting items is fairly simple. First, we add a new method to the IProductRepository interface, as shown in Listing 9-20.
添加對條目進行刪除的支持相當簡單。首先,我們把一個新方法添加到IProductRepository接口,如清單9-20所示。

Listing 9-20. Adding a Method to Delete Products
清單9-20. 添加一個刪除產品的方法

using System.Linq;
using SportsStore.Domain.Entities; 
namespace SportsStore.Domain.Abstract {
public interface IProductRepository { IQueryable<Product> Products { get; }
void SaveProduct(Product product);
void DeleteProduct(Product product); } }

Next, we implement this method in our Entity Framework repository class, EFProductRepository, as shown in Listing 9-21.
下一步,在我們的Entity Framework存儲庫類EFProductRepository中實現這個方法,如清單9-21所示。

Listing 9-21. Implementing Deletion Support in the Entity Framework Repository Class
清單9-21. 在實體框架存儲庫類中實現刪除支持

...
public void DeleteProduct(Product product) {
    context.Products.Remove(product);
    context.SaveChanges();
}
...

The final step is to implement a Delete action method in the Admin controller. This action method should support only POST requests, because deleting objects is not an idempotent operation. As we’ll explain in Chapter 11, browsers and caches are free to make GET requests without the user’s explicit consent, so we must be careful to avoid making changes as a consequence of GET requests. Listing 9-22 shows the new action method.
最后一步是在Admin控制器中實現一個Delete動作方法。這個動作方法應當只支持POST請求,因為刪除對象不是一個冪等的(idempotent)操作。正如我們將在第11章要解釋的那樣,瀏覽器和緩存會隨意地形成GET請求而不要用戶明確的同意,因此,我們必須小心地避免形成GET請求的結果。清單9-22演示了這個新方法。

Listing 9-22. The Delete Action Method
清單9-22. Delete動作方法

[HttpPost]
public ActionResult Delete(int productId) {
    Product prod = repository.Products.FirstOrDefault(p => p.ProductID == productId); 
if (prod != null) { repository.DeleteProduct(prod); TempData["message"] = string.Format("{0} was deleted", prod.Name); }
return RedirectToAction("Index"); }

UNIT TEST: DELETING PRODUCTS
單元測試:刪除產品

We want to test two behaviors of the Delete action method. The first is that when a valid ProductID is passed as a parameter, the action method calls the DeleteProduct method of the repository and passes the correct Product object to be deleted. Here is the test:
我們希望測試Delete動作方法的兩個行為。第一是當一個有效的ProductID作為參數傳遞時,該動作方法調用存儲庫的DeleteProduct方法,並把正確的Product對象刪除掉。以下是該測試:

[TestMethod]
public void Can_Delete_Valid_Products() {
    // Arrange - create a Product
    // 布置 — 創建一個產品
    Product prod = new Product { ProductID = 2, Name = "Test" };
// Arrange - create the mock repository // 布置 — 創建模仿存儲庫 Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m => m.Products).Returns(new Product[] { new Product {ProductID = 1, Name = "P1"}, prod, new Product {ProductID = 3, Name = "P3"}, }.AsQueryable());
// Arrange - create the controller // 布置 — 創建控制器 AdminController target = new AdminController(mock.Object);
// Act - delete the product // 動作 — 刪除產品 target.Delete(prod.ProductID);
// Assert - ensure that the repository delete method was // called with the correct Product // 斷言 — 確保存儲庫的刪除方法是針對正確的產品被調用的 mock.Verify(m => m.DeleteProduct(prod)); }

The second test is to ensure that if the parameter value passed to the Delete method does not correspond to a valid product in the repository, the repository DeleteProduct method is not called. Here is the test:
第二個測試是,如果傳遞給Delete方法的參數值不能對應於存儲庫中的一個有效的產品,確保存儲庫的DeleteProduct方法不會被調用。以下是該測試:

[TestMethod]
public void Cannot_Delete_Invalid_Products() {
    // Arrange - create the mock repository
    // 布置 — 創建模仿存儲庫
    Mock<IProductRepository> mock = new Mock<IProductRepository>();
    mock.Setup(m => m.Products).Returns(new Product[] {
        new Product {ProductID = 1, Name = "P1"},
        new Product {ProductID = 2, Name = "P2"},
        new Product {ProductID = 3, Name = "P3"},
    }.AsQueryable());
// Arrange - create the controller // 布置 — 創建控制器 AdminController target = new AdminController(mock.Object);
// Act - delete using an ID that doesn't exist // 動作 — 用一個不存在的ID進行刪除 target.Delete(100);
// Assert - ensure that the repository delete method was // called with the correct Product // 斷言 — 確保存儲庫刪除方法是針對正確的Product進行調用的 mock.Verify(m => m.DeleteProduct(It.IsAny<Product>()), Times.Never()); }

You can see the new function at work simply by clicking one of the Delete buttons in the product list page, as shown in Figure 9-15. As shown in the figure, we have taken advantage of the TempData variable to display a message when a product is deleted from the catalog.
簡單地點擊產品列表頁面中的一個Delete按鈕,你可以看到這個新功能在工作,如圖9-15所示。正如圖中所顯示的那樣,當產品從分類中被刪除時,我們利用了TempData變量顯示了一條消息。

圖9-15

Figure 9-15. Deleting a product from the catalog
圖9-15. 從分類中刪除一個產品

And at this point, we’ve implemented all of the CRUD operations. We can now create, read, update, and delete products.
到了這里,我們已經完成了所有CRUD操作。我們現在可以創建、讀取、更新、及刪除產品了。

Securing the Administration Features
使管理特性安全

It won’t have escaped your attention that anyone would be able to modify the product catalog if we deployed the application right now. All someone would need to know is that the administration features are available using the Admin/Index URL. To prevent random people from wreaking havoc, we are going to password-protect access to the entire Admin controller.
如果現在部署這個應用程序,你肯定會注意到,任何人都可以修改產品分類。一個人只要知道,使用Admin/Index網址,就可以使用管理特性。為了阻止一些人的惡意破壞,我們打算對整個Admin控制器的訪問進行口令式保護。

Setting Up Forms Authentication
建立表單認證

Since ASP.NET MVC is built on the core ASP.NET platform, we have access to the ASP.NET Forms Authentication facility, which is a general-purpose system for keeping track of who is logged in. We’ll cover forms authentication in more detail in Chapter 22. For now, we’ll simply show you how to set up the most basic of configurations.
由於ASP.NET MVC建立在核心的ASP.NET平台之上,我們可以訪問ASP.NET的表單認證工具,它是對已登錄人員保持跟蹤的一個通用系統。我們將在第22章更詳細地涉及表單認證。現在,我們只簡單地向你演示如何建立最基本的配置。

If you open the Web.config file, you will be able to find a section entitled authentication, like this one:
如果打開Web.config文件,你將能夠找到一個以authentication為標題的小節,像這樣:

<authentication mode="Forms">
  <forms loginUrl="~/Account/LogOn" timeout="2880"/>
</authentication>

As you can see, forms authentication is enabled automatically in an MVC application created with the Empty or Internet Application template. The loginUrl attribute tells ASP.NET which URL users should be directed to when they need to authenticate themselves—in this case, the /Account/Logon page. The timeout attribute specifies how long a user is authenticated after logging in. By default, this is 48 hours (2,880 minutes). We’ll explain some of the other configuration options in Chapter 22.
正如你所看到的,在一個用Empty或Internet應用程序模板創建的MVC應用程序中,表單認證是自動可用的。LoginUrl屬性告訴ASP.NET,當用戶需要對其自己進行認證時,他們應該被定向到哪個URL — 這里是/Account/Logon頁面。Timeout屬性指明一個被認證的用戶登錄之后的時間有多長。默認地,是48小時(2880分鍾)。我們將在第22章解釋一些其它配置選項。

■ Note The main alternative to forms authentication is Windows authentication, where the operating system credentials are used to identify users. This is a great facility if you are deploying intranet applications and all of your users are in the same Windows domain. However, it’s not applicable for Internet applications.
注:形成認證的另一個主要選項是Windows認證,它以操作系統憑據用於標識用戶。如果你部署一個企業內部網(intranet)應用程序,而你的所有用戶都在同一個Windows域中,這是一個很好的工具。然而,它不適用於互聯網(Internet)應用程序。

If we had selected the MVC Internet Application template when we created the SportsStore project, Visual Studio would have created the AccountController class and its LogOn action method for us. The implementation of this method would have used the core ASP.NET membership feature to manage accounts and passwords, which we’ll cover in Chapter 22. Here, the membership system would be overkill for our application, so we will use a simpler approach. We will create the controller ourselves.
如果我們在創建SportsStore項目時已經選擇了“MVC Internet Application(MVC網絡應用程序)”模板,Visual Studio將會為我們創建AccountController類及其LogOn動作方法。這個方法的實現將使用核心ASP.NET的成員特性來管理賬號和口令,這些將在第22章涉及。這里,對我們的應用程序而言,成員系統是不必要的過度行為,因此我們將使用一種更簡單一點的辦法。我們將創建一個我們自己的控制器。

To start, we will create a username and password that will grant access to the SportsStore administration features. Listing 9-23 shows the changes to apply to the authentication section of the Web.config file.
為了開始工作,我們將創建一個允許訪問SportsStore管理特性的用戶名和口令。清單9-23顯示了運用於Web.config文件的authentication(認證)小節的修改。

Listing 9-23. Defining a Username and Password
清單9-23. 定義用戶名和口令

<authentication mode="Forms">
    <forms loginUrl="~/Account/LogOn" timeout="2880">
        <credentials passwordFormat="Clear">
            <user name="admin" password="secret" />
        </credentials>
    </forms>
</authentication>

We have decided to keep things very simple and hard-code a username (admin) and password (secret) in the Web.config file. Most web applications using forms authentication store user credentials in a database, which we show you how to do in Chapter 22. Our focus in this chapter is applying basic security to an MVC application, so hard-coded credentials suit us just fine.
我們決定讓事情保持簡單,並且在Web.config文件中硬編碼了一個用戶名(admin)和口令(secret)。大多數使用表單認證的web應用程序會把用戶憑據存儲在一個數據庫中,我們將在第22章演示如何去做。本章中的焦點是把基本的安全性運用於一個MVC應用程序,因此硬編碼的憑據正好是合適的。

Applying Authorization with Filters
運用帶有過濾器的授權

The MVC Framework has a powerful feature called filters. These are .NET attributes that you can apply to an action method or a controller class. They introduce additional logic when a request is processed. Different kinds of filters are available, and you can create your own custom filters, too, as we’ll explain in Chapter 13. The filter that interests us at the moment is the default authorization filter, Authorize. We will apply it to the AdminController class, as shown in Listing 9-24.
MVC框架有一個叫做過濾器的功能強大的特性。這些過濾器是一些.NET的注解屬性,你可以把它們運用於一個動作方法或一個控制器類。它們在一個請求被處理時,會引入一些附加的邏輯。各種不同的過濾器都是可用的,而且你也可以創建你自己的過濾器,就像我們在第13章所解釋的那樣。此刻我們感興趣的過濾器是默認的授權過濾器,Authorize。我們將把它運用於AdminController類,如清單9-24所示。

Listing 9-24. Adding the Authorize Attribute to the Controller Class
清單9-24. 將Authorize(授權)屬性添加到控制器類

using System.Web.Mvc;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using System.Linq; 
namespace SportsStore.WebUI.Controllers { [Authorize] public class AdminController : Controller { private IProductRepository repository; public AdminController(IProductRepository repo) { repository = repo; } ...

When applied without any parameters, the Authorize attribute grants access to the controller action methods if the user is authenticated. This means that if you are authenticated, you are automatically authorized to use the administration features. This is fine for SportsStore, where there is only one set of restricted action methods and only one user. In Chapters 13 and 22, you’ll see how to apply the Authorize filter more selectively to separate the notions of authentication (being identified by the system) and authorized (being allowed to access a given action method).
當不帶任何參數地運用時,如果用戶已被認證,這個Authorize注解屬性便允許訪問該控制器的動作方法。意即,如果你被認證了,你就被自動地授權使用管理特性。這對SportsStore是很好的,在這里只有一組受限的動作方法並只有一個用戶。在第13章和第22章中,你將看到如何更有選擇性地運用Authorize過濾器,把認證(由系統標識)與授權(允許訪問給定的動作方法)分開來。

■ Note You can apply filters to an individual action method or to a controller. When you apply a filter to a controller, it works as though you had applied it to every action method in the controller class. In Listing 9-24, we applied the Authorize filter to the class, so all of the action methods in the Admin controller are available only to authenticated users.
注:你可以把過濾器運用於個別的動作方法或控制器。當你把一個過濾器運用於一個控制器時,就如同把它運用於該控制器中的每一個動作方法一樣。在清單9-24中,我們把Authorize過濾器運用於這個類,因此,在Admin控制器中的所有動作方法都只對已認證用戶才是可用的。

You can see the effect that the Authorize filter has by running the application and navigating to the /Admin/Index URL. You will see an error similar to the one shown in Figure 9-16.
通過運行應用程序,並導航到/Admin/Index網址,便可以看到Authorize過濾器所具有的效果。你將看到類似於圖9-16所顯示的錯誤。

圖9-16

Figure 9-16. The effect of the Authorize filter
圖9-16. Authorize過濾器的效果

When you try to access the Index action method of the Admin controller, the MVC Framework detects the Authorize filter. Since you have not been authenticated, you are redirected to the URL specified in the Web.config forms authentication section: Account/LogOn. We have not created the Account controller yet, but you can still see that the authentication is working, although it doesn’t prompt us to authenticate ourselves.
當你試圖訪問Admin控制器的Index動作方法時,MVC框架檢測到了Authorize過濾器。由於你還沒有被認證,便被重定向到Web.config表單認證小節所指定的URL:Account/LogOn。我們還沒有創建Account控制器,但你仍然可以看到認證已經起作用了,盡管它還沒有提示我們進行認證。

Creating the Authentication Provider
創建認證提供器

Using the forms authentication feature requires us to call two static methods of the System.Web.Security.FormsAuthentication class:
使用表單認證特性需要我們調用System.Web.Security.FormsAuthentication類的兩個靜態方法:

  • The Authenticate method lets us validate credentials supplied by the user.
    Authenticate方法讓我們驗證由用戶提供的憑據。
  • The SetAuthCookie method adds a cookie to the response to the browser, so that users don’t need to authenticate every time they make a request.
    SetAuthCookie方法把一個cookie添加到對瀏覽器的響應,這樣,用戶在發出請求時不需要每次都要認證。

The problem with calling static methods in action methods is that it makes unit testing the controller difficult. Mocking frameworks such as Moq can mock only instance members. This problem arises because the FormsAuthentication class predates the unit-testing-friendly design of MVC. The best way to address this is to decouple the controller from the class with the static methods using an interface. An additional benefit is that this fits in with the broader MVC design pattern and makes it easier to switch to a different authentication system later.
在動作方法中調用靜態方法帶來的問題是,它會使控制器的單元測試困難。像Moq這樣的模仿框架只能模仿實例成員。之所以會出現這一問題,是因為FormsAuthentication先於MVC的友好單元測試設計。解決這一問題的最好辦法是,用一個接口去掉控制器與帶有這種靜態方法的類之間的耦合。一個附帶的好處是這符合更廣泛的MVC設計模式,並且使它更容易切換到不同的認證系統。

We start by defining the authentication provider interface. Create a new folder called Abstract in the Infrastructure folder of the SportsStore.WebUI project and add a new interface called IAuthProvider. The contents of this interface are shown in Listing 9-25.
我們從定義認證提供器接口開始。在SportsStore.WebUI項目的Infrastructure文件夾中創建一個名為Abstract的新文件夾,並添加一個名為IAuthProvider的新接口。該接口的內容如清單9-25所示。

Listing 9-25. The IAuthProvider Interface
清單9-25. IAuthProvider接口

namespace SportsStore.WebUI.Infrastructure.Abstract {
    public interface IAuthProvider {
        bool Authenticate(string username, string password);
    }
}

We can now create an implementation of this interface that acts as a wrapper around the static methods of the FormsAuthentication class. Create another new folder in Infrastructure—this time called Concrete—and create a new class called FormsAuthProvider. The contents of this class are shown in Listing 9-26.
我們現在可以創建該接口的一個實現,以此作為FormsAuthentication類中靜態方法的封裝程序。在Infrastructure文件夾中創建另一個新文件夾 — 這次叫做Concrete — 並創建一個名為FormsAuthProvider的新類。這個類的內容如清單9-26所示。

Listing 9-26. The FormsAuthProvider Class
清單8-26. FormsAuthProvider類

using System.Web.Security;
using SportsStore.WebUI.Infrastructure.Abstract; 
namespace SportsStore.WebUI.Infrastructure.Concrete {
public class FormsAuthProvider : IAuthProvider {
public bool Authenticate(string username, string password) { bool result = FormsAuthentication.Authenticate(username, password); if (result) { FormsAuthentication.SetAuthCookie(username, false); } return result; } } }

The implementation of the Authenticate model calls the static methods that we wanted to keep out of the controller. The final step is to register the FormsAuthProvider in the AddBindings method of the NinjectControllerFactory class, as shown in Listing 9-27 (the addition is shown in bold).
這個認證模型的實現調用了我們希望放在控制器之外的靜態方法。最后一步是在NinjectControllerFactory類的AddBindings方法中注冊這個FormsAuthProvider,如清單9-27所示。

Listing 9-27. Adding the Authentication Provider Ninject Binding
清單9-27. 添加認證提供器的Ninject綁定

private void AddBindings() {
    // put additional bindings here
    // 這里放置附加綁定器
    ninjectKernel.Bind<IProductRepository>().To<EFProductRepository>(); 
// create the email settings object // 創建郵件設置對象 EmailSettings emailSettings = new EmailSettings { WriteAsFile = bool.Parse(ConfigurationManager.AppSettings["Email.WriteAsFile"] ?? "false") }; ninjectKernel.Bind<IOrderProcessor>() .To<EmailOrderProcessor>() .WithConstructorArgument("settings", emailSettings);
ninjectKernel.Bind<IAuthProvider>().To<FormsAuthProvider>(); }

Creating the Account Controller
創建Account控制器

The next task is to create the Account controller and the LogOn action method. In fact, we will create two versions of the LogOn method. The first will render a view that contains a login prompt, and the other will handle the POST request when users submit their credentials.
接下來的任務是創建Account控制器和LogOn動作方法。事實上,我們將創建兩個版本的LogOn方法。第一個將渲染一個登錄提示的視圖,另一個將在用戶遞交他們的憑據時處理POST請求。

To get started, we will create a view model class that we will pass between the controller and the view. Add a new class to the Models folder of the SportsStore.WebUI project called LogOnViewModel and edit the content so that it matches Listing 9-28.
為了開始工作,我們將創建一個要在控制器和動作方法之間進行傳遞的視圖模型類。把一個新類添加到SportsStore.WebUI項目的Models文件夾,名為LogOnViewModel,並編輯其內容,使之與清單9-28吻合。

Listing 9-28. The LogOnViewModel Class
清單9-28. LogOnViewModel類

using System.ComponentModel.DataAnnotations; 
namespace SportsStore.WebUI.Models {
public class LogOnViewModel { [Required] public string UserName { get; set; }
[Required] [DataType(DataType.Password)] public string Password { get; set; } } }

This class contains properties for the username and password, and uses the data annotations to specify that both are required. In addition, we use the DataType attribute to tell the MVC Framework how we want the editor for the Password property displayed.
這個類含有用戶名和口令屬性,並且使用數據注解來指定兩者都是必須的。此外,我們使用DataType屬性來告訴MVC框架,我們希望如何顯示Password屬性的編輯器。

Given that there are only two properties, you might be tempted to do without a view model and rely on the ViewBag to pass data to the view. However, it is good practice to define view models so that the data passed from the controller to the view and from the model binder to the action method is typed consistently. This allows us to use template view helpers more easily.
所給定的只有兩個屬性,這也許會引誘你不使用一個視圖模型,而依靠ViewBag來把數據傳遞給視圖。然而,定義視圖模型是一種很好的實踐,因為,把數據從控制器傳遞給視圖以及從模型綁定器傳遞給動作方法,是十分典型的。這使我們能夠更容易地使用模板視圖輔助器。

Next, create a new controller called AccountController, as shown in Listing 9-29.
接下來,創建一個名為AccountController的新控制器,如清單9-29所示。

Listing 9-29. The AccountController Class
清單9-29. AccountController類

using System.Web.Mvc;
using SportsStore.WebUI.Infrastructure.Abstract;
using SportsStore.WebUI.Models; 
namespace SportsStore.WebUI.Controllers {
public class AccountController : Controller { IAuthProvider authProvider;
public AccountController(IAuthProvider auth) { authProvider = auth; }
public ViewResult LogOn() { return View(); }
[HttpPost] public ActionResult LogOn(LogOnViewModel model, string returnUrl) { if (ModelState.IsValid) { if (authProvider.Authenticate(model.UserName, model.Password)) { return Redirect(returnUrl ?? Url.Action("Index", "Admin")); } else { ModelState.AddModelError("", "Incorrect username or password"); return View(); } } else { return View(); } } } }

Creating the View
創建視圖

Right-click in one of the action methods in the Account controller class and select Add View from the pop-up menu. Create a strongly typed view called LogOn that uses LogOnViewModel as the view model type, as shown in Figure 9-17. Check the option to use a Razor layout and select _AdminLayout.cshtml.
右擊Account控制器類中的一個動作方法,並從彈出菜單選擇“添加視圖”。創建一個名為LogOn的強類型視圖,用LogOnViewModel作為該視圖的模型類型,如圖9-17所示。選中“use a Razor layout(使用一個Razor布局)”復選框,並選擇_AdminLayout.cshtml。

圖9-17

Figure 9-17. Adding the LogOn view
圖9-17. 添加LogOn視圖

Click the Add button to create the view and edit the markup so that it matches Listing 9-30.
點擊“添加”按鈕以創建這個視圖,並編輯其標記使之與清單9-30吻合。

Listing 9-30. The LogOn View
清單9-30. LogOn視圖

@model SportsStore.WebUI.Models.LogOnViewModel
@{ ViewBag.Title = "Admin: Log In"; Layout = "~/Views/Shared/_AdminLayout.cshtml"; }
<h1>Log In</h1> <p>Please log in to access the administrative area:</p> @using(Html.BeginForm()) { @Html.ValidationSummary(true) @Html.EditorForModel() <p><input type="submit" value="Log in" /></p> }

You can see how the view looks in Figure 9-18.
你可以在圖9-18中看到該視圖的樣子。

圖9-18

Figure 9-18. The LogOn view
圖9-18. LogOn視圖

The DataType attribute has led the MVC Framework to render the editor for the Password property as an HTML password-input element, which means that the characters in the password are not visible. The Required attribute that we applied to the properties of the view model are enforced using client-side validation (the required JavaScript libraries are included in the layout). Users can submit the form only after they have provided both a username and password, and the authentication is performed at the server when we call the FormsAuthentication.Authenticate method.
DataType注解屬性讓MVC框架把Password屬性的編輯器渲染成一個HTML的口令輸入元素,意即,在口令中的字符是不可見的。我們運用於視圖模型屬性的Required注解屬性強制使用客戶端驗證(所需要的JavaScript庫被包含在布局中)。用戶只可以在他們提供了用戶和口令之后才能遞交這個表單,而當我們調用FormsAuthentication.Authenticate方法時,認證在服務器端執行。

■ Caution In general, using client-side data validation is a good idea. It off-loads some of the work from your server and gives users immediate feedback about the data they are providing. However, you should not be tempted to perform authentication at the client, since this would typically involve sending valid credentials to the client so they can be used to check the username and password that the user has entered, or at least trusting the client’s report of whether they have successfully authenticated. Authentication must always be done at the server.
小心:一般而言,使用客戶端驗證是一種很好的思想。它卸載了服務器的一些工作,並對用戶提供的數據給出了快速的反饋。然而,你不應該試圖在客戶端進行認證,因為這將典型地要涉及到把有效的憑據發送到客戶端,以便能夠用它來檢查用戶已經輸入的用戶名和口令,或者至少對用戶是否已成功驗證的客戶端報告是信任的。認證必須永遠在服務器完成。

When we receive bad credentials, we add an error to the ModelState and rerender the view. This causes our message to be displayed in the validation summary area, which we have created by calling the Html.ValidationSummary helper method in the view.
當接收到一個劣質憑據時,我們把一條錯誤消息加到了ModelState,並渲染這個視圖。這會引起在驗證摘要區域顯示這條消息,該摘要區是我們在視圖中通過調用Html.ValidationSummary輔助器方法創建的區域。

■ Note Notice that we call the Html.ValidationSummary helper method with a bool parameter value of true in Listing 9-27. Doing so excludes any property validation messages from being displayed. If we had not done this, any property validation errors would be duplicated in the summary area and next to the corresponding input element.
注:注意,在清單9-27中,我們調用了Html.ValidationSummary輔助器方法,其帶有一個布爾參數值true。這樣便排除了顯示任何屬性驗證消息。如果我們不這么做,屬性驗證錯誤將在摘要區和相應的input元素之后重復顯示。

UNIT TEST: AUTHENTICATION
單元測試:認證

Testing the Account controller requires us to check two behaviors: a user should be authenticated when valid credentials are supplied, and a user should not be authenticated when invalid credentials are supplied. We can perform these tests by creating mock implementations of the IAuthProvider interface and checking the type and nature of the result of the controller LogOn method, like this:
測試Account控制器需要我們檢查兩個行為:在提供了有效憑據時,用戶應該被認證;而在提供非法憑據時,用戶不應該被認證。我們可以通過創建IAuthProvider接口的模仿實現,並檢查控制器LogOn方法結果的類型和性質來執行這些測試,像這樣:

[TestMethod]
public void Can_Login_With_Valid_Credentials() {
    // Arrange - create a mock authentication provider
    // 布置 — 創建模仿認證提供器
    Mock<IAuthProvider> mock = new Mock<IAuthProvider>();
    mock.Setup(m => m.Authenticate("admin", "secret")).Returns(true); 
// Arrange - create the view model // 布置 — 創建視圖模型 LogOnViewModel model = new LogOnViewModel { UserName = "admin", Password = "secret" };
// Arrange - create the controller // 布置 — 創建控制器 AccountController target = new AccountController(mock.Object);
// Act - authenticate using valid credentials // 動作 — 用有效的憑據進行認證 ActionResult result = target.LogOn(model, "/MyURL");
// Assert // 斷言 Assert.IsInstanceOfType(result, typeof(RedirectResult)); Assert.AreEqual("/MyURL", ((RedirectResult)result).Url); }
[TestMethod] public void Cannot_Login_With_Invalid_Credentials() { // Arrange - create a mock authentication provider // 布置 — 創建模仿認證提供器 Mock<IAuthProvider> mock = new Mock<IAuthProvider>(); mock.Setup(m => m.Authenticate("badUser", "badPass")).Returns(false);
// Arrange - create the view model // 布置 — 創建視圖模型 LogOnViewModel model = new LogOnViewModel { UserName = "badUser", Password = "badPass" };
// Arrange - create the controller // 布置 — 創建控制器 AccountController target = new AccountController(mock.Object);
// Act - authenticate using valid credentials // 動作 — 用有效憑據認證 ActionResult result = target.LogOn(model, "/MyURL");
// Assert // 斷言 Assert.IsInstanceOfType(result, typeof(ViewResult)); Assert.IsFalse(((ViewResult)result).ViewData.ModelState.IsValid); }

This takes care of protecting the SportsStore administration functions. Users will be allowed to access these features only after they have supplied valid credentials and received a cookie, which will be attached to subsequent requests. We’ll come back to authentication in Chapters 13 and 22.
這起到了保護SprotsStore管理功能的作用。只當用戶提供了有效的憑據並接收一個cookie之后,才允許用戶訪問這些功能。客戶端所接收的cookie將被附加到后繼的請求中。我們將在第13和22章返回到認證上來。

■ Tip It is best to use Secure Sockets Layer (SSL) for applications that require authentication so that the credentials and the authentication cookie (which is used to subsequently identify the user, as we’ll describe in Chapter 22) are transmitted over a secure connection. Setting this up is worth doing. See the IIS documentation for details.
提示:對需要認證的應用程序最好使用安全套接字層(SSL),以使得憑據和認證cookie(用於后繼地標識該用戶,正如我們將在第22章描述的那樣)通過一個安全連接進行傳輸。建立SSL是有價值的事情。詳細請參閱IIS文檔。

Image Uploads
圖像上載

We are going to complete the SportsStore application with something a little more sophisticated, We will add the ability for the administrator to upload product images and store them in the database so that they are displayed in the product catalog.
我們打算用一些更具技巧的東西來完成這個SportsStore應用程序,我們將為管理員添加上載產品圖像,並把它們存儲到數據庫中去的能力,以使這些圖像能夠顯示在產品分類中。

Extending the Database
擴展數據庫

Open the Visual Studio Server Explorer window and navigate to the Products table in the database we created in Chapter 7. Right-click the table and select Open Table Definition from the pop-up menu. Add the two new columns that are shown in Figure 9-19.
打開Visual Studio的服務器資源管理器窗口,並導航到我們在第7章創建的數據庫中的Products表。右擊此表並從彈出菜單選擇“打開表定義”。添加如圖9-19所示的兩個新列。

圖9-19

Figure 9-19. Adding new columns to the Products table
圖9-19. 把新列加到Products表

Select Save Products from the File menu (or press Control+S) to save the changes to the table.
從文件菜單選擇“保存Products”(或按Ctrl + S)來保存對此表的修改。

Enhancing the Domain Model
增強域模型

We need to add two new fields to the Products class in the SportsStore.Domain project that correspond to the columns we added to the database. The additions are shown in bold in Listing 9-31.
我們需要把兩個新字段加到SportsStore.Domain項目的Products類,這兩個字段對應於我們添加到數據庫的列。所添加的內容在清單9-31中以黑體顯示。

Listing 9-31. Adding Properties to the Product Class
清單9-31. 在Product類上添加屬性

using System.ComponentModel.DataAnnotations;
using System.Web.Mvc; 
namespace SportsStore.Domain.Entities {
public class Product { [HiddenInput(DisplayValue=false)] public int ProductID { get; set; }
[Required(ErrorMessage = "Please enter a product name")] public string Name { get; set; }
[Required(ErrorMessage = "Please enter a description")] [DataType(DataType.MultilineText)] public string Description { get; set; }
[Required] [Range(0.01, double.MaxValue, ErrorMessage = "Please enter a positive price")] public decimal Price { get; set; }
[Required(ErrorMessage = "Please specify a category")] public string Category { get; set; }
public byte[] ImageData { get; set; } // 原文這里有錯 — 譯者注 [HiddenInput(DisplayValue = false)] public string ImageMimeType { get; set; } } }

We don’t want either of these new properties to be visible when the MVC Framework renders an editor for us. To that end, we use the HiddenInput attribute on the ImageMimeType property. We don’t need to do anything with the ImageData property, because the framework doesn’t render an editor for byte arrays. It does this only for “simple” types, such as int, string, DateTime, and so on.
在MVC框架為我們渲染編輯器時,我們不希望這兩個新屬性是可見的。於是,我們在ImageMimeType屬性上使用了HiddenInput注解屬性。我們不需要對ImageData屬性做任何事情,因為框架不會為一個字節數組渲染一個編輯器。這一規則只對“簡單”類型起作用,如int、string、DateTime等等。

■ Caution Make sure that the names of the properties that you add to the Product class exactly match the names you gave to the new columns in the database.
小心:要確保你添加到Product類的屬性名與你在數據庫中所給定的新列嚴格匹配。

Updating the Entity Framework Conceptual Model
更新實體框架概念模型

■注:本小節是多余的,在SportsStore應用程序中不需要做這部分工作。而且,如果做了,會出現錯誤 — 譯者注

We have created the new columns in the database and the corresponding properties in the Product class. Now we must update the Entity Framework conceptual model so that the two are mapped together properly. This is a quick- and-easy process. Open the SportsStore.edmx file in the Concrete/ORM folder of the SportsStore.Domain project. You will see the current conceptual representation of the Product class as it is known by the Entity Framework, shown in the left panel of Figure 9-20.
我們已經創建了數據庫中的新列和Product類中相應的屬性。現在,我們必須更新實體框架的概念模型,以使這兩者被適當地一起映射。這是一個快而容易的過程。打開SportsStore.Domain項目的Concrete/ORM文件夾中的SportsStore.edmx文件。你將看到Product類的當前的概念表示,因為實體框架是知道這個Product的,如圖9-20左邊的面板所示。

圖9-20

Figure 9-20. Updating the conceptual model
圖9-20. 更新概念模型

Right-click in the space that surrounds the Product object and select Update Model from Database from the pop-up menu. The Update Wizard dialog box appears and begins to query the database. Without making any changes, click the Finish button. This causes the Entity Framework to refresh its understanding of the parts of the database it is already aware of. After a moment, you will see that the ImageData and ImageMimeType properties have been added to the conceptual Product, as shown in the right panel of Figure 9-20.
右擊Product對象周圍的空白處,並從彈出菜單選擇“Update Model from Database(從數據庫更新模型)”。會出現更新向導對話框,並開始查詢數據庫。不用進行任何修改,點擊“Finish(完成)”按鈕。這會導致實體框刷新它已經感知的對數據庫部分的理解。一會兒之后,你將看到ImageData和ImageMimeType屬性已經被添加到Product概念模型,如圖9-29右側的面板所示。

Creating the Upload User Interface Elements
創建Upload用戶接口元素

Our next step is to add support for handling file uploads. This involves creating a UI that the administrator can use to upload an image. Modify the Views/Admin/Edit.cshtml view so that it matches Listing 9-32 (the additions are in bold).
我們的下一步是添加對處理文件上載的支持。這包括創建一個管理員可以用來上載圖像的UI。修改Views/Admin/Edit.cshtml視圖,以使它與清單9-32匹配(黑體部分)。

Listing 9-32. Adding Support for Images
清單9-32. 添加對圖像的支持

@model SportsStore.Domain.Entities.Product
@{ ViewBag.Title = "Admin: Edit " + @Model.Name; Layout = "~/Views/Shared/_AdminLayout.cshtml"; }
<h1>Edit @Model.Name</h1>
@using (Html.BeginForm("Edit", "Admin", FormMethod.Post, new { enctype = "multipart/form-data" })) {
@Html.EditorForModel()
<div class="editor-label">Image</div> <div class="editor-field"> @if (Model.ImageData == null) { @:None } else { <img width="150" height="150" src="@Url.Action("GetImage", "Product", new { Model.ProductID })" /> } <div>Upload new image: <input type="file" name="Image" /></div> </div>
<input type="submit" value="Save" /> @Html.ActionLink("Cancel and return to List", "Index") }

You may not be aware that web browsers will upload files properly only when the HTML form element defines an enctype value of multipart/form-data. In other words, for a successful upload, the form element must look like this:
你也許還不知道,只有當HTML的form元素定義了一個值為multipart/form-data的enctype時,Web瀏覽器才會適當地上傳文件。換句話說,要進行成功的上傳,form元素必須看上去像這樣:

<form action="/Admin/Edit" enctype="multipart/form-data" method="post">
...
</form>

Without the enctype attribute, the browser will transmit only the name of the file and not its content, which is no use to us at all. To ensure that the enctype attribute appears, we must use an overload of the Html.BeginForm helper method that lets us specify HTML attributes, like this:
沒有這個enctype屬性,瀏覽器將只傳遞文件名,而不是它的內容,這對我們根本沒用。為了確保enctype屬性出現,我們必須使用Html.BeginForm輔助器的一個重載方法,使我們能夠指定HTML屬性,像這樣:

@using (Html.BeginForm("Edit", "Admin",
    FormMethod.Post, new { enctype = "multipart/form-data" })) {

Also notice that if the Product being displayed has a non-null ImageData property value, we add an img element and set its source to be the result of calling the GetImage action method of the Product controller. We’ll implement this shortly.
還要注意到,如果被顯示的Product有一個非空的ImageData屬性值,我們添加了一個img元素,並把它的源設置為調用Product控制器的GetImage動作方法的結果。我們很快就會實現它。

Saving Images to the Database
將圖像保存到數據庫

We need to enhance the POST version of the Edit action method in the AdminController class so that we take the image data that has been uploaded to us and save it in the database. Listing 9-33 shows the changes that are required.
我們需要增強AdminController類中POST版本的Edit動作方法,以取得上傳給我們的圖像數據,並把它保存到數據庫中。清單9-33顯示了所需要的修改。

Listing 9-33. Handling Image Data in the AdminController Class
清單9-33. 在AdminController類中處理圖像數據

[HttpPost]
public ActionResult Edit(Product product, HttpPostedFileBase image) {
    if (ModelState.IsValid) {
if (image != null) { product.ImageMimeType = image.ContentType; product.ImageData = new byte[image.ContentLength]; image.InputStream.Read(product.ImageData, 0, image.ContentLength); }
// save the product // 保存產品 repository.SaveProduct(product);
// add a message to the viewbag // 將消息添加到viewbag TempData["message"] = string.Format("{0} has been saved", product.Name);
// return the user to the list // 將用戶返回到列表頁面 return RedirectToAction("Index"); } else { // there is something wrong with the data values // 存在數據錯誤 return View(product); } }

We have added a new parameter to the Edit method, which the MVC Framework uses to pass the uploaded file data to us. We check to see if the parameter value is null; if it is not, we copy the data and the MIME type from the parameter to the Product object so that it is saved to the database.
我們對Edit方法添加了一個新參數,MVC框架把它用於傳遞上載文件的數據。我們查看該參數的值是否為空;若非空,便把這些數據和該參數的MIME類型拷貝到Product對象,以便把它們保存到數據庫。

■ Note You’ll need to update your unit tests to reflect the new parameter in Listing 9-33. Providing a null parameter value will satisfy the compiler.
注:你將需要更新你的單元測試,以反映出清單9-33中的新參數。提供一個null參數值便會使編譯器得到滿足。

Implementing the GetImage Action Method
實現GetImage動作方法

In Listing 9-32, we added an img element whose content was obtained through a GetImage action method. We are going to implement this so that we can display images contained in the database. Listing 9-34 shows the method we added to the ProductController class.
在清單9-32中,我們添加了一個img元素,它的內容是通過GetImage動作方法獲得的。我們打算實現它,以使我們能夠顯示包含在數據庫中的圖像。清單9-34顯示了我們添加到ProductController類中的這個方法。

Listing 9-34. The GetImage Action Method
清單9-34. GetImage動作方法

public FileContentResult GetImage(int productId) {
    Product prod = repository.Products.FirstOrDefault(p => p.ProductID == productId);
    if (prod != null) {
        return File(prod.ImageData, prod.ImageMimeType);
    } else {
        return null;
    }
}

This method tries to find a product that matches the ID specified by the parameter. The FileContentResult class is returned from an action method when we want to return a file to the client browser, and instances are created using the File method of the base controller class. We’ll discuss the different types of results you can return from action methods in Chapter 12.
此方法試圖找到一個與參數指定的ID匹配的產品。當我們想把一個文件返回給客戶端瀏覽器時,FileContentResult是從一個動作方法返回的,而實例是用controller基類的File方法創建的。我們將在第12章討論你可以從動作方法返回的不同結果類型。

UNIT TEST: RETRIEVING IMAGES
單元測試:接收圖像

We want to make sure that the GetImage method returns the correct MIME type from the repository and make sure that no data is returned when we request a product ID that doesn’t exist. Here are the test methods we created:
我們希望確保GetImage方法從存儲庫中返回了正確的MIME類型,並確保在請求一個不存在的產品ID時,沒有返回數據。以下是我們創建的測試方法:

[TestMethod]
public void Can_Retrieve_Image_Data() {
// Arrange - create a Product with image data // 布置 — 創建一個帶有圖像的產品 Product prod = new Product { ProductID = 2, Name = "Test", ImageData = new byte[] {}, ImageMimeType = "image/png" };
// Arrange - create the mock repository // 布置 — 創建模仿存儲庫 Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m => m.Products).Returns(new Product[] { new Product {ProductID = 1, Name = "P1"}, prod, new Product {ProductID = 3, Name = "P3"} }.AsQueryable());
// Arrange - create the controller // 布置 — 創建控制器 ProductController target = new ProductController(mock.Object);
// Act - call the GetImage action method // 動作 — 調用GetImage動作方法 ActionResult result = target.GetImage(2);
// Assert // 斷言 Assert.IsNotNull(result); Assert.IsInstanceOfType(result, typeof(FileResult)); Assert.AreEqual(prod.ImageMimeType, ((FileResult)result).ContentType); }
[TestMethod] public void Cannot_Retrieve_Image_Data_For_Invalid_ID() {
// Arrange - create the mock repository // 布置 — 創建模仿存儲庫 Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m => m.Products).Returns(new Product[] { new Product {ProductID = 1, Name = "P1"}, new Product {ProductID = 2, Name = "P2"} }.AsQueryable());
// Arrange - create the controller // 布置 — 創建控制器 ProductController target = new ProductController(mock.Object);
// Act - call the GetImage action method // 動作 — 調用GetImage動作方法 ActionResult result = target.GetImage(100);
// Assert // 斷言 Assert.IsNull(result); }

When dealing with a valid product ID, we check that we get a FileResult result from the action method and that the content type matches the type in our mock data. The FileResult class doesn’t let us access the binary contents of the file, so we must be satisfied with a less-than-perfect test. When we request an invalid product ID, we simply check to ensure that the result is null.
當處理一個有效的產品ID時,我們會檢查,從該動作方法得到了一個FileResult結果,並且該內容的類型與模仿數據的類型相匹配。FileResult類不讓我們訪問二進制的文件內容,因此我們必須對這個不太完美的測試感到滿意。當請求一個非法的產品ID時,我們簡單地進行檢查,以確認其結果為空。

The administrator can now upload images for products. You can try this yourself by editing one of the products. Figure 9-21 shows an example.
管理員現在可以上載產品的圖像了。你可以通過編輯一個產品自己試一下。圖9-21顯示了一個例子。

圖9-21

Figure 9-21. Adding an image to a product listing
圖9-21. 把一個圖像添加到一個產品列表

Displaying Product Images
顯示產品圖像

All that remains is to display the images alongside the product description in the product catalog. Edit the Views/Shared/ProductSummary.cshtml view to reflect the changes shown in bold in Listing 9-35.
所剩下的工作是在產品分類的產品描述旁邊顯示這個圖像。編輯Views/Shared/ProductSummary.cshtml視圖,以反映出清單9-35的黑體所顯示的修改。

Listing 9-35. Displaying Images in the Product Catalog
清單9-35. 在產品分類中顯示圖像

@model SportsStore.Domain.Entities.Product
<div class="item"> 
@if (Model.ImageData != null) { <div style="float:left;margin-right:20px"> <img width="75" height="75" src="@Url.Action("GetImage", "Product", new { Model.ProductID })" /> </div> }
<h3>@Model.Name</h3> @Model.Description
<div class="item">
@using(Html.BeginForm("AddToCart", "Cart")) { @Html.HiddenFor(x => x.ProductID) @Html.Hidden("returnUrl", Request.Url.PathAndQuery) <input type="submit" value="+ Add to cart" /> }
</div> <h4>@Model.Price.ToString("c")</h4> </div>

With these changes in place, the customers will see images displayed as part of the product description when they browse the catalog, as shown in Figure 9-22.
通過這些適當的修改,當客戶瀏覽分類時,他們將看到圖像,它是作為產品描述的一部分顯示的,如圖9-22所示。

圖9-22

Figure 9-22. Displaying product images
圖9-22. 顯示產品圖像

Summary
小結

In this and the previous two chapters, we have demonstrated how the ASP.NET MVC Framework can be used to create a realistic e-commerce application. This extended example has introduced many of the framework’s key features: controllers, action methods, routing, views, model binding, metadata, validation, layouts, authentication, and more. You have also seen how some of the key technologies related to MVC can be used. These included the Entity Framework, Ninject, Moq, and the Visual Studio support for unit testing.
在本章以及前面兩章中,我們已經演示了,可以如何運用ASP.NET MVC框架來創建真實的電子商務應用程序。這個擴展示例已經介紹了框架的許多關鍵特性:控制器、動作方法、路由、視圖、模型綁定、元數據、驗證、布局、認證等等。你也已經看到了如何使用與MVC相關的一些關鍵技術。這些包括實體框架、Ninject、Moq、以及Visual Studio對單元測試的支持。

We have ended up with an application that has a clean, component-oriented architecture that separates out the various concerns, leaving us with a code base that will be easy to extend and maintain. The second part of this book digs deep into each MVC Framework component to give you a complete guide to its capabilities.
我們最終實現了一個應用程序,它具有整潔的、實現了關注分離的面向組件的體系結構,給我們留下了易於擴展和維護的代碼基礎(意即,可以在現有代碼的基礎上,進一步開發此應用程序,或用它開發其它應用程序 — 譯者注)。本書的第二部分將深入到MVC框架每個組件的內部,以對它的能力給出完整指南。


免責聲明!

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



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