在以前的章節,偶們設置了核心的基礎設施,如今咱們將使用基礎設計添加關鍵特性,你將會看到投資是如何回報的。咱們可以很簡單很容易地添加劇要的面向客戶的特性。沿途,你也會看到一些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
}