您的位置:首页 > 其它

使用MVC4,Ninject,EF,Moq,构建一个真实的应用电子商务SportsStore(六)

2013-06-05 17:23 706 查看
添加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。运行你的应用,能看到如下画面:





这和我们上篇最后的结果是一样的,现在你在地址栏中添加如下参数:?category=Soccer ,是你的地址栏看上去像这样http://localhost:47072/?category=Soccer 你会看到这样的画面:





我们的测试文件现在需要添加一个功能,使它能够正确的过滤一个分类并接收一个指定分类的产品:

[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>


运行应用,你将看到我们的边条上已经出现了这条消息字符串:





产生分类列表

现在,我们就在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>在模型类输入框中。





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


运行你的应用,能应该能看到如下画面:





我们还要需要进一步完善,因为我们现在还不能让用户清楚的看出当前选择了那种分类,我们要高亮当前选中的分类,这样看起来才更加友好、实用。

修改我们的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
})
}


运行一下看看结果吧!





纠正页码

从上图中我们很轻易就能看出,我们的页码是错的,我们只有两个产品,却显示了有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);
}


运行一下,现在看下我们成果吧!





好了,今天就到这里里吧!内容实在是有点多,但都是必须的,而且实用的技术,下一篇中,我们将为我们的应用添加一个购物车,这是电子商务网站上必须的功能,不然怎么卖商品呢?如果您觉得我的文章实用,对你有所帮助,请推荐它给你的朋友,请继续关注我的续篇!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