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

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

1 添加导航控件

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

  • 增强List action模型,让它能过滤repository中的Product对象
  • 重访并增强URL方案,修改我们的重路由策略
  • 创建sidebar风格的分类列表,高亮当前分类,并链接其它分类

1.1 过滤Product列表

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

1 publicclass ProductsListViewModel 2 { 3 public IEnumerable<Product> Products { get; set; } 4 public PagingInfo PagingInfo { get; set; } 5 publicstring CurrentCategory { get; set; } 6 }

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

1 public ViewResult List( string category,int? id) 2 { 3 int page = id.HasValue ? id.Value : 1; 4 ProductsListViewModel viewModel =new ProductsListViewModel 5 { 6 Products=repository.Products 7 .Where(p=>category==null||p.Category==category) 8 .OrderBy(p=>p.ProductID) 9 .Skip((page-1)*pageSize) 10 .Take(pageSize), 11 PagingInfo=new PagingInfo 12 { 13 CurrentPage=page, 14 ItemPerpage=pageSize, 15 TotalItems=repository.Products.Count() 16 }, 17 CurrentCategory=category 18 }; 19 return View(viewModel); 20 }

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

1.2 更新已存在的单元测试

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

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

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

1.3 分类过滤单元测试

1 [TestMethod] 2 publicvoid Can_Filter_Products() 3 { 4 Mock<IProductRepository> mock =new Mock<IProductRepository>(); 5 mock.Setup(m => m.Products).Returns(new Product[]{ 6 new Product {ProductID=1,Name="P1",Category="Cat1"}, 7 new Product {ProductID=2,Name="P2",Category="Cat2"}, 8 new Product {ProductID=3,Name="P3",Category="Cat1"}, 9 new Product {ProductID=4,Name="P4",Category="Cat2"}, 10 new Product {ProductID=5,Name="P5",Category="Cat3"} 11 }.AsQueryable()); 12 13 //Arrange14 ProductController controller =new ProductController(mock.Object); 15 controller.pageSize =3; 16 17 //Action18 Product[] result = ((ProductsListViewModel)controller.List("Cat2", 1).Model).Products.ToArray(); 19 20 //Assert21 Assert.AreEqual(result.Length, 2); 22 Assert.IsTrue(result[0].Name =="P2"&& result[0].Category =="Cat2"); 23 Assert.IsTrue(result[1].Name=="P4"&&result[1].Category=="Cat2"); 24 }

1.4 改善URL方案

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

1 publicstaticvoid RegisterRoutes(RouteCollection routes) 2 { 3 routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 4 5 routes.MapRoute(null, 6 "",//匹配空URL,如 / 7 new 8 { 9 controller ="Product", 10 action ="List", 11 category = (string)null, 12 id =113 } 14 ); 15 16 routes.MapRoute( 17 null, 18 "Page{id}",//匹配 /Page2 ,但是不能匹配 /PageX19 new { controller ="Product", action ="List", category = (string)null }, 20 new { id =@"\d+" }//约束:id必须是数字21 ); 22 23 routes.MapRoute(null, 24 "{category}",//匹配 /Football 或 /没有斜线的任何字符25 new26 { 27 controller ="Product", 28 action ="List", 29 id =130 }); 31 32 routes.MapRoute( 33 null, // 路由名称34 "{category}/Page{id}", // 匹配 /Football/Page56735 new { controller ="Product", action ="List" }, 36 new { id =@"\d+" } 37 ); 38 39 }

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

URL Leads To
/ 显示所有分类的products列表的第一页
/Page2 显示所有类别的items列表的第二页
/Soccer 显示指定分类的items列表的第一页
/Soccer/Page2 显示指定分类的items列表的指定页
/Anything/Else 调用Anything controller的Else action

路由系统既能处理来自客户端的请求,也能处理我们发出的URLs请求。

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

1 @Html.PageLinks(Model.PagingInfo, x => Url.Action("List", 2 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。

1 publicstring Menu() 2 { 3 return"Hello from NavController"; 4 }

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

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

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

2.2 生成分类列表

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

1 publicclass NavController : Controller 2 { 3 // 4 // GET: /Nav/ 5 private IProductRepository repository; 6 7 public NavController(IProductRepository repo) 8 { 9 repository = repo; 10 } 11 12 public PartialViewResult Menu() 13 { 14 IEnumerable<string> categories = repository.Products 15 .Select(x => x.Category) 16 .Distinct() 17 .OrderBy(x => x); 18 19 return PartialView(categories); 20 }

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

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

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

1 [TestMethod] 2 publicvoid Can_Create_Categories() 3 { 4 Mock<IProductRepository> mock =new Mock<IProductRepository>(); 5 mock.Setup(m => m.Products).Returns(new 6 Product[]{ 7 new Product{ProductID=1,Name="P1",Category="Apples"}, 8 new Product{ProductID=2,Name="P2",Category="Apples"}, 9 new Product{ProductID=3,Name="P3",Category="Plums"}, 10 new Product{ProductID=4,Name="P4",Category="Oranges"} 11 }.AsQueryable()); 12 13 NavController target =new NavController(mock.Object); 14 15 string[] results = ((IEnumerable<string>)target.Menu().Model).ToArray(); 16 17 Assert.AreEqual(results.Length, 3); 18 Assert.AreEqual(results[0], "Apples"); 19 Assert.AreEqual(results[1], "Oranges"); 20 Assert.AreEqual(results[2], "Plums"); 21 }

2.4 创建部分视图

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

1 @model IEnumerable<string> 2 3 @{ 4 Layout =null; 5 } 6 7 @Html.ActionLink("Home","List","Product") 8 9 @foreach(var link in Model){ 10 @Html.RouteLink(link, new11 { 12 controller ="Product", 13 action ="List", 14 category=link, 15 id =116 }) 17 }

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

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

2.4 高亮当前分类

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

1 public ViewResult Menu(string category=null) 2 { 3 ViewBag.SelectedCategory = category; 4 5 IEnumerable<string> categories = repository.Products 6 .Select(x => x.Category) 7 .Distinct() 8 .OrderBy(x => x); 9 10 return View(categories); 11 }

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

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

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

1 [TestMethod] 2 publicvoid Indicates_Selected_Category() 3 { 4 Mock<IProductRepository> mock =new Mock<IProductRepository>(); 5 mock.Setup(m => m.Products).Returns( 6 new Product[]{ 7 new Product{ProductID=1,Name="P1",Category="Apples"}, 8 new Product{ProductID=4,Name="P4",Category="Oranges"} 9 }.AsQueryable()); 10 11 //Arrange - create to controller12 NavController target =new NavController(mock.Object); 13 14 //Arrage - define the category to selected15 string categoryToSelect ="Apples"; 16 17 //Action18 string result = target.Menu(categoryToSelect).ViewBag.SelectedCategory; 19 20 //Assert21 Assert.AreEqual(categoryToSelect, result); 22 }

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

1 new { 2 @class = link == ViewBag.SelectedCategory ?"selected" : null3 }

在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,修复它。分页信息携带分类到总数。

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

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

1 [TestMethod] 2 publicvoid Generate_Category_Specific_Product_Count() 3 { 4 Mock<IProductRepository> mock =new Mock<IProductRepository>(); 5 mock.Setup(m => m.Products).Returns( 6 new Product[]{ 7 new Product {ProductID=1,Name="P1",Category="Cat1"}, 8 new Product {ProductID=2,Name="P2",Category="Cat2"}, 9 new Product {ProductID=3,Name="P3",Category="Cat1"}, 10 new Product {ProductID=4,Name="P4",Category="Cat2"}, 11 new Product {ProductID=5,Name="P5",Category="Cat3"} 12 }.AsQueryable()); 13 //Arrange - create a controller and make the page size 3 items14 ProductController target =new ProductController(mock.Object); 15 target.pageSize =3; 16 17 //Action - test the product counts for different categories18 int res1 = ((ProductsListViewModel)target.List("Cat1").Model).PagingInfo.TotalItems; 19 int res2 = ((ProductsListViewModel)target.List("Cat2").Model).PagingInfo.TotalItems; 20 int res3 = ((ProductsListViewModel)target.List("Cat3").Model).PagingInfo.TotalItems; 21 int res4 = ((ProductsListViewModel)target.List(null).Model).PagingInfo.TotalItems; 22 23 //Assert24 Assert.AreEqual(res1, 2); 25 Assert.AreEqual(res2, 2); 26 Assert.AreEqual(res3, 1); 27 Assert.AreEqual(res4, 5); 28 }
posted @ 2013-08-29 17:40  Reinhard_Hsu  阅读(192)  评论(0编辑  收藏  举报