在以前的章节,偶们设置了核心的基础设施,如今咱们将使用基础设计添加关键特性,你将会看到投资是如何回报的。咱们可以很简单很容易地添加剧要的面向客户的特性。沿途,你也会看到一些MVC框架提供的附加的特性。html
1 添加导航控件框架
若是使用分类导航,须要作如下三个方面:ide
- 加强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对象,并是我用咱们新增的属性指示那个分类被选中。spa
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 更新已存在的单元测试3d
咱们修改了List action方法的签名,它会放置一些已经存在的单元测试方法被编译。为了解决此事,传递null做为List方法的第一个参数。例如Can_Send_Pagination_View_Model,会变成这样orm
1
ProductsListViewModel result
=
(ProductsListViewModel)controller.List(
null
,
2
).Model;
经过使用null,咱们像之前同样,获得了所有的repository。htm
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
}