管理功能,如何身份认证,对controller和action方法过滤安全的访问,并在用户需要时提供证书。
1 添加分类管理
方便管理的controller,有两类页面,List页面和edit页面。
1.1 创建CRUD Controller
在Controller文件夹上点右键,创建带CRUD的controller。我们要展示如何构建controller,并解释每个步骤,删除所有的方法,只留构造函数。
1
public
class
AdminController : Controller
2
{
3
private
IProductRepository repository;
4
5
public
AdminController(IProductRepository repo)
6
{
7
repository
=
repo;
8
}
9
}
1.2 用Repository中的产品渲染一个Grid
添加Index方法,显示repository中的所有产品。
1
public
ViewResult Index()
2
{
3
return
View(repository.Products);
4
}
1.2.1 Index action的单元测试
Index方法能正确地返回repository中的所有Product对象。
1
[TestMethod]
2
public
void
Index_Contains_All_Products()
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
"
},
8
new
Product{ProductID
=
2
,Name
=
"
P2
"
},
9
new
Product{ProductID
=
3
,Name
=
"
P3
"
}
10
}.AsQueryable());
11
12
//
Arrange - create a controller
13
AdminController target
=
new
AdminController(mock.Object);
14
15
//
Action
16
Product[] result
=
((IEnumerable
<
Product
>
)target.Index().ViewData.Model).ToArray();
17
18
//
Assert
19
Assert.AreEqual(result.Length,
3
);
20
Assert.AreEqual(
"
P1
"
, result[
0
].Name);
21
Assert.AreEqual(
"
P2
"
, result[
1
].Name);
22
Assert.AreEqual(
"
P3
"
, result[
2
].Name);
23
}
1.3 创建一个新视图
在/vie/shared中创建_AdminLayout.cshtml,布局文件名约定以_开头。
1
<
link href
=
"
@Url.Content(
"
~/
Content
/
Admin.css
"
)
"
rel
=
"
stylesheet
"
type
=
"
text/css
"
/>
引用CSS文件。
1.3 实现List View
创建AdminController的Index方法的视图,选择强类型视图,模型类是Product,使用layout文件,选在刚刚建立的_AdminLayout布局文件。并将 scaffold view(支架模型)设为List。选择了List的支架,VS会假设你使用IEnumerable序列作为模型视图的类型。
1
@model IEnumerable
<
SportsStore.Domain.Entities.Product
>
2
3
@{
4
ViewBag.Title
=
"
Index
"
;
5
Layout
=
"
~/Views/Shared/_AdminLayout.cshtml
"
;
6
}
7
8
<
h1
>
All Products
</
h1
>
9
10
<
table
class
=
"
Grid
"
>
11
<
tr
>
12
<
th
>
ID
</
th
>
13
<
th
>
14
Name
15
</
th
>
16
<
th
class
=
"
NumericCol
"
>
17
Price
18
</
th
>
19
<
th
>
20
Actions
21
</
th
>
22
</
tr
>
23
24
@foreach (var item
in
Model) {
25
<
tr
>
26
<
td
>
27
@item.ProductID
28
</
td
>
29
<
td
>
30
@Html.ActionLink(item.Name,
"
Edit
"
,
new
{item.ProductID})
31
</
td
>
32
<
td
class
=
"
NumericCol
"
>
33
@item.Price.ToString(
"
c
"
)
34
</
td
>
35
<
td
>
36
@using(Html.BeginForm(
"
Delete
"
,
"
Admin
"
)){
37
@Html.Hidden(
"
ProductID
"
,item.ProductID)
38
<
input type
=
"
submit
"
value
=
"
Delete
"
/>
39
}
40
</
td
>
41
42
</
tr
>
43
}
44
45
</
table
>
46
<
p
>
@Html.ActionLink(
"
Add a new product
"
,
"
Create
"
)
</
p
>
1.4 编辑Products
要提供创建和更新特性,我们将添加产品编辑页面。
- 显示一个页面,允许管理员改变产品属性的值
- 添加一个action方法,提交改变后处理
1.4.1 创建Edit Action方法
1
[TestMethod]
2
public
void
Can_Edit_Product()
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
"
},
8
new
Product{ProductID
=
2
,Name
=
"
P2
"
},
9
new
Product{ProductID
=
3
,Name
=
"
P3
"
}
10
}.AsQueryable());
11
12
//
Arrange - create a controller
13
AdminController target
=
new
AdminController(mock.Object);
14
15
//
Action
16
Product p1
=
target.Edit(
1
).ViewData.Model
as
Product;
17
Product p2
=
target.Edit(
2
).ViewData.Model
as
Product;
18
Product p3
=
target.Edit(
3
).ViewData.Model
as
Product;
19
20
//
Assert
21
Assert.AreEqual(
1
, p1.ProductID);
22
Assert.AreEqual(
2
, p2.ProductID);
23
Assert.AreEqual(
3
, p3.ProductID);
24
}
25
26
[TestMethod]
27
public
void
Cannot_Edit_Nonexistent_Product()
28
{
29
Mock
<
IProductRepository
>
mock
=
new
Mock
<
IProductRepository
>
();
30
mock.Setup(m
=>
m.Products).Returns(
31
new
Product[] {
32
new
Product{ProductID
=
1
,Name
=
"
P1
"
},
33
new
Product{ProductID
=
2
,Name
=
"
P2
"
},
34
new
Product{ProductID
=
3
,Name
=
"
P3
"
}
35
}.AsQueryable());
36
37
//
Arrange - create a controller
38
AdminController target
=
new
AdminController(mock.Object);
39
40
//
Action
41
Product result
=
target.Edit(
4
).ViewData.Model
as
Product;
42
43
//
Assert
44
Assert.IsNull(result);
45
}
1.4.2 创建Edit视图
使用强类型视图,模型类为Product。可以使用支架中的edit,但是我们使用Empty。使用_AdminLayout的布局文件。
1
@model SportsStore.Domain.Entities.Product
2
3
@{
4
ViewBag.Title
=
"
Admin: Edit
"
+
@Model.Name;
5
Layout
=
"
~/Views/Shared/_AdminLayout.cshtml
"
;
6
}
7
8
<
h1
>
Edit @Model.Name
</
h1
>
9
@using(Html.BeginForm()){
10
@Html.EditorForModel()
11
<
input type
=
"
submit
"
value
=
"
Save
"
/>
12
@Html.ActionLink(
"
Cancel and return to List
"
,
"
Index
"
)
13
}
与手工地写每个label和inputs相比,我们调用Html.EditorForModel helper方法。这个方法请求MVC框架,创建编辑界面,它会检查模型的类型。EditorForModel很方便,但不能产生最吸引人的结果。我们不想让管理员看到或编辑ProductID属性,并且描述属性的文本框太小。
我们可以使用model metadate(模型元数据),给MVC框架致命怎样为属性创建编辑器。这允许我们,对属性使用特性,来影响Html.EditorForModel方法的输出。
更新Product类
1
public
class
Product
2
{
3
[HiddenInput(DisplayValue
=
false
)]
4
public
int
ProductID {
get
;
set
; }
5
6
public
string
Name {
get
;
set
; }
7
8
[DataType(DataType.MultilineText)]
9
public
string
Description {
get
;
set
; }
10
11
public
decimal
Price {
get
;
set
; }
12
public
string
Category {
get
;
set
; }
13
}
HiddenInput需要添加System.Web.Mvc的引用。DateType需要添加System.ComponentModel.DataAnnotations的引用。HiddenInput属性告诉MVC框架,将这个属性渲染为隐藏的表元素。DataType属性允许我们指定值时如何呈现和编辑。
界面依然很简陋,我们可以使用CSS改善。当MVC框架为每个属性创建input fields,它指派不同的CSS classes。textarea元素上有class=”text-box multi-line”。我们改变它,在Content文件夹下更改Admin.css。页面模板视图助手EditorForModel不是总符合我们的需求,我们将会自定义。
1.4.3 更新Product Repository
要处理编辑前,我们得增强product repository,才能保存更改。给IProductRepository接口新增方法。
1
public
interface
IProductRepository
2
{
3
IQueryable
<
Product
>
Products {
get
; }
4
void
SaveProduct(Product product);
5
}
EF实现的repository,即EFProductRepository类中添加这个方法
1
public
void
SaveProduct(Product product)
2
{
3
if
(product.ProductID
==
0
){
4
context.Products.Add(product);
5
}
6
context.SaveChanges();
7
}
SaveChanges方法的实现,如果ProductID是0就添加一个Product给repository。它接受任何对现存Product的更改。
1.4.4 处理Edit POST 请求
当管理员点击Save按钮,Edit action方法会处理POST请求。
1
[HttpPost]
2
public
ActionResult Edit(Product product)
3
{
4
if
(ModelState.IsValid)
5
{
6
repository.SaveProduct(product);
7
TempData[
"
message
"
]
=
string
.Format(
"
{0} has been saved
"
, product.Name);
8
return
RedirectToAction(
"
Index
"
);
9
}
10
else
11
{
12
return
View(product);
13
}
14
}
先检查模型绑定已经验证用户提交的输数据。如果一切OK,保存变更到repositoy,然后调用Index action方法,返回到产品列表页面。如果有问题,再次渲染Edit视图,让用户更正。
在我们保存变更到repository后,我们使用TempData特性存储一个消息。这是键值类型的字典,和session data和View Bag相似。关键的不同之处是TempData会在HTTP request最后被删除。
注意我们从Edit方法返回了ActionResult类型。之前偶们都是用ViewResult类型,ViewResult是派生自ActionResult,当你想让框架渲染一个视图的时候可以使用。然而,其他类型可以使用ActionResult,RedirectToAction就是其中的一个。我们在Edit action方法中,用它调用Index action方法。
在这种情况下,用户被重定向,我们可以使用VeiwBag。ViewBag在controller和view之间传递数据,它不能比当前HTTP请求更长时间地持有数据。偶们可以使用session data特性,但是消息会在偶们明确地移除它时才删除,我们不想这样做。所以,TempData特使非常适合。数据被限制为单一用户的session(所以用户看不到其他用户的TempData),并且存留到我们阅读它。我们会在视图被action方法渲染后阅读数据。
1.4.5 Edit提交的单元测试
我们要确保对Product有效的更新,模型绑定已经被创建,传递给product repository保存。我们也想检查无效的更新,不会传给repository。
1
[TestMethod]
2
public
void
Can_Save_Valid_Changes()
3
{
4
//
Arrange - create mock repository
5
Mock
<
IProductRepository
>
mock
=
new
Mock
<
IProductRepository
>
();
6
//
Arrange - create the controller
7
AdminController target
=
new
AdminController(mock.Object);
8
//
Arrange - create the product
9
Product product
=
new
Product { Name
=
"
Test
"
};
10
11
//
Act - try to save the Product
12
ActionResult result
=
target.Edit(product);
13
14
//
Assert - check that the repository was called
15
mock.Verify(m
=>
m.SaveProduct(product));
16
//
Assert - check the method result type
17
Assert.IsNotInstanceOfType(result,
typeof
(ViewResult));
18
}
19
20
[TestMethod]
21
public
void
Cannot_Save_Invalid_Changes()
22
{
23
//
Arrange - create mock repository
24
Mock
<
IProductRepository
>
mock
=
new
Mock
<
IProductRepository
>
();
25
//
Arrange - create the controller
26
AdminController target
=
new
AdminController(mock.Object);
27
//
Arrange - create a product
28
Product product
=
new
Product { Name
=
"
Test
"
};
29
//
Arrange - add an error to the model state
30
target.ModelState.AddModelError(
"
error
"
,
"
error
"
);
31
32
//
Act - try to save the product
33
ActionResult result
=
target.Edit(product);
34
35
//
Assert - check that the repository was not called
36
mock.Verify(m
=>
m.SaveProduct(It.IsAny
<
Product
>
()), Times.Never());
37
//
Assert - check the method result type
38
Assert.IsInstanceOfType(result,
typeof
(ViewResult));
39
}
1.4.6 显示确认消息
在_AdminLaout.cshtml布局上显示TempData的消息。通过处理模板上的消息,我们可以在任何使用模板的视图上创建消息,而不用创建附加的Razor块。
1
<
div
>
2
@if(TempData[
"
message
"
]
!=
null
){
3
<
div
class
=
"
Message
"
>
@TempData[
"
message
"
]
</
div
>
4
}
5
@RenderBody()
6
</
div
>
这样做的好处是,无论用户打开哪个页面,只要使用相同的layout,即使改变了工作流的其他页面,用户也会看到消息。如果你重新载入页面,消息会小时,因为TempData会在阅读后被删除。
1.4.7 添加模型校验
像为ShippingDetails类一样,为Product类添加模型校验
1
public
class
Product
2
{
3
[HiddenInput(DisplayValue
=
false
)]
4
public
int
ProductID {
get
;
set
; }
5
6
[Required(ErrorMessage
=
"
Please enter a product name
"
)]
7
public
string
Name {
get
;
set
; }
8
9
[Required(ErrorMessage
=
"
Please enter a description
"
)]
10
[DataType(DataType.MultilineText)]
11
public
string
Description {
get
;
set
; }
12
13
[Required]
14
[Range(
0.01
,
double
.MaxValue,ErrorMessage
=
"
Please enter a positive price
"
)]
15
public
decimal
Price {
get
;
set
; }
16
17
[Required(ErrorMessage
=
"
Please specify a category
"
)]
18
public
string
Category {
get
;
set
; }
19
}
可以将这些限制属性移动到其他类中,并告诉MVC如何找到他们。
当使用Html.EditorForModel helper方法创建表单元素时,MVC框架会给inline加进markup和CSS。
1.4.8 启用客户端校验
MVC框架可以基于我们领域模型类中使用的data annotations执行客户端校验。这个特性默认启用,但是它还没有工作,因为哦我们没有添加必须的JavaScript库的链接。在_AdminLayout.cshtml文件上链接JavaScript库,可以在任何使用这个布局的页面上客户端校验。
1
<
script
src
="@Url.Content("
~/Scripts/jquery-1.4.4.min.js")" type
="text/javascript"
></
script
>
2
<
script
src
="@Url.Content("
~/Scripts/jquery.validate.min.js")" type
="text/javascript"
></
script
>
3
<
script
src
="@Url.Content("
~/Scripts/jquery.validate.unobtrusive.min.js")" type
="text/javascript"
></
script
>
使用客户端校验,会立即响应,并且不需要将请求发送到服务器。
如果你不想启用当前action的客户端校验,需要在view或controller中使用下面的声明
1
HtmlHelper.ClientValidationEnabled = false;
2
HtmlHelper.UnobtrusiveJavaScriptEnabled = false;
要禁用整个程序的客户端校验,需要将上面的声明添加到Global.asax的Application_Start方法中。或在Web.config文件中加入下面:
1
<
configuration
>
2
<
appSettings
>
3
<
add
key
="ClientValidationEnabled"
value
="false"
/>
4
<
add
key
="UnobtrusiveJavaScriptEnabled"
value
="false"
/>
5
</
appSettings
>
6
</
configuration
>
1.5 创建新产品
在AdminController中创建新方法
1
public ViewResult Create()
2
{
3
return View("Edit", new Product());
4
}
Create方法没有渲染它的默认视图,而是指定了Edit视图。这是完美的可接受的,一个action方法使用总是关联其他view的view。在这个例子中,我们注入一个新的Product对象,做诶视图模型,Edit视图使用空字段填充。
1
<
form
action
="/Admin/Create"
method
="post"
>
此时Html.BeginForm默认产生的表单,action为条用它的action,即Create。只有action为Edit时,才能正常编辑。要修复这点,我们可以使用html.BeginForm helper方法的重载版本,来指定触发表单生成的action和congtroller是Edit和Admin。
1
Html.BeginForm("Edit","Admin")
2
<
form
action
="/Admin/Edit"
method
="post"
>
1.6 删除Products
要添加delete是非常简单,首先要在IProductRepository接口添加新方法。
1
public
interface
IProductRepository
2
{
3
IQueryable
<
Product
>
Products {
get
; }
4
void
SaveProduct(Product product);
5
void
DeleteProduct(Product product);
6
}
7
public
void
DeleteProduct(Product product)
8
{
9
context.Products.Remove(product);
10
context.SaveChanges();
11
}
最后需要在AdminController中实现Delete action方法。这个方法必须仅支持POST请求,因为产出对象不是一个等幂操作。浏览器和缓存可以造出GET请求,而不用用户明确地同意。所以我们必须小心避免改变Get请求的结果。
1
[HttpPost]
2
public
ActionResult Delete(
int
productId)
3
{
4
Product product
=
repository.Products.FirstOrDefault(p
=>
p.ProductID
==
productId);
5
if
(product
!=
null
){
6
repository.DeleteProduct(product);
7
TempData[
"
message
"
]
=
string
.Format(
"
{0} was deleted
"
, product.Name);
8
}
9
return
RedirectToAction(
"
Index
"
);
10
}
1.6.1 删除产品的单元测试
我们想要测试两个特性,第一个是当一个有效的ProductID作为参数传递给action方法,它调用repository的DeleteProduct方法,并传递正确的、要删除的Product对象。
第二个测试是确保如果传递给Delete方法的参数值,不是repositrory中的可用Product,repository的DeleteProduct方法没有被调用。
1
[TestMethod]
2
public
void
Can_Delete_Valid_Products()
3
{
4
//
Arrange - create a Product
5
Product prod
=
new
Product { ProductID
=
2
, Name
=
"
Test
"
};
6
7
//
Arrange - create the mock repository
8
Mock
<
IProductRepository
>
mock
=
new
Mock
<
IProductRepository
>
();
9
mock.Setup(m
=>
m.Products).Returns(
new
Product[] {
10
new
Product {ProductID
=
1
, Name
=
"
P1
"
},
11
prod,
12
new
Product {ProductID
=
3
, Name
=
"
P3
"
},
13
}.AsQueryable());
14
15
//
Arrange - create the controller
16
AdminController target
=
new
AdminController(mock.Object);
17
18
//
Act - delete the product
19
target.Delete(prod.ProductID);
20
21
//
Assert - ensure that the repository delete method was
22
//
called with the correct Product
23
mock.Verify(m
=>
m.DeleteProduct(prod));
24
}
25
26
[TestMethod]
27
public
void
Cannot_Delete_Invalid_Products()
28
{
29
30
//
Arrange - create the mock repository
31
Mock
<
IProductRepository
>
mock
=
new
Mock
<
IProductRepository
>
();
32
mock.Setup(m
=>
m.Products).Returns(
new
Product[] {
33
new
Product {ProductID
=
1
, Name
=
"
P1
"
},
34
new
Product {ProductID
=
2
, Name
=
"
P2
"
},
35
new
Product {ProductID
=
3
, Name
=
"
P3
"
},
36
}.AsQueryable());
37
38
//
Arrange - create the controller
39
AdminController target
=
new
AdminController(mock.Object);
40
//
Act - delete using an ID that doesn't exist
41
target.Delete(
100
);
42
43
//
Assert - ensure that the repository delete method was
44
//
called with the correct Product
45
mock.Verify(m
=>
m.DeleteProduct(It.IsAny
<
Product
>
()), Times.Never());
46
}