王志岳

塔塔信息技术(中国)有限公司 | CoderBus

导航

使用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

好了,今天就到这里里吧!内容实在是有点多,但都是必须的,而且实用的技术,下一篇中,我们将为我们的应用添加一个购物车,这是电子商务网站上必须的功能,不然怎么卖商品呢?如果您觉得我的文章实用,对你有所帮助,请推荐它给你的朋友,请继续关注我的续篇!

posted on 2013-06-05 17:23  王志岳  阅读(2725)  评论(6编辑  收藏  举报

开发者导航