使用MVC4,Ninject,EF,Moq,構建一個真實的應用電子商務SportsStore(十)


我們現在還需要為管理員提供一個途徑,使他能方便的管理網站的商品目錄,這也是所有網站都需要的功能,常用到了幾乎所有開發人員都要開發這種功能的地步,為了簡化這種重復開發、又沒有技術含量的工作,VS的設計和開發者們也試圖通過MVC框架來自動生成這些功能,幫助開發人員,這也是我們開發這個管理后台的主要目的---學習如何通過MVC生成一個具有CRUD功能的管理模塊。

創建CRUD Controller

我們將會創建一個新的controller去處理administration功能.右擊SportsStore.WebUI工程的Controllers文件夾並選擇添加Controller. 命名為AdminController,並確保模板下拉框中選擇的是Empty MVC Controller:

image

修改代碼如下:

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

我們所關心的是Index方法能否正確返回Product對象,所以,我們不能偷懶,還是創建一個mock repository 的實現,通過action 方法返回數據,做一個比較。現在我們就添加一個新的單元測試類,命名為AdminTests.cs:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Controllers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;

namespace SportsStore.UnitTests
{
    [TestClass]
    public class AdminTests
    {
        [TestMethod]
        public void Index_Contains_All_Products()
        {
            // Arrange - create the mock repository
            Mock<IProductsRepository> mock = new Mock<IProductsRepository>();
            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);
        }
    }
}

 

 

創建一個新的Layout

右擊SportsStore.WebUI工程的Views/Shared並選擇添加新建項. 選擇MVC 4 布局頁模板並設置名字為_AdminLayout.cshtml:

image

我們這個命名使用了一個下划線開頭,這是因為微軟還有另外一種技術,叫做WebMatrix,這種技術也使用Razor語法,它通過帶下划線的命名來保持一個頁面被駐留在瀏覽器中,在MVC4中不存在這種需求,當把一個啟動layout命名為帶下划線的這種命名規則,卻一直被保存着,在任何地方都適用。

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <link href="~/Content/Admin.css" rel="stylesheet" type="text/css" />
    <title></title>
</head>
<body>
<div>
    @RenderBody()
</div>
</body>
</html>

我們還用為這個View添加一個Admin.css樣式單,右擊Content文件夾,添加一個樣式單:

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; }

 

我們已經完成了這個layout,現在要為這個Admin控制器添加一個Index視圖了:

image

這里一定要仔細看清楚上面的圖片哦!我們選擇了List scaffold(支架模板),這很重要.點擊添加按鈕,生成了一個帶有代碼的新視圖,仔細看看這些代碼,你可以將它漢化一下,為了不引起大家誤解,我不去漢化它,就讓它保持原樣吧!在地址欄中輸入/Admin/index,你會看到下圖:

image

現在,你該明白scaffold為我們做了什么了吧!好了,現在你已經看到了效果了,讓我們再去修善一下Index.cshtml文件吧!

@model IEnumerable<SportsStore.Domain.Entities.Product>
@{
    ViewBag.Title = "Admin: 全部商品";
    Layout = "~/Views/Shared/_AdminLayout.cshtml";
}
<h1>全部商品</h1>
<table class="Grid">
<tr>
<th>ID</th>
<th>名稱</th>
<th class="NumericCol">價格</th>
<th>操作</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="刪除"/>
    }
</td>
</tr>
}
</table>
<p>@Html.ActionLink("添加新產品", "Create")</p>
image 

編輯Products

要做這項工作很簡單,和我們之前的操作非常相似,我們要做的無非是兩件事:

1.顯示一個允許管理員修改價格和屬性的頁面。

2.添加一個Action方法,當數據被提交后,這個方法能處理修改后的數據。

現在,我們就去為AdminController添加一個Edit方法,記住,你要添加下面的代碼到你的AdminController類:

using SportsStore.Domain.Entities;

 

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

添加測試方法到你的AdminTest類中:

        [TestMethod]
        public void Can_Edit_Product() {
            // Arrange - create the mock repository
            Mock<IProductsRepository> mock = new Mock<IProductsRepository>();
            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<IProductsRepository> mock = new Mock<IProductsRepository>();
            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);
        }

 

