ASPNET MVC4 都是Action的控制器(Actions-packed Controllers)
--我没有自信是否能把本次的任务完成,但我会负责任的完成
本人能力有限,尽量将书中的知识浓缩去讲,仔细学过后,然后你再学习其他语言的MVC框架也就大同小异了
本次覆盖知识点:
- 1. 怎样才算一个控制器(What makes a controller)
- 2. 控制器中的成员有什么 (What belongs in a controller)
- 3. 手动映射视图模型 (Manually mapping view models )
- 4. 验证该用户的输入 (Validating user input )
- 5. 使用默认的单元测试模版(Using the default unit test project )
在最近两章里,我们看了一下创建一个Guestbook应用程序基础知识,也看了几种不同的把数据传递给视图的可以实现的方式。在本章里,我们将会完成Guestbook例子,我们只添加一点点细节部分。我们将研究什么应该是controller中的一部分,什么不应该是,然后看一下怎样手动构造一个视图models,验证用户的输入,写几个不会使用到视图的控制器操作(action)。这个将会让我们在controller中建造一个很常用action块(block),也就是不返回view类型action代码,我们以前学的都是返回一个view(),这里就不是的了。
我们也将向你简单地介绍一下单元测试controller中的action,这样可以确保这些action可以正确的工作。我们将会使用默认的单元测试项目,然后为GuestbookController创建单元测试,测试前面几章的工作情况。
在我们学习这些新知识之前,我们先快速地概括一下controllers和actions
4.1 探讨Controllers和actions
备注:controller action指controller中的action(也就是你们经常写的方法而已)
controller actions指controller中全部actions
action名字加action表示 某某名字的action,例如有个叫Index的action,我就会这样表示
Index action.知道了吗?这方便我写博客,也更方便理解
view model指页面(view)中用到model,我上篇博客说的视图模型就是view model
action method指 返回值是ActionResult那个方法,比如Index那个action方法
user interface,用户接口,你可以意会理解成页面,也就是要跟用户交互的东西,比如我们给用提供一个页面,让用户录入数据,这就是一个interface,一个接口,你暂且在这里就是view,就是页面
business logic,商业逻辑,类似于数据访问层中业务实现的逻辑
domain,你可以理解数据访问层
domain model,数据访问层要使用到的model
以后我想直接写英文了,好方便理解,在上一波博客我是吃过苦了,文章读起来好拗口
在第一波里面我们看出来了Controller的重要性,它里面有很多方法(action),这些action都可以通过url实现触发调用.而这些action就是把model中的数据传到页面上的中间者,也可以理解为搬运工吧,所以理解action是怎样工作的是很重要的.在下一节,我们主要更详细地了解下controller actions是怎样工作的.
下面我们看下action的样子(GuestbookController中的Index action)
它继承了Controller,也含有默认的action,我们也知道所有的控制器必须继承Controller,其实框架允许我们只要至少实现IController否则,它是不能处理Web请求的
下面我们看看框架是怎么知道哪个class是个被当做controller来处理,因为controller说白了也是一个类而已.下面让我们看看
IController
4.1.1 IController和Controller 父类(base class)
IController定义了一个controller最根本的东东---Execute方法接受一个RequestContext类型的对象
下面是一个简单的controller,我们写一些html放到响应流里面(response stream)
我们手动实现一下:
这是一个实现了IController的控制器,我们实现了Execute方法,我们就能直接访问HttpContext,Request还有Response对象了.这种方式很容易定义但是没什么用.这样做,我们就无法直接呈现view.而且我们这样做---在controller中直接就写HTML,把逻辑和呈现的内容混在一起了,很难受.
好吧,我们先不看框架那些有用的特征,比如说安全性(第8章会讲),model binding(第10章),action results(第16章).
这样做,我们也丧失了定义action方法的能力----------因为所有的请求都被Execute处理了
实际上,你需要去实现IController也不太可能那样去做,因为它本身没什么用(避开框架的主要部分,有一些理由能让你觉的还是有用的).通常我们会继承父类----------ControllerBase和Controller
ControllerBase
ControllerBase类除了包含了我们已经看过的一些基本特征以外,它本身就直接实现了IController.举个例子:ControllerBase也包含ViewData属性(把数据出给view的一个手段).但是ControllerBase仍然还是不怎么有用-----它仍然还是不能通过使用action来呈现view.所以Controller来了
Controller
Controller继承ControllerBase,所以除了包含一些有意义的东西以外,它还包含了ControllerBase定义的东东(比如ViewData).它包含了ControllerActionInvoker(这是一个知道怎样根据url找到对应的方法(method),并执行了那个方法,它定义了一些方法,比如View(可以通过controller action来呈现一个view))
这是一个当你开始创建好你的controller的时候会自动继承的类,所以说直接继承ControllerBase或者IController都不会有什么帮助.但是我们知道了在MVC 管道(pipeline)模型中扮演很重要的角色,知道它们的存在还是很有用的.
好了,现在我们已经知道了怎样把一个类变成一个Controller.下面我们看看action
4.1.2 怎样才可以成为一个action method
在第二章(我的第一波博客,我从第二章开始讲的,第一章是介绍,以后我会按书里面的章节顺序讲,还希望谅解)我们 在controller中看到了一些public的action方法(事实上,决定一个方法能否成为一个action,是有条规律规则的,讲起来还是蛮复杂的,我们将会在第16章开始讲解).
平常呢,action method都是返回一个ActionResult的一个实例.举个例子,一个action返回值可以是void类型的,也可以直接输出HTML(很像我们刚刚的SimpleController)
我们也可以这样写,它们结果是一样的,返回一个HTML片段(snippet)
这个没有问题,因为ControllerActionInvoker它保证了action的返回值总是ActionResult.如果action返回的是一个ActionResult(例如ViewResult),ControllerActionInvoker就会被调用.但是如果action返回的一个不同的类型(就像这里,一个string),它的返回值是ContentObject对象(也是一个把要写入的东西放入响应流里面的东东),直接使用ContentResult也是一样的
这就是一个很简单的action了,不需要view就可以直接在浏览器中呈现HTML标签了。但是在真实世界的应用程序中通常不会这样用的,还是最好通过view把要呈现的页面和controller分开,这样做当我们改变user interface的时候,就不用动controller中的代码了
除了呈现标签或者返回一个view,还有其他几种类型,举个例子,我们可以使用RedirectToRouteResult,让用户在浏览的时候跳到其他页面(我们在第二章(我的第一波博客)中的RedirectToAction方法你已经使用过了,还记得吗?),我们也可以返回JSON(在第七章的AJAX中我们将会学到)
我们可以使用NonActionAttribute让一个controller中的public的action不是一个action。
NonActionAttribute是一个action方法选择器(action method selector),它可以重写 匹配一个方法名到一个action名的默认行为(behavior).NonActionAttribute是最简单的一个选择器,它可以使那些可以通过URL访问的方法不可以通过这种手段来访问,其实在第二章,我们也已经看到了一些选择器,比如HttpPostAttribute(可以确保这个action可以响应HTTP post形式的请求,其他方式都是不可以访问的)
提醒
NonActionAttribute很少使用的,在一个controller中一个public的方法你不想它成为action,你最好想一下controller中是不是它最好的地方,如果这个方法有用,你可能会写成private访问修饰符级别的。如果这个方法必须public,因为需要测试的原因,我觉得应该提取成一个单独的类
现在让我简单地看一下什么构成了action,你将会看到一些不同的方式,把内容放到浏览器中显示。除了view,你也可以直接发送content(内容),或者直接执行其他的action(重定向),这些技术在你的应用程序中都会很有用。
现在让我们去看一下action中的逻辑
4.2 在action method中有什么
MVC中最大的两点就是将各自的职责分离,user interface 和 逻辑(比如页面提交数据怎么处理)分开,因此整个程序就更容易维护(maintain)了。如果你没有让你的controller更精巧(lightweight实际是轻量级的意思),把重点都放在controller里,你就不会体会到它的好处了。
Controller应该扮演中间者(coordinator)--它不应该包含business logic。反之亦然(vice versa),它可以把view上的表单,用户的输入的数据(user input),来自view的,封装成一个对象,然后我们把这个对象在domain(实际business logic代码写
在的地方)层中使用,这样我们数据的存储都可以完成了。
让我们看一下通过controller如何把一个任务完成--手动的映射view models,接受用户的输入的数据。首先,展现的是如何匹配view models,现在我们打开我们的guestbook例子,添加一个新的页面, 用不同的方式显示数据,存数据就先放着。接下来我们在这个页面上,把数据添加到数据库之前先添加一些验证,因为我们的数据库不需要存储一些没有用的(invalid)数据。在本节结束之后,你应该在知道怎样构建一个view model了,怎样完成基本的input验证,应该有个大致的了解了
4.2.1 手动地去匹配view models(view models 指view使用到的model)
在第三章(上一波博客),我们已经有了强类型视图的印象,还有一个view model--为了能够把数据更好地显示在屏幕上,创建的而一个单独的model对象。
到目前为止,我们做的例子中,我们使用到了同样的类(GuestbookEntry)作为我们的domain model和view model--它展现了我们数据库中的信息,也是在user interface上要表示的字段,信息
在非常小的项目中使用,比如我们的guestbook,这还可以,但是随着程序越来越复杂,用户界面也复杂了,model中的数据已经不能直接匹配了,就有必要要分成两部分(题外话:我现在自己公司做的项目就是两部分,一个EF生成的实体,一个扩充的实体,都已Dto结尾,只是为了更方便的显示数据),正是由于这个,我们应该有能力可以把domain model转换成view model,让数据更容易在user interface显示
我们就写个例子吧,向我们的Guestbook项目上添加一个页面:做一个总结页面,知道每一个人发了多少个comment,首先我们创建一个view model(view model专门让页面好展现数据,就看你怎么设计了)
在Models文件下建立CommentSummary.cs
我们现在需要创建一个controller action----返回一个CommentSummary集合,我们在GuestbookController中写action
1: public ActionResult CommentSummary()
2: {
3: var entries = from entry in _db.Entries
4: group entry by entry.name into groupByName
5: orderby groupByName.Count() descending
6: select new CommentSummary
7: {
8: NumberOfComments = groupByName.Count(),
9: UserName = groupByName.Key
10: };
11: return View(entries.ToList());
12:
13: }
我们使用linq查询,不懂的可以看一下我的 小孩系列LINQ教程
这个映射(mapping)逻辑相当简单,把这些代码写在控制器你还是可以讲得通的,但是如果这个mapping变得更加复杂(举个例子,如果它取了很多来自不同数据源的信息,只是为了构造view model),我们应该把这些逻辑从controller action中移出,分离(就像以前的三层框架),让我们的controller更加简单,轻量级的
在view中,我们循环输出这个列表,用table展现
右键该action名称,保持默认,添加一个视图
1: @model IEnumerable<GuestInfo.Models.CommentSummary>
2:
3: <table>
4: <tr>
5: <th>Number of comments</th>
6: <th>User name</th>
7: </tr>
8: @foreach(var summaryRow in Model) {
9: <tr>
10: <td>@summaryRow.NumberOfComments</td>
11: <td>@summaryRow.UserName</td>
12: </tr>
13: }
14: </table>
自动匹配View models
除了手动地把domain object和view models进行映射(mapping)以外,你也可以使用工具,比如开源的AutoMapper,像这个目的,更少的代码就可以完成了,我们将在第11章中MVC项目中教你怎么使用AutoMapper
在这一节,我们已经很简短地看了一点view model,但是在下一章我们将要更仔细地看,我们当然也会研究view models和input models的不同点
除了mapping操作,controller中还有个经常用到的任务,验证用户输入
4.2.2 验证用户输入(input validation)
回到第二章,我们在GuestbookController.cs中的Create action接受了用户的input(输入)。
这个action,接受了Create.cshtml post过来的input的数据(一个GuestbookEntry对象(已经被MVC模型绑定(model-binding)机制处理了)的表单),设置一下日期,然后我们Insert一条数据到数据库,虽然已经完成了,但还没有真的完成--我们还没有任何的验证。此时,用户是可以不输入他们的name或者comment就可以提交(submit)数据的。现在让我们添加一些验证。
首先让我们在GuestbookEntry类中用required特性 注解一下 Name和Message,使用using System.ComponentModel.DataAnnotations;导入命名空间
1: using System;
2: using System.Collections.Generic;
3: using System.ComponentModel.DataAnnotations;
4: using System.Linq;
5: using System.Web;
6:
7: namespace GuestInfo.Models
8: {
9: public class GuestbookEntry
10: {
11: public int Id { get; set; }
12: [Required]
13: public string name { get; set; }
14: [Required]
15: public string Message { get; set; }
16: public DateTime DateAdded { get; set; }
17:
18: }
19: }
使用注解(Annotate),也是一种验证对象属性的方法,Required表示这个filed不能为空,还有一些其他注解,比如StringLengthAttribute:验证字符串长度的。(我们将会在第6章更深入学习一下)
一旦注解了,当Create action被调用的时候,MVC将会自动地验证这些属性,看是否验证通过,我们可以使用ModelState.IsValid属性,然后就可以做决定了,怎么处理。修改一下Create
1:
2: [HttpPost]//限制只能是HTTP Post 能够访问这个方法
3: public ActionResult Create(GuestbookEntry entry)
4: {
5: if (ModelState.IsValid)
6: {
7: entry.DateAdded = DateTime.Now;
8: _db.Entries.Add(entry);
9: _db.SaveChanges();
10: return RedirectToAction("Index");
11: }
12: return View(entry);
13: }
注意
调用ModelState.IsValid实际上不会进行表单验证,它只是检查验证是失败还是成功。验证发生在Controller action被调用之前
我们可以调用Html.ValidationSummary方法,当验证失败的时候,显示错误信息
运行页面,还没输入任何内容点击提交:
使用这些助手,MVC将会自动检测验证错误的信息,还应用了一个css样式,因为我们的程序是基于默认的MVC项目模版上写的,这个无效的信息,将会显示浅红色背景色
这个错误的信息内容你可以这样改,在Required加参数
同样地如果你不想 硬编码消息,想要依赖于通过资源文件,支持本地化,你可以指定ResourceName和Resource type
例如:
1: [Required(ErrorMessageResourceType=typeOf(MyResources),ErrorMessageResourceName="RequiredMessageError")]
2: public string Message { get; set; }
4.3 单元测试介绍
在这一节,我们将要简短地看一下测试controllers。有很多测试的方法,这里我们主要讲:unit testing
单元测试很小,脚本测试,使用的语言和你项目中的语言一样。为了验证某个方法对不对,能不能达到想要的效果就去单独隔离式地测试某个组件的方法或函数。随着程序的变大,单元测试也会变多,看到一个应用程序有成百甚至上千个单元测试都是很正常的,它们可以在任何时间执行,去验证bug,让bug不会再发生
为了让单元测试运行地更快,我们不能调用进程外面的东西,这点很重要。当测试一个controller中的代码,任何独立的部分都是模拟的,所以主要的产品代码还是controller它本身。针对这个有一种可能,控制器被设计成独立的部分,很容易就被调用(比如说database或者web服务)
为了更高效地测试我们的GuestbookController,为了允许测试,我们要做一些修改,但是在我们做这些事情之前,让我们看一下ASP.NET MVC中默认的单元测试模版
4.3.1 使用默认的单元测试模版
默认地,当你创建一个新的ASP.NET MVC项目的时候,VS会提供一个创建单元测试项目的选项(在第二章我们已经见过了)
如果你选择一个创建单元测试项目,vs将会生成一个using Visual Studio Unit Testing FrameWork。这个单元测试项目包括了一些示例的测试,比如在HomeControllerTest类可以看见
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using System.Web.Mvc;
6: using Microsoft.VisualStudio.TestTools.UnitTesting;
7: using GuestBook;
8: using GuestBook.Controllers;
9:
10: namespace Guestbook.Tests.Controllers
11: {
12: [TestClass]
13: public class HomeControllerTest
14: {
15: [TestMethod]
16: public void Index()
17: {
18: // 排列
19: HomeController controller = new HomeController();
20:
21: // 操作
22: ViewResult result = controller.Index() as ViewResult;
23:
24: // 断言
25: Assert.AreEqual("修改此模板以快速启动你的 ASP.NET MVC 应用程序。", result.ViewBag.Message);
26: }
27:
28: [TestMethod]
29: public void About()
30: {
31: // 排列
32: HomeController controller = new HomeController();
33:
34: // 操作
35: ViewResult result = controller.About() as ViewResult;
36:
37: // 断言
38: Assert.IsNotNull(result);
39: }
40:
41: [TestMethod]
42: public void Contact()
43: {
44: // 排列
45: HomeController controller = new HomeController();
46:
47: // 操作
48: ViewResult result = controller.Contact() as ViewResult;
49:
50: // 断言
51: Assert.IsNotNull(result);
52: }
53: }
54: }
看代码,跟以前其他 客户端形式的单元测试还是很像的,很好就学会了,这里我就不多说了
4.3.2 测试GuestbookController
有一些事情,GuestbookController的实现直接初始化,使用了GuestContext对象,这个对象访问数据库。这就是说没有数据库的前提下,要想正确的获得数据,这个测试不可能进行,这是一个集成测试(Integration Test),而不是单元测试了
尽管集成测试很重要,它能确保一个应用程序的组件是否正确的在配合,也就是说如果我们对在controller中测试这个逻辑感兴趣,我们不得不先测试数据库连接。在小数量的测试,这个可能,但是如果一个项目有成百上千个测试,执行时间明显下降,因为每一个都要连接数据库。这个问题的解决方案是减弱控制器中的GuestbookContext对象
不直接使用GuestbookContext,我们介绍一个repository,它提供了一个入口,在处理数据访问操作,操作GuestbookEntry对象,我们为我们的repository写个接口
这个接口定义了4个方法,匹配4个查询,对应GuestbookController
我们定义了包含查询逻辑的概念上的实现
1: using GuestInfo.Models;
2: using System;
3: using System.Collections.Generic;
4: using System.Linq;
5: using System.Text;
6: using System.Threading.Tasks;
7:
8: namespace Guestbook.Contract
9: {
10: public class GuestbookRepository : IGuestbookRepository
11: {
12: private GuestbookContext _db = new GuestbookContext();
13:
14: public IList<GuestbookEntry> GetMostRecentEntries()
15: {
16: return (from entry in _db.Entries
17: orderby entry.DateAdded descending
18: select entry).Take(20).ToList();
19: }
20:
21: public void AddEntry(GuestbookEntry entry)
22: {
23: entry.DateAdded = DateTime.Now;
24: _db.Entries.Add(entry);
25: _db.SaveChanges();
26:
27: }
28:
29: public GuestbookEntry FindById(int id)
30: {
31: var entry = _db.Entries.Find(id);
32: return entry;
33: }
34:
35: public IList<CommentSummary> GetCommentSummary()
36: {
37: var entries = from entry in _db.Entries
38: group entry by entry.Name into groupedByName
39: orderby groupedByName.Count() descending
40: select new CommentSummary
41: {
42: NumberOfComments = groupedByName.Count(),
43: UserName = groupedByName.Key
44: };
45: return entries.ToList();
46: }
47: }
48: }
接下来我们在GuestbookController中添加一些代码,把接口引进来
修改代码:
using Guestbook.Contract;
using GuestInfo.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace GuestInfo.Controllers
{
public class GuestbookController : Controller
{
private GuestbookContext _db = new GuestbookContext();
IGuestbookRepository _respository;
public GuestbookController() {
_respository = new GuestbookRepository();
}
public GuestbookController(IGuestbookRepository gu)
{
_respository = gu;
}
//
// GET: /Guestbook/
public ActionResult Index()
{
////Shift+Tab减少缩进,Tab增加缩进
//var mostRecentEntries = (from o in _db.Entries orderby o.DateAdded descending select o).Take(20);
//// ViewBag.Entries = mostRecentEntries.ToList();
//var model = mostRecentEntries.ToList();
var mostRecent = _respository.GetMostRecentEntries();
return View(mostRecent);
}
public ActionResult Create()
{
return View();
}
[HttpPost]//限制只能是HTTP Post 能够访问这个方法
public ActionResult Create(GuestbookEntry entry)
{
if (ModelState.IsValid)
{
//entry.DateAdded = DateTime.Now;
//_db.Entries.Add(entry);
//_db.SaveChanges();
_respository.AddEntry(entry);
return RedirectToAction("Index");
}
return View(entry);
}
public ActionResult Show(int id)
{
//var entry = _db.Entries.Find(id); //找到该Id的Entries
var entry = _respository.FindById(id);
bool hasPermission = User.Identity.Name == entry.name; //如果登陆人的姓名等于entry录入人的姓名,就显示Edit按钮
ViewData["hasPermission"] = hasPermission;
ViewBag.hasPermission = hasPermission;
return View(entry);
}
public ActionResult CommentSummary()
{
//var entries = from entry in _db.Entries
// group entry by entry.name into groupByName
// orderby groupByName.Count() descending
// select new CommentSummary
// {
// NumberOfComments = groupByName.Count(),
// UserName = groupByName.Key
// };
var entries = _respository.GetCommentSummary();
return View(entries.ToList());
}
}
}
虽然我们已经把逻辑查询部分移出了controller,但是仍然需要查询和测试。它不再是单元测试的一部分,而是一个集成测试,锻炼概念上的respository实例访问数据库。
依赖注入(Dependence Injection)
调把依赖性传进对象的构造函中的技术被称为依赖注入,我们已经手动地完成了依赖注入,通过在我们的类中添加很多构造函数。在第18章,我们将学习怎样使用依赖注入容器去避免这个多构造函数的需求。更多关于依赖注入的信息 http://manning.com/seeman
此时我们已经有能力绕开数据库测试我们的controller actions了,我们达到这个目的,我们要在我们的IGuestbookRepository接口的实现类做些假数据。我们将创建一个新类,实现这个接口,把所有的操作放进内存中,我们将使用mocking framework比如说moq,RHino Mocks(都可以通过Nuget安装),他们能够为我们自动创建含有假数据的接口的实现类
1: using Guestbook.Contract;
2: using GuestInfo.Models;
3: using System;
4: using System.Collections.Generic;
5: using System.Linq;
6: using System.Web;
7:
8: namespace GuestInfo.Interface
9: {
10: public class FakeGuestbookRepository : IGuestbookRepository
11: {
12: private List<GuestbookEntry> _entries
13: = new List<GuestbookEntry>();
14:
15: public IList<GuestbookEntry> GetMostRecentEntries()
16: {
17: return new List<GuestbookEntry>
18: {
19: new GuestbookEntry
20: {
21: DateAdded = new DateTime(2011, 6, 1),
22: Id = 1,
23: Message = "Test message",
24: name = "Jeremy"
25: }
26: };
27: }
28:
29: public void AddEntry(GuestbookEntry entry)
30: {
31: _entries.Add(entry);
32: }
33: public GuestbookEntry FindById(int id)
34: {
35: return _entries.SingleOrDefault(x => x.Id == id);
36: }
37:
38: public IList<CommentSummary> GetCommentSummary()
39: {
40: return new List<CommentSummary>
41: {
42: new CommentSummary
43: {
44: UserName = "Jeremy", NumberOfComments = 1
45: }
46: };
47: }
48: }
49: }
这个假实现暴露了相同的方法作为一个真实的版本,
除了那些简单的内存的集合,GetCommentSummary和GetMostRecentEntries方法是不能够响应获得的,其他方法都可以获得进行测试。新建一个单元测试的项目
这是两个测试用例
1: [TestMethod]
2: public void Index_RendersView()
3: {
4: var controller = new GuestbookController(
5: new FakeGuestbookRepository());
6: var result = controller.Index() as ViewResult;
7: Assert.IsNotNull(result);
8: }
9:
10: [TestMethod]
11: public void Index_gets_most_recent_entries()
12: {
13: var controller = new GuestbookController(
14: new FakeGuestbookRepository());
15: var result = (ViewResult)controller.Index();
16: var guestbookEntries = (IList<GuestbookEntry>) result.Model;
17: Assert.AreEqual(1, guestbookEntries.Count);
18:
19: }