您的位置:首页 > 编程语言 > ASP

【Pro ASP.NET MVC 3 Framework】.学习笔记.6.SportsStore:导航

2013-08-29 17:40 886 查看
在之前的章节,偶们设置了核心的基础设施,现在我们将使用基础设计添加关键特性,你将会看到投资是如何回报的。我们能够很简单很容易地添加重要的面向客户的特性。沿途,你也会看到一些MVC框架提供的附加的特性。

1 添加导航控件

如果使用分类导航,需要做以下三个方面:

增强List action模型,让它能过滤repository中的Product对象

重访并增强URL方案,修改我们的重路由策略

创建sidebar风格的分类列表,高亮当前分类,并链接其它分类

1.1 过滤Product列表

偶们要增强视图模型类ProductViewModel。为了渲染sidebar,我们要传送当前分类给view。

publicclass ProductsListViewModel { public IEnumerable<Product> Products { get; set; } public PagingInfo PagingInfo { get; set; } publicstring CurrentCategory { get; set; } }

我们给视图模型新增了CurrentCategory属性,下一步是更新ProductController类,让List action方法会以分类过滤Product对象,并是我用我们新增的属性指示那个分类被选中。

public ViewResult List( string category,int? id) { int page = id.HasValue ? id.Value : 1; ProductsListViewModel viewModel =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, ItemPerpage=pageSize, TotalItems=repository.Products.Count() }, CurrentCategory=category }; return View(viewModel); }

我们修改了三个部分。第一,我们添加一个叫做category的参数。第二,改进Linq查询,如果category不是Null,仅匹配Category属性的Product对象被选择。最后一个改变是设置CurrentCategory的属性。这些变化会导致不能正确计算TotalItems的值。

1.2 更新已存在的单元测试

我们修改了List action方法的签名,它会放置一些已经存在的单元测试方法被编译。为了解决此事,传递null作为List方法的第一个参数。例如Can_Send_Pagination_View_Model,会变成这样

ProductsListViewModel result = (ProductsListViewModel)controller.List(null, 2).Model;

通过使用null,我们像以前一样,得到了全部的repository。

1.3 分类过滤单元测试

[TestMethod] publicvoid Can_Filter_Products() { Mock<IProductRepository> mock =new Mock<IProductRepository>(); 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 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"); }

1.4 改善URL方案

没有人像看到或使用丑陋的URLs,如/?category=Soccer。

publicstaticvoid RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute(null, "",//匹配空URL,如 / new { controller ="Product", action ="List", category = (string)null, id =1 } ); routes.MapRoute( null, "Page{id}",//匹配 /Page2 ,但是不能匹配 /PageX new { controller ="Product", action ="List", category = (string)null }, new { id =@"\d+" }//约束:id必须是数字 ); routes.MapRoute(null, "{category}",//匹配 /Football 或 /没有斜线的任何字符 new { controller ="Product", action ="List", id =1 }); routes.MapRoute( null, // 路由名称 "{category}/Page{id}", // 匹配 /Football/Page567 new { controller ="Product", action ="List" }, new { id =@"\d+" } ); }

路由添加的顺序是很重要的。如果改变顺序,会有意想不到的效果。

URLLeads To
/显示所有分类的products列表的第一页
/Page2显示所有类别的items列表的第二页
/Soccer显示指定分类的items列表的第一页
/Soccer/Page2显示指定分类的items列表的指定页
/Anything/Else调用Anything controller的Else action
路由系统既能处理来自客户端的请求,也能处理我们发出的URLs请求。

Url.Action方法是生成外向链接的最方便的方式。之前,我们用它来显示Page links,现在,为了分类过滤,需要传递这个信息给helper方法。

@Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new { id=x,category=Model.CurrentCategory}))

通过传递CurrentCategory我们生成的URL不会丢失分类过滤信息。

2 构建分类导航目录

我们会在多个controllers中用到这个分类列表,所以它应该独立,并可以重用。MVC框架有child action的概念,特别适合用来创建可重用的导航控件。Child Action依赖RenderAction这个HTML helper方法,它能让你在当前view中包含数量的action方法的输出。

这个方法给我们一个真实的controller,包含任何我们需要的程序逻辑,并能像其他controller一样单元测试。这确实是一个不错的方法,创建程序的小片段,保持整个MVC框架的方法。

2.1 创建导航控件

需要创建一个新的NavController controller,Menu action,用来渲染导航目录,并将方法的输出注入到layout。

publicstring Menu() { return"Hello from NavController"; }

要想在layout中渲染child action,编辑_Layout.cshtml文件,调用RenderAction help方法。

<div id="categories"> @{ Html.RenderAction("Menu", "Nav"); } </div>

RenderAction方法直接将content写入response流,像RenderPartial方法一样。这意味着方法返回void,它不能使用常规的Razor@tag。我们必须在Razor代码块中闭合调用方法,并使用分号终止声明。也可以使用Action方法,如果不喜欢代码块语法。

2.2 生成分类列表

