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


添加Navigation控件

上篇我們已經對UI部分做了整理,但是我們網站看起來仍然很奇怪,因為用戶無法選擇他們想看的商品類別,必須要一頁一頁的瀏覽,直到找到自己想要買的東西。我經常在網上瀏覽一些技術站點,並添加他們到我的收藏夾,但收藏夾里的條目太多了,還是不能方便的找到自己想看的網址,偶然發現了一個網站,叫做開發者導航(http://www.devseek.net),它收錄了我所需要的所有網址,這正是我想要的,於是我今天也用這個導航的字眼,來為我們的網站添加一個分類過濾的功能。我們今天的內容主要有三個部分:

1.增強ProductController類的List action功能,使它能夠分類商品。

2.修改並加強URL scheme 和我們的rerouting策略。

3.在邊條上創建分類列表,並高亮當前的分類和連接。

過濾產品列表

為了渲染我們的邊條,我們需要和我們ProductsListViewModel類溝通,過濾出產品分類的列表,現在就打開這個文件,讓我們為它做個Enhancement。

using System.Collections.Generic;
using SportsStore.Domain.Entities;

namespace SportsStore.WebUI.Models {

    public class ProductsListViewModel {

        public IEnumerable<Product> Products { get; set; }
        public PagingInfo PagingInfo { get; set; }
        public string CurrentCategory { get; set; }
    }
}

我們為這個類添加了一個當前分類的屬性,用來顯示當前用戶選擇的分類。我們要更新我們ProductController,使它能夠使用這個屬性:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Models;

namespace SportsStore.WebUI.Controllers
{
    public class ProductController : Controller
    {
        private IProductsRepository repository;
        public int PageSize = 4;

        public ProductController(IProductsRepository productRepository)
        {
            this.repository = productRepository;
        }

        public ViewResult List(string category, int page = 1)
        {

            ProductsListViewModel model = new ProductsListViewModel
            {
                Products = repository.Products
                                    .Where(p => category == null || p.Category == category)
                                    .OrderBy(p => p.ProductID)
                                    .Skip((page - 1) * PageSize)
                                    .Take(PageSize),
                                    PagingInfo = new PagingInfo
                                    {
                                        CurrentPage = page,
                                        ItemsPerPage = PageSize,
                                        TotalItems = repository.Products.Count()
                                    },
                                    CurrentCategory = category
            };
            return View(model);
        }

    }
}

上面的代碼我們做了3個改變。第一,我們添加了一個新的參數叫做category. 這個參數通過我們的第二個變化被使用,這第二個變化就是我改進了Linq查詢,如果category參數不是null,只有匹配這個分類的產品才能被選擇。這最后一個改變就是設置CurrentCategory 屬性的值,然而,我們這3點改變,就意味着PagingInfo.TotalItems的值是不正確的,我們必須解決這個問題。

更新現有的測試方法

我們改變了List的參數列表,這使得我們必須更新我們現有的測試方法,為了保證我們的測試方法都可用,我們要為他們添加一個null值,作為第一個參數傳遞,找到Can_Paginate方法,將List(2).Model改成List(null, 2).Model。運行你的應用,能看到如下畫面:

image

這和我們上篇最后的結果是一樣的,現在你在地址欄中添加如下參數:?category=Soccer ,是你的地址欄看上去像這樣http://localhost:47072/?category=Soccer 你會看到這樣的畫面:

image

我們的測試文件現在需要添加一個功能,使它能夠正確的過濾一個分類並接收一個指定分類的產品:

[TestMethod]
        public void Can_Filter_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", Category = "Cat1"},
                new Product {ProductID = 2, Name = "P2", Category = "Cat2"},
                new Product {ProductID = 3, Name = "P3", Category = "Cat1"},
                new Product {ProductID = 4, Name = "P4", Category = "Cat2"},
                new Product {ProductID = 5, Name = "P5", Category = "Cat3"}
                }.AsQueryable());
                // Arrange - create a controller and make the page size 3 items
                ProductController controller = new ProductController(mock.Object);
                controller.PageSize = 3;
                // Action
                Product[] result = ((ProductsListViewModel)controller.List("Cat2", 1).Model)
                .Products.ToArray();
                // Assert
                Assert.AreEqual(result.Length, 2);
                Assert.IsTrue(result[0].Name == "P2" && result[0].Category == "Cat2");
                Assert.IsTrue(result[1].Name == "P4" && result[1].Category == "Cat2");
        }

 

 

這個測試創建了一個mock repository,它包含了類別中的一個Product對象,一個被指定使用在Action方法中的分類,並且結果被check,確保在右側的產品對象都是正確的。

改善URL Scheme

我們的URL地址看上去太丑了,也不專業,現在我們必須花點時間去改善一下App_Start/RouteConfig.cs文件:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace SportsStore.WebUI
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(null,
            "",
            new {
            controller = "Product", action = "List",
            category = (string)null, page = 1
            } );

            routes.MapRoute(null,
            "Page{page}",
            new { controller = "Product", action = "List", category = (string)null },
            new { page = @"\d+" }
            );

            routes.MapRoute(null,
            "{category}",
            new { controller = "Product", action = "List", page = 1 }
            );

            routes.MapRoute(null,
            "{category}/Page{page}",
            new { controller = "Product", action = "List" },
            new { page = @"\d+" }
            );

            routes.MapRoute(null, "{controller}/{action}");

        }
    }
}

 