創建Edit視圖

image

修改代碼如下:

@model SportsStore.Domain.Entities.Product
@{
    ViewBag.Title = "Admin: 編輯" + @Model.Name;
    Layout = "~/Views/Shared/_AdminLayout.cshtml";
    }

    <h1>Edit @Model.Name</h1>

@using (Html.BeginForm()) {
    @Html.EditorForModel()
    <input type="submit" value="保存" />
    @Html.ActionLink("取消並返回", "Index")
}

現在,你點擊一下商品名稱,我們能看到下圖的結果:

image

Wow,MVC真是太方便了,但是,我仔細看看才發現,我們的ProductID居然能夠編輯,這個是一個嚴重的Bug,我絕不允許它的存在,但是,這是什么原因導致的呢?仔細分析一下,原來我們的View中使用了

EditorForModel 方法, 這個方法的確非常的方便,但我們卻沒有得到一個非常理想的結果,我們不希望管理員能夠看到或編輯產品編號,而且這個產品簡介的字段也太短了,參閱MVC指導手冊后,我才發現,我們需要使用模塊數據元的特性去影響 Html.EditorForModel方法,不管怎么說,去試試看吧!打開SportsStore.Domain工程Entities文件夾中的Product類,添加兩個新的引用:

using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;

修改后的代碼如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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; }
    }
}

 

好,再運行一下!

image

哈哈,這才是我們想要的結果!

更新Product Repository

現在,我們需要一個新的功能,使我們能夠保存產品的更新。首先,我們要在IProductRepository接口中添加一個新的方法:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SportsStore.Domain.Entities;

namespace SportsStore.Domain.Abstract
{

    public interface IProductsRepository
    {
        IQueryable<Product> Products { get; }

        void SaveProduct(Product product);
    }
}

現在我們要把這個方法的實現添加的我們的EF實現類中,你還記得它放在哪嗎?去找找吧!

using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using System.Linq;
namespace SportsStore.Domain.Concrete
{
    public class EFProductRepository : IProductsRepository
    {
        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); } else
 { Product dbEntry = context.Products.Find(product.ProductID); if (dbEntry != null) { dbEntry.Name = product.Name; dbEntry.Description = product.Description; dbEntry.Price = product.Price; dbEntry.Category = product.Category; } } context.SaveChanges(); }
    }
}

當管理員點擊保存按鈕時,會產生一個Edit的Post請求,我們需要重載一個Edit方法,去處理這個請求,在你的AdminController中添加如下方法:

        [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);
            }
        }

添加如下測試方法到AdminTest類:

[TestMethod]
        public void Can_Save_Valid_Changes() {
            // Arrange - create mock repository
            Mock<IProductsRepository> mock = new Mock<IProductsRepository>();
            // 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<IProductsRepository> mock = new Mock<IProductsRepository>();
            // 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));
        }

 

顯示確認信息

現在我們要在_AdminLayout.cshtml文件中添加一條標簽,我們使用TempData模板處理一個消息,這樣,我們就能夠在任何使用了這個模板的頁面處理消息,而不用到處去寫Razor:

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <link href="~/Content/Admin.css" rel="stylesheet" type="text/css" />
    <title></title>
</head>
<body>
<div>
    @if (TempData["message"] != null) {
    <div class="Message">@TempData["message"]</div>
    }
    @RenderBody()
</div>
</body>
</html>

今天的內容實在是不少,暫時先寫到這里吧,活不是一天干完的,我相信,你花一天時間畫的畫,你用一年的時間都賣不出去,而你花一年的時間畫的畫,你卻能在一天之內賣出去!明天我們繼續為我們的管理后台添加數據驗證的功能和刪除的功能,這些雖然沒有太高的技術含量,但卻是我們經常需要用到的東西,在本系列教程的最后部分,我們將進入到本教程最精彩的部分---網站安全。我將詳盡的講解如何使用MVC授權和過濾等MVC的重量級技術,請繼續關注我們的項目,關注我的續篇!


免責聲明!

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



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