我们不想在controller中生成URLs,我们用helper方法来做这些。所有我们要在Menu action方法中做的,就是创建一个分类列表:

publicclass NavController : Controller { // // GET: /Nav/ private IProductRepository repository; public NavController(IProductRepository repo) { repository = repo; } public PartialViewResult Menu() { IEnumerable<string> categories = repository.Products .Select(x => x.Category) .Distinct() .OrderBy(x => x); return PartialView(categories); }

Menu action方法很简单,它只用Linq查询,获得分类的名字的列表,并传输他们到视图。

2.3 生成分类列表的单元测试

我们的目标是要生成一个按字母表排列的没有重复项的列表。最简单的方式,是提供含有重复分类的,没有排列顺序的测试数据,传递给NavController,断言数据已经处理了干净了。

[TestMethod] publicvoid Can_Create_Categories() { Mock<IProductRepository> mock =new Mock<IProductRepository>(); 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()); NavController target =new NavController(mock.Object); string[] results = ((IEnumerable<string>)target.Menu().Model).ToArray(); Assert.AreEqual(results.Length, 3); Assert.AreEqual(results[0], "Apples"); Assert.AreEqual(results[1], "Oranges"); Assert.AreEqual(results[2], "Plums"); }

2.4 创建部分视图

视图名Menu,选中创建部分视图,模型类填IEnumerable<string>

@model IEnumerable<string> @{ Layout =null; } @Html.ActionLink("Home","List","Product") @foreach(var link in Model){ @Html.RouteLink(link, new { controller ="Product", action ="List", category=link, id =1 }) }

我们添加叫做Home的链接,会显示在分类列表的顶部,让和用户返回到没有分类过滤的,所有products列表的首页。为了做到这点,使用了ActionLink helper方法,使用偶们早前配置的路由信息生成HTML anchor元素。

然后枚举分类名字,使用RouteLink方法为他们创建连接。有点像ActionLink,但它让我们提供一组name/value pairs,当从路由配置生成URL时。

2.4 高亮当前分类

一般我们会创建一个包含分类列表和被选中的分类的视图模型。但是这次,我们展示View Bag特性。这个特性允许我们不使用视图模型,从controller传递数据到view。

public ViewResult Menu(string category=null) { ViewBag.SelectedCategory = category; IEnumerable<string> categories = repository.Products .Select(x => x.Category) .Distinct() .OrderBy(x => x); return View(categories); }

我们添加给Menu action方法添加了category参数,它由路由配置自动提供。我们给View的ViewBag动态创建了SelectedCategory属性,并设置它的值。ViewBag是一个动态对象。

2.5 报告被选中分类的单元测试

通过读取ViewBag中属性的值,我们可以测试Menu action方法是否正确地添加了被选中分类的细节。

[TestMethod] publicvoid Indicates_Selected_Category() { Mock<IProductRepository> mock =new Mock<IProductRepository>(); mock.Setup(m => m.Products).Returns( new Product[]{ new Product{ProductID=1,Name="P1",Category="Apples"}, new Product{ProductID=4,Name="P4",Category="Oranges"} }.AsQueryable()); //Arrange - create to controller NavController target =new NavController(mock.Object); //Arrage - define the category to selected string categoryToSelect ="Apples"; //Action string result = target.Menu(categoryToSelect).ViewBag.SelectedCategory; //Assert Assert.AreEqual(categoryToSelect, result); }

我们不需要转换ViewBag属性的值,这是相对于ViewData先进的地方。

new { @class = link == ViewBag.SelectedCategory ?"selected" : null }

在Menu.cshtml局部视图中的@html.RouteLink增加第三个参数。第一个参数是string linkText,第二个参数是object routeValues,第三个参数是object htmlAttributes。当前选中的分类会被指派 selected CSS类。

注意在匿名对象中的@class,作为新参数传递给RouteLink helper方法。它不是Razor tag。HTML使用class给元素指派CSS样式,C#使用class创建class。我们使用了C#特性,避免与HTML关键字class冲突。@符号允许我们使用保留的关键字。如果我们仅调用class参数,不加@,编译器会假设我们定义了一个新的C#类型。当我们使用@符号,编译器会知道我们想要创建在匿名类型中创建一个叫做class的参数。

2.6 修正页面总数

当前,页数指向所有的产品。当使用分类后,页数应不同。我们可以通过更新List action方法的ProductController,修复它。分页信息携带分类到总数。

TotalItems=category==null? repository.Products.Count(): repository.Products.Where(e=>e.Category==category).Count()

如果分类被选中,我们返回这个分类的items数。如果没有选中,返回总数。

[TestMethod] publicvoid Generate_Category_Specific_Product_Count() { Mock<IProductRepository> mock =new Mock<IProductRepository>(); 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 res4 = ((ProductsListViewModel)target.List(null).Model).PagingInfo.TotalItems; //Assert Assert.AreEqual(res1, 2); Assert.AreEqual(res2, 2); Assert.AreEqual(res3, 1); Assert.AreEqual(res4, 5); }
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