URL

導航到

/

列出說有產品的第一頁列表

/Page2

列出所有產品的指定頁

/Soccer

列出指定類別的產品的第一頁

/Soccer/Page2

列出指定類別的產品的指定頁

/Anything/Else

調用Anything 控制器的Else方法

這是我們URL Scheme的具體含義。MVC使用ASP.NET routing 系統去處理從用戶端發來的請求,同時,它也向外發出URL scheme,這就是我們能夠嵌入到網頁中的地址,我們要做的就是這些應用中的地址都是被組裝起來的。

現在我們就去添加一些對分類過濾的支持:

@model SportsStore.WebUI.Models.ProductsListViewModel
@{
    ViewBag.Title = "Products";
}
@foreach (var p in Model.Products)
{
    Html.RenderPartial("ProductSummary", p);
}
<div class="pager">
        @Html.PageLinks(Model.PagingInfo, x => Url.Action("List",
               new {page = x, category = Model.CurrentCategory}))
</div>

 

構建一個分類導航菜單

我們需要提供給用戶一種途徑,使用戶能夠選擇某種分類,這就需要我們必須提供分類信息給用戶,讓他們去選擇,而這個分類的信息必須要在多個控制器中運用,這就要求它必須是自包含的並且可重用的。在ASP.NET MVC框架中有個child actions的概念, 大家都喜歡用它來創建諸如可重用的導航控件之類的東西。一個child action 依賴與HTML helper方法,這個方法被稱為RenderAction,它讓我們從當前view的任意action方法中包含輸出,現在我們創建一個新的Controller(我們稱它為NavController) 和一個action方法 (菜單) ,並且渲染一個導航菜單,然后從這個方法中注入output到layout中。這個方法給了我們一個真正的控制器,無論我們的應用邏輯是什么都可以使用它,並且我們都能像其他控制器那用去測試它。

創建Navigation控制器

右擊WebUI中的Controllers文件夾,創建一個名為NavController的控制器,選擇空的MVC模板,刪除自動生成的index方法,添加代碼如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace SportsStore.WebUI.Controllers
{
    public class NavController : Controller
    {
        //
        // GET: /Nav/

        public string Menu()
        {
            return "Hello from NavController";
        }

    }
}

 

這個方法返回一個消息字符串,但這對於我們整合一個child action到這個應用的其他部分已經足夠用了。我們希望這個分類列表展現在所有頁面上,所以我們將在layout中渲染這個child action,而不是在一個指定的View中。 現在我們編輯Views/Shared/_Layout.cshtml 文件,讓它調用RenderAction helper方法。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
<link href="~/Content/Site.css" type="text/css" rel="stylesheet" />
</head>
<body>
    <div id="header">
        <div class="title">SPORTS STORE</div>
    </div>
    <div id="categories">
       @{ Html.RenderAction("Menu", "Nav"); }
    </div>
    <div id="content">
        @RenderBody()
    </div>
</body>
</html>

運行應用,你將看到我們的邊條上已經出現了這條消息字符串:

image

產生分類列表

現在,我們就在Menu action方法中創建分類列表:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using SportsStore.Domain.Abstract;

namespace SportsStore.WebUI.Controllers
{
    public class NavController : Controller
    {
        //
        // GET: /Nav/

        private IProductsRepository repository;
        public NavController(IProductsRepository repo)
        {
            repository = repo;
        }
        public PartialViewResult Menu()
        {
            IEnumerable<string> categories = repository.Products
            .Select(x => x.Category)
            .Distinct()
            .OrderBy(x => x);
            return PartialView(categories);
        }

    }
}

我們的控制器現在接受一個IProductsRepository的實現,這個實現是通過Ninject提供的,還有一個變化,就是我們使用Linq從repository中獲得分類信息,請注意,我們調用了一個PartialView的方法,返回了一個PartialViewResult對象。現在讓我們去更新一下我們的測試文件吧!添加如下代碼到你的測試文件:

        [TestMethod]
        public void Can_Create_Categories() {
                // 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", Category = "Apples"},
                new Product {ProductID = 2, Name = "P2", Category = "Apples"},
                new Product {ProductID = 3, Name = "P3", Category = "Plums"},
                new Product {ProductID = 4, Name = "P4", Category = "Oranges"},
                }.AsQueryable());
                // Arrange - create the controller
                NavController target = new NavController(mock.Object);
                // Act = get the set of categories
                string[] results = ((IEnumerable<string>)target.Menu().Model).ToArray();
                // Assert
                Assert.AreEqual(results.Length, 3);
                Assert.AreEqual(results[0], "Apples");
                Assert.AreEqual(results[1], "Oranges");
                Assert.AreEqual(results[2], "Plums");
        }

