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

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

1 添加导航控件

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

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

1.1 过滤Product列表

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

1 public class ProductsListViewModel 2 { 3 public IEnumerable < Product > Products { get ; set ; } 4 public PagingInfo PagingInfo { get ; set ; } 5 public string 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 public void 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 // Arrange 14 ProductController controller = new ProductController(mock.Object); 15 controller.pageSize = 3 ; 16 17 // Action 18 Product[] result = ((ProductsListViewModel)controller.List( " Cat2 " , 1 ).Model).Products.ToArray(); 19 20 // Assert 21 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 public static void 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 = 1 13 } 14 ); 15 16 routes.MapRoute( 17 null , 18 " Page{id} " , // 匹配 /Page2 ,但是不能匹配 /PageX 19 new { controller = " Product " , action = " List " , category = ( string ) null }, 20 new { id = @" \d+ " } // 约束:id必须是数字 21 ); 22 23 routes.MapRoute( null , 24 " {category} " , // 匹配 /Football 或 /没有斜线的任何字符 25 new 26 { 27 controller = " Product " , 28 action = " List " , 29 id = 1 30 }); 31 32 routes.MapRoute( 33 null , // 路由名称 34 " {category}/Page{id} " , // 匹配 /Football/Page567 35 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 public string 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 public class 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 public void 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, new 11 { 12 controller = " Product " , 13 action = " List " , 14 category = link, 15 id = 1 16 }) 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 public void 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 controller 12 NavController target = new NavController(mock.Object); 13 14 // Arrage - define the category to selected 15 string categoryToSelect = " Apples " ; 16 17 // Action 18 string result = target.Menu(categoryToSelect).ViewBag.SelectedCategory; 19 20 // Assert 21 Assert.AreEqual(categoryToSelect, result); 22 }

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

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

在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 public void 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 items 14 ProductController target = new ProductController(mock.Object); 15 target.pageSize = 3 ; 16 17 // Action - test the product counts for different categories 18 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 // Assert 24 Assert.AreEqual(res1, 2 ); 25 Assert.AreEqual(res2, 2 ); 26 Assert.AreEqual(res3, 1 ); 27 Assert.AreEqual(res4, 5 ); 28 }

你可能感兴趣的:(framework)