現在讓我們去創建這個PartialView吧!

創建PartialView

在NavController中,右擊Menu方法,選擇添加View,並輸入IEnumerable<string>在模型類輸入框中。

image

 

 

 

修改Menu.cshtml文件如下:

@model IEnumerable<string>
@Html.ActionLink("Home", "List", "Product")

    @foreach (var link in Model) {
            @Html.RouteLink(link, new {
            controller = "Product",
            action = "List",
            category = link,
            page = 1
    })
}

 

在Site.css文件中添加如下代碼:

DIV#categories A
{
font: bold 1.1em "Arial Narrow","Franklin Gothic Medium",Arial; display: block;
text-decoration: none; padding: .6em; color: Black;
border-bottom: 1px solid silver;
}
DIV#categories A.selected { background-color: #666; color: White; }
DIV#categories A:hover { background-color: #CCC; }
DIV#categories A.selected:hover { background-color: #666; }

 

運行你的應用,能應該能看到如下畫面:

image

 

我們還要需要進一步完善,因為我們現在還不能讓用戶清楚的看出當前選擇了那種分類,我們要高亮當前選中的分類,這樣看起來才更加友好、實用。

修改我們的NavController中的Menu方法如下:

public PartialViewResult Menu(string category = null)
        {
            ViewBag.SelectedCategory = category;

            IEnumerable<string> categories = repository.Products
            .Select(x => x.Category)
            .Distinct()
            .OrderBy(x => x);
            return PartialView(categories);
        }

為我們的測試文件添加一個選中的測試方法:

        [TestMethod]
        public void Indicates_Selected_Category()
        {
            // 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", Category = "Apples"},
                          new Product {ProductID = 4, Name = "P2", Category = "Oranges"},
                         }.AsQueryable());
            // Arrange - create the controller
            NavController target = new NavController(mock.Object);
            // Arrange - define the category to selected
            string categoryToSelect = "Apples";
            // Action
            string result = target.Menu(categoryToSelect).ViewBag.SelectedCategory;
            // Assert
            Assert.AreEqual(categoryToSelect, result);
        }

更新Menu.cshtml如下:

@model IEnumerable<string>
@Html.ActionLink("Home", "List", "Product")

    @foreach (var link in Model) {
            @Html.RouteLink(link, new {
            controller = "Product",
            action = "List",
            category = link,
            page = 1
    },
    new {
    @class = link == ViewBag.SelectedCategory ? "selected" : null
    })
}

 

運行一下看看結果吧!

image

糾正頁碼

從上圖中我們很輕易就能看出,我們的頁碼是錯的,我們只有兩個產品,卻顯示了有3頁,我們必須糾正這個錯誤!打開ProductController,找到List方法,修改如下:

public ViewResult List(string category, int page = 1)
        {

            ProductsListViewModel model = new ProductsListViewModel
            {
                Products = repository.Products
                           .Where(p => category == null || p.Category == category)
                           .OrderBy(p => p.ProductID)
                           .Skip((page - 1) * PageSize)
                           .Take(PageSize),
                           PagingInfo = new PagingInfo
                           {
                               CurrentPage = page,
                               ItemsPerPage = PageSize,
                               TotalItems = category == null ?
                                      repository.Products.Count() :
                           repository.Products.Where(e => e.Category == category).Count()
                                    },
                                    CurrentCategory = category
            };
            return View(model);
        }

添加測試方法到測試文件:

        [TestMethod]
        public void Generate_Category_Specific_Product_Count() {
                // 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", Category = "Cat1"},
                new Product {ProductID = 2, Name = "P2", Category = "Cat2"},
                new Product {ProductID = 3, Name = "P3", Category = "Cat1"},
                new Product {ProductID = 4, Name = "P4", Category = "Cat2"},
                new Product {ProductID = 5, Name = "P5", Category = "Cat3"}
                }.AsQueryable());
                // Arrange - create a controller and make the page size 3 items
                ProductController target = new ProductController(mock.Object);
                target.PageSize = 3;
                // Action - test the product counts for different categories
                int res1 = ((ProductsListViewModel)target
                .List("Cat1").Model).PagingInfo.TotalItems;
                int res2 = ((ProductsListViewModel)target
                .List("Cat2").Model).PagingInfo.TotalItems;
                int res3 = ((ProductsListViewModel)target
                .List("Cat3").Model).PagingInfo.TotalItems;
                int resAll = ((ProductsListViewModel)target
                .List(null).Model).PagingInfo.TotalItems;
                // Assert
                Assert.AreEqual(res1, 2);
                Assert.AreEqual(res2, 2);
                Assert.AreEqual(res3, 1);
                Assert.AreEqual(resAll, 5);
        }

運行一下,現在看下我們成果吧!

image

好了,今天就到這里里吧!內容實在是有點多,但都是必須的,而且實用的技術,下一篇中,我們將為我們的應用添加一個購物車,這是電子商務網站上必須的功能,不然怎么賣商品呢?如果您覺得我的文章實用,對你有所幫助,請推薦它給你的朋友,請繼續關注我的續篇!


免責聲明!

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



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