以下内容摘自:
http://www.cnblogs.com/r01cn/archive/2011/12/04/2275208.html
http://www.cnblogs.com/r01cn/archive/2011/12/06/2276733.html
感谢作者的翻译,这里只是译文。原书名:Pro ASP.NET MVC 3 Framework
第十二章 控制器与动作 PART2
产生输出
控制器在完成了一个请求的处理之后,它通常需要生成一个响应。通过实现IController接口,我们直接生成的是祼机控制器(意即很原始的控制器,或者叫祼态控制器,或干脆叫做裸控制器更好些? — 译者注),我们需要负责处理一个请求的各个方面,包括生成对客户端的响应。例如,如果我们想要发送一个HTML响应,那么我们必须生成并装配HTML的数据,然后用Response.Write方法把它发送到客户端。类似地,如果我们想把用户的浏览器重定向到另一个URL,我们需要调用Response.Redirect方法,并直接传递我们感兴趣的URL。清单12-7对这两种办法都作了演示。
Listing 12-7. Generating Results in an IController Implementation
using System.Web.Routing;
namespace ControllersAndActions.Controllers {
public class BasicController : IController {
public void Execute(RequestContext requestContext) {
string controller = ( string)requestContext.RouteData.Values[ " controller "];
string action = ( string)requestContext.RouteData.Values[ " action "];
requestContext.HttpContext.Response.Write(
string.Format( " Controller: {0}, Action: {1} ", controller, action));
// ... or ...
requestContext.HttpContext.Response.Redirect( " /Some/Other/Url ");
}
}
}
当通过Controller类派生控制器时,你可以使用同样的办法。上述在Execute方法中读取requestContext.HttpContext.Response属性时返回的是HttpResponseBase类,这个类在我们的派生控制器中可以直接通过Controller.Response属性进行使用,如清单12-8所示。
Listing 12-8. Using the Response Property to Generate Output
namespace ControllersAndActions.Controllers {
public class DerivedController : Controller {
public void Index() {
string controller = ( string)RouteData.Values[ " controller "];
string action = ( string)RouteData.Values[ " action "];
Response.Write(
string.Format("Controller: {0}, Action: {1}", controller, action));
// ... or ...
Response.Redirect("/Some/Other/Url");
}
}
}
这种办法是可以工作的,但它有几个问题:
· The controller classes must contain details of HTML or URL structure, which makes the classes harder to read and maintain.
控制器类必须含有HTML或URL结构的细节,这些使类难以阅读和维护。
· 把响应直接生成为输出的控制器难以进行单元测试。为了确定输出表示的是什么,你需要生成Response对象的模仿实现,然后才能处理你从控制器接收到的输出。例如,这可能意味着要解析HTML关键词,这是费时而痛苦的过程。
· 这种精细处理每个响应细节的方式是乏味而易错的。有些程序员喜欢对建立原始控制器的过程进行绝对控制,但大多数人很快就失败了。
幸运的是,MVC框架一个叫做动作结果的很好的特性解决了所有这些问题。以下小节介绍这个动作结果概念,并向你演示可以用它来生成控制器响应的不同方法。
理解动作结果
MVC框架通过使用动作结果把指明(stating)意图与执行(executing)意图分离开来。它工作起来十分简单。
不是直接用Response对象进行工作,而是返回派生于ActionResult类的一个对象,它描述我们希望控制器响应要做什么,例如,渲染一个视图、重定向到另一个URL或动作方法。
注意:动作结果系统是一种命令模式。这个模式描述你所处的场景并发送一些对象,这些对象描述了要执行的操作。更多细节参阅http://en.wikipedia.org/wiki/Command_pattern。
在面向对象编程中,命令模式是一种设计模式,在这个模式中,用一个对象来表示和封装之后要调用的方法所需要的所有信息。这些信息包括方法名、拥有这个方法的对象、以及该方法参数的值。
根据这一定义,我们可以这样来理解动作结果:动作结果是一种命令,它的目的是要执行一个方法。为了实现这种要执行一个方法的命令,我们需要形成一个对象,用这个对象来封装一些信息,这些信息包括目标方法名、包含该目标方法的对象、该目标方法所需要的参数等。
当MVC框架从一个动作方法接收一个ActionResult对象时,MVC框架调用由这个类所定义的ExecuteResult方法。然后该动作结果的实现为你处理这个Response对象,生成符合你意图的输出。一个简单的例子是清单12-9所示的RedirectResult类。MVC框架开源的好处之一是你可以看到场景背后的事情是如何进行的。我们简化了这个类,以使它易于阅读。
Listing 12-9. The System.Web.Mvc.RedirectResult Class
public RedirectResult( string url) : this(url, permanent: false) {
}
public RedirectResult( string url, bool permanent) {
Permanent = permanent;
Url = url;
}
public bool Permanent {
get;
private set;
}
public string Url {
get;
private set;
}
public override void ExecuteResult(ControllerContext context) {
string destinationUrl = UrlHelper.GenerateContentUrl(Url, context.HttpContext);
if (Permanent) {
context.HttpContext.Response.RedirectPermanent(destinationUrl,
endResponse: false);
}
else {
context.HttpContext.Response.Redirect(destinationUrl, endResponse: false);
}
}
}
当我们生成RedirectResult类的实例时,我们在其中传递了重定向用户的URL,以及(可选地)是否是永久还是临时重定向。ExecuteResult方法,它将在我们的动作方法完成时由MVC框架执行,通过框架提供的ControllerContext对象得到这个查询的Response对象,并调用RedirectPermanent或Redirect方法,这是我们在清单12-8中明确用手工做的事情。
UNIT TESTING CONTROLLERS AND ACTIONS
单元测试控制器与动作
MVC框架的许多部分都被设计成便于单元测试,特别是对控制器与动作。这种支持有几个原因:
· 你可以在一个web服务器之外测试控制器与动作。通过它们的基类(如HttpRequestBase)访问上下文对象,这易于模仿。
· 你不需要解析任何HTML来测试一个动作方法的结果。你可以检查所返回的ActionResult对象,以确保你接收了预期的结果。
· 你不需要模拟客户端请求。MVC框架的模型绑定系统允许你编写以方法参数接收输入的动作方法。要测试一个动作方法,你只要简单地直接调用该动作方法,并提供你感兴趣的参数值。
随着本书的进行,我们将向你演示如何生成各种动作结果的单元测试。
不要忘记,单元测试并不是事情的全部。当动作方法被依次调用时,会出现应用程序的复杂行为。单元测试最好与其它测试办法相结合。
我们可以使用这个RedirectResult类,通过在动作方法中生成RedirectResult的新实例并将其返回。清单12-10演示了我们的DerivedController类,它被更新成有两个动作方法,其中一个用RedirectResult把请求重定向到另一个。
Listing 12-10. Using the RedirectResult Class
namespace ControllersAndActions.Controllers {
public class DerivedController : Controller {
public void Index() {
string controller = ( string)RouteData.Values[ " controller "];
string action = ( string)RouteData.Values[ " action "];
Response.Write(
string.Format( " Controller: {0}, Action: {1} ", controller, action));
}
public ActionResult Redirect() {
return new RedirectResult("/Derived/Index");
}
}
}
如果你启动这个应用程序,并导航到/Derived/Redirect,你的浏览器将被重定向到/Derived/Index。为了使你的代码更简单,Controller类包含了用来生成各种ActionResults的一些便携方法。于是,例如通过返回Redirect方法的结果,我们可以取得清单12-10所示的效果,如清单12-11所示。
Listing 12-11. Using a Controller Convenience Method for Creating an Action Result
return Redirect( " /Derived/Index ");
}
在动作结果系统中再没有其它复杂的东西了,你最终实现了更简单、更清晰、且更具一致性的代码。而且,你可以很容易地测试你的动作方法。例如,在这个重定向案例中,你可以简单地检查该动作方法返回了一个RedirectResult实例,以及Url属性含有你预期的目标。
MVC框架含有许多内建的动作结果类型,如表12-2所示。所有这些类型都派生于ActionResult,其中有不少在Controller类中有便利的辅助方法。
Type 类型 | Description 描述 | Controller Helper Methods 控制器辅助方法 |
---|---|---|
ViewResult | 返回指定的或默认的视图模板 | View |
PartialViewResult | 返回指定的或默认的局部视图模板 | PartialView |
RedirectToRouteResult | 将HTTP 301或302重定向发送给一个动作方法或特定的路由条目,根据你的路由配置生成一个URL | RedirectToAction RedirectToActionPermanent RedirectToRoute RedirectToRoutePermanent |
RedirectResult | 将HTTP 301或302重定向发送给一个特定的URL | Redirect RedirectPermanent |
ContentResult | 把原始的文字数据返回给浏览器,可选地设置一个content-type(内容类型)头 | Content |
FileResult | 直接把二进制数据(如磁盘文件或内存中的字节数组)传输给浏览器 | File |
JsonResult | 序列化一个JSON格式的.NET对象,并把它作为响应进行发送 | Json |
JavaScriptResult | 发送一个应当由浏览器执行的JavaScript源代码片段(这只是打算提供给AJAX场合使用的,在第19章中描述) | JavaScript |
HttpUnauthorizedResult | 将响应的HTTP状态码设置为401(意为“未授权”),这会引发当前的认证机制(表单认证或Windows认证)来要求访问者登录 | None |
HttpNotFoundResult | 返回一个HTTP 404 — 未找到错误 | HttpNotFound |
HttpStatusCodeResult | 返回一个指定的HTTP码 | None |
EmptyResult | 什么也不做 | None |
在以下小节中,我们将向你演示如何使用这些结果,以及如何生成和使用自定义动作结果。
通过渲染视图返回HTML
一个动作方法最常用的一种响应形式是生成HTML并把它发送给浏览器。当使用动作结果系统时,你通过生成一个ViewResult类的实例来做这件事,该实例指定你想渲染以生成这个HTML视图,当清单12-12所示。
Listing 12-12. Specifying a View to Be Rendered Using ViewResult
namespace ControllersAndActions.Controllers {
public class ExampleController : Controller {
public ViewResult Index() {
return View( " Homepage ");
}
}
}
在这个清单中,我们使用View辅助方法来生成ViewResult类的一个实例,然后它作为该动作方法的结果被返回。
注意:返回类型是ViewResult。该方法被编译并运行,就好像我们已经指定了更一般的ActionResult类型一样。事实上,有些MVC程序员会把每个动作方法的结果都定义成ActionResult,即使他们知道该动作方法返回的是一个更特定的类型。在知道方法返回的类型时,我们更喜欢用最具体的类型,这是常规的面向对象约定。在以下的例子中,我们特别注重了这种实践,以使你清楚可以如何去使用每一种结果类型。
通过送给View方法的参数,你指定了你要渲染的视图。在这个例子中,我们已经指定了Homepage视图。
注意:我们可能明确地生成了ViewResult对象,(用return new ViewResult { ViewName = "Homepage" };)。这是一个完全可接受的办法,但我们更喜欢使用由Controller类所定义的便利辅助方法。
当MVC框架调用ViewResult对象的ExecuteResult方法时,将开始搜索你已经指定的视图。如果在你的项目中使用了区域,那么框架将查找以下位置:
· /Areas/ < AreaName >/Views/ < ControllerName >/ < ViewName >.ascx
· /Areas/ < AreaName >/Views/Shared/ < ViewName >.aspx
· /Areas/ < AreaName >/Views/Shared/ < ViewName >.ascx
· /Areas/ < AreaName >/Views/ < ControllerName >/ < ViewName >.cshtml
· /Areas/ < AreaName >/Views/ < ControllerName >/ < ViewName >.vbhtml
· /Areas/ < AreaName >/Views/Shared/ < ViewName >.cshtml
· /Areas/ < AreaName >/Views/Shared/ < ViewName >.vbhtml
你可以从上述列表看出,框架也查找了遗留视图引擎生成的视图(.aspx和.ascs文件扩展名),即使我们在生成该项目时指定的是Razor。框架也查找了C#和VB的.NET Razor模板(.cshtml文件为C#模板的,.vbhtml是VB的)。MVC框架依次检查这些文件是否存在。只要它定位到一个匹配,它就用这个视图来渲染该动作方法的结果。
如果你未使用区域,或者你使用了区域,但在前述列表中没找到文件,那么框架会使用以下的位置继续它的搜索:
· /Views/ < ControllerName >/ < ViewName >.ascx
· /Views/Shared/ < ViewName >.aspx
· /Views/Shared/ < ViewName >.ascx
· /Views/ < ControllerName >/ < ViewName >.cshtml
· /Views/ < ControllerName >/ < ViewName >.vbhtml
· /Views/Shared/ < ViewName >.cshtml
· /Views/Shared/ < ViewName >.vbhtml
再一次地,一旦MVC框架检测一个位置并找到一个文件,搜索便停止,已经找到的这个视图便被用来渲染响应到客户端。
在清单12-12中,我们未使用区域,因此框架查找的第一个位置将是/Views/Example/Index.aspx。注意,这个类名的Controller部分是被忽略的(意即,ExampleController的Controller被忽略 — 译者注),因此,在ExampleController中生成的ViewResult会导致对名为Example的目录的搜索。
UNIT TEST: RENDERING A VIEW
单元测试:渲染一个视图
To test the view that an action method renders, you can inspect the ViewResult object that it returns. This is not quite the same thing—after all, you are not following the process through to check the final HTML that is generated—but it is close enough, as long as you have reasonable confidence that the MVC Framework view system works properly.
为了测试一个动作方法渲染的视图,你可以检查它所返回的ViewResult对象。这并不是十分相同的事情 — 必竟,你并不是在通过查检最终生成的HTML来跟踪这一过程 — 但这也已经足够了,只要你有理由确信MVC框架视图系统能恰当地工作。
要测试的第一个情况是当动作方法选择了一个特定视图时,像这样:
return View( " Homepage ");
}
通过读取ViewResult对象的ViewName属性,你可以确定哪个视图被选中了,如以下测试方法所示:
public void ViewSelectionTest() {
// Arrange - create the controller
ExampleController target = new ExampleController();
// Act - call the action method
ActionResult result = target.Index();
// Assert - check the result
Assert.AreEqual( " Homepage ", result.ViewName);
}
当你测试选择了默认视图的动作方法时,稍有不同,像这样:
return View();
}
在这种情况下,你需要对视图名用空字符串,像这样:
Assert.AreEqual("", result.ViewName);
MVC框架搜索视图目录的顺序是“约定优于配置”这一规范的另一个例子(The sequence of directories that the MVC Framework searches for a view is another example of convention over configuration)。你不需要用框架来注册你的视图文件。只需要把它们放在一组已知位置的一个位置上,框架会找到它们。
提示:定位视图的命名约定是可定制的。我们将在第15章向你演示如何去做。
在我们调用View方法时,通过忽略待渲染视图名,我们可以使这个约定更进一步,如清单12-13所示。
Listing 12-13. Creating a ViewResult Without Specifying a View
namespace ControllersAndActions.Controllers {
public class ExampleController : Controller {
public ViewResult Index() {
return View();
}
}
}
当我们这样做时,MVC框架假设我们想渲染的是一个与动作方法同名的视图。意即,清单12-13中对View方法的调用将启动对一个名字为Index的视图的搜索。
注意:其效果是一个与动作方法同名的视图被搜寻,但视图名实际上是根据RouteData.Values[“action”]的值所确定的,这就是你在清单12-7中以及在第11章路由系统部分所看到的情况。
View方法有许多重载版本。它们对应所创建的ViewResult对象上设置的不同属性。例如,通过明确地命名一个布局,你可以覆盖一个视图所使用的(默认)布局,像这样:
return View( " Index ", " _AlternateLayoutPage ");
}
译者注:在这个方法中,"_AlternateLayoutPage"参数给出的是布局文件名(不带扩展名),该布局应当是/View/Shared/文件夹中的_AlternateLayoutPage.cshtml文件。
SPECIFYING A VIEW BY ITS PATH
通过路径指定视图
命名约定的办法方便而简单,但它限制了你可以渲染的视图。如果你想渲染一个特定的视图,你可以通过提供一个明确的路径并绕过搜索阶段来做这件事。以下是一个例子:
namespace ControllersAndActions.Controllers {
public class ExampleController : Controller {
public ViewResult Index() {
return View("~/Views/Other/Index.cshtml");
}
}
}
当你像这样指定了一个视图时,路径必须从/或~/开始,并包括文件扩展名(例如,.cshtml对应的是含有C#代码的Razor视图)。
如果你感到你要用这一特性(feature),我们建议你花一些时间,并自问一下你要达到什么结果。如果是试图渲染属于另一个控制器的视图,那么你也许最好把用户重向到那个控制器的一个动作方法(参见本章稍后“重定向到一个动作方法”小节的例子)。因为它不适合你项目的组织方式,如果你试图围绕命名方案进行工作,那么参见第15章,它解释了如何实现一个自定义搜索顺序。(这一小节的含义是你最好不要用这种办法直接指定视图,这不利于应用程序的进一步扩展和维护 — 译者注)
把数据从动作方法传递给视图
我们通常需要把数据从一个动作方法传递给一个视图。MVC框架提供了许多做这种事情的不同方式,我们将在以下小节中加以描述。在这些小节中,我们提及的关于视图的论题,将在第15章作深度讨论。在本章中,我们将只讨论足以演示我们感兴趣的控制器特性(features)所需要的视图功能性。
提供视图模型对象
你可以把一个对象作为View方法的参数,把这个对象发送给视图,如清单12-14所示。
Listing 12-14. Specifying a View Model Object
DateTime date = DateTime.Now;
return View(date);
}
我们已经传递了一个DateTime对象作为视图模型。我们可以在视图中用Razor的Model关键词来访问这个对象,如清单12-15所示。
Listing 12-15. Accessing a View Model in a Razor View
ViewBag.Title = "Index";
}
< h2 >Index </ h2 >
The day is: @(((DateTime)Model).DayOfWeek)
清单12-15所示的视图称为非类型或弱类型视图。该视图不知道关于视图模型对象的任何事情,而把它作为对象的一个实例来看待。为了得到DayOfWeek属性的值,我们需要把这个对象转换成DateTime的一个实例。这可以工作,但会产生杂乱的视图。我们可以通过生成强类型视图加以整理,在强类型视图中,我们告诉视图,视图模型对象将是什么类型,如清单12-16所示。
Listing 12-16. A Strongly Typed View
@{
ViewBag.Title = "Index";
}
< h2 >Index </ h2 >
The day is: @Model.DayOfWeek
我们用Razor的model关键词指定了视图的模型类型。注意,当我们指定模型类型时,使用小写的m,而在读取值时使用大写的M。这样做不仅便于整洁我们的视图,而且Visual Studio对强类型视图支持智能感应,如图12-3所示。
图12-3. 支持强类型视图的智能感应
UNIT TEST: VIEW MODEL OBJECTS
单元测试:视图模型对象
你可以通过ViewResult.ViewData.Model属性来访问从动作方法传递给视图的视图模型对象。以下是一个简单的动作方法:
return View(( object) " Hello, World ");
}
该动作方法传递一个字符串作为视图模型对象。我们已经把它转换成object,以便编译器不会认为我们想用的是指定视图名的那个View重载。我们可以通过ViewData.Model属性来访问这个视图模型,如以下测试方法所示:
public void ViewSelectionTest() {
// Arrange - create the controller
ExampleController target = new ExampleController();
// Act - call the action method
ActionResult result = target.Index();
// Assert - check the result
Assert.AreEqual( " Hello, World ", result.ViewData.Model);
}
用ViewBag传递数据
在第3章中我们介绍过View Bag(视图包)特性(feature)。该特性允许你在一个动态对象上定义任意属性,并在视图中访问它们。这个动态对象可以通过Controller.ViewBag属性进行访问,如清单12-17所示。
Listing 12-17. Using the View Bag Feature
ViewBag.Message = "Hello";
ViewBag.Date = DateTime.Now;
return View();
}
在该清单中,我们通过简单赋值的办法,已经定义了名为Message和Date的属性。在此之前这些属性是不存在的,我们不做任何准备地生成了它们。要在视图中读回这些数据,我们简单地采用在动作方法中设置的同样的属性,如清单12-18所示。
Listing 12-18. Reading Data from the ViewBag
ViewBag.Title = "Index";
}
< h2 >Index </ h2 >
The day is: @ViewBag.Date.DayOfWeek
< p />
The message is: @ViewBag.Message
ViewBag相对于使用一个视图模型对象有一个优点:它便于把多个对象发送给视图。如果我们只能使用视图模型,那么,为了获得清单12-17和12-18的同样效果,我们就需要生成一个新类型,它具有string和DateTime成员。
当用动态对象进行工作时,你可以在视图中调用方法和属性的任意序列,像这样:
The day is: @ViewBag.Date.DayOfWeek.Blah.Blah.Blah
Visual Studio不能提供对任何动态对象的智能感应支持,包括ViewBag,而且在视图被渲染之前不支持诸如“对此无法展示”之类的错误提示。
提示:我们喜欢ViewBag的灵活性,但我们倾向于使用强类型视图。在同一个视图中既使用视图模型也使用View Bag是没有限制的。它们两者可以无干扰地一起工作。
UNIT TEST: VIEWBAG
单元测试ViewBag
你可以通过ViewResult.ViewBag属性读取ViewBag的值。以下测试方法是针对清单12-17中动作方法的测试:
public void ViewSelectionTest() {
// Arrange - create the controller
ExampleController target = new ExampleController();
// Act - call the action method
ActionResult result = target.Index();
// Assert - check the result
Assert.AreEqual("Hello", result.ViewBag.Message);
}
用View Data传递数据
View Bag特性(feature)是随MVC的第3版引入的。在此之前,用视图模型对象的替代方案主要是View Data。View Data(视图数据)特性(feature)类似于View Bag,但它是用ViewDataDictionary类不是动态对象实现的。ViewDataDictionary类似于规则的“键/值”集合,并通过Controller类的ViewData属性进行访问,如清单12-19所示。
Listing 12-19. Setting Data in the ViewData Class
ViewData[ " Message "] = " Hello ";
ViewData[ " Date "] = DateTime.Now;
return View();
}
就像使用“键/值”集合那样,你可以在视图中读回这些数据值,如清单12-20所示。
Listing 12-20. Reading View Data in a View
ViewBag.Title = "Index";
}
< h2 >Index </ h2 >
The day is: @(((DateTime)ViewData["Date"]).DayOfWeek)
< p />
The message is: @ViewData["Message"]
你可以看出,我们必须对从视图数据获得的对象进行转换,这十分类似于我们对非类型视图所做的那样。
注意:在View Bag可用的情况下,我们现在不太喜欢View Data了。
UNIT TEST: VIEW DATA
单元测试:View Data
通过读取ViewResult.ViewData的值,你可以测试使用View Data的动作方法。以下测试是用于清单12-19中的动作方法的:
public void ViewSelectionTest() {
// Arrange - create the controller
ExampleController target = new ExampleController();
// Act - call the action method
ActionResult result = target.Index();
// Assert - check the result
Assert.AreEqual( " Hello ", result.ViewData[ " Message "]);
}
执行重定向
一个动作方法的通常结果并不是直接产生输出,而是把用户的浏览器重定向到另一个URL。大多数情况下,这个URL是应用程序中的另一个动作方法,它生成你希望用户看到的输出。
POST/Redirect/GET模式
重定向最经常的应用是用在处理HTTP POST请求的动作方法中。正如我们在上一章所提到的,当你希望修改应用程序状态时,才会使用POST请求。如果你只是简单地在请求处理之后马上返回HTML,你就陷入这样的风险:用户点击浏览器的重载按钮(意指“刷新当前页面” — 译者注),并再次递交该表单、引发异常及不符需求的结果。
为了避免这种问题,你可以遵循Post/Redirect/Get模式。在这个模式中,你接收一个POST请求、处理它、然后重定向浏览器,以便由浏览器形成另一个GET请求的URL。GET请求不会修改你应用程序的状态,因此,该请求的任何不经意的再次递交都不会引起任何问题。
在执行重定向时,给浏览器发送的是以下两个HTTP代码之一:
· 发送HTTP代码302,这是一个临时重定向。这是最常用的重定向类型,而且直到MVC 3,这是MVC框架内建支持的唯一的一种重定向。当使用Post/Redirect/Get模式时,这是你要发送的代码。
· 发送HTTP代码301,它表示一个永久重定向。应该小心地使用它,因为它指示HTTP代码接收器不要再次请求原始URL,并使用包含重定向代码所伴随的新URL。如果你有疑问,请使用临时重定向,即,发送代码302。
重定向到字面(Literal)URL
重定向浏览器最基本的方式是调用Redirect方法,它返回RedirectResult类的一个实例,如清单12-21所示。
Listing 12-21. Redirecting to a Literal URL
return Redirect("/Example/Index");
}
你要重定向的URL应当表示成一个字符串,并作为传递给Redirect方法的参数。Redirect方法发送一个临时重定向。你可以用RedirectPermanent方法发送一个永久重定向,如清单12-22所示。
Listing 12-22. Permanently Redirecting to a Literal URL
return RedirectPermanent("/Example/Index");
}
提示:如果你喜欢,你可以用Redirect方法的重载版本,它以一个布尔型参数指定是否永久重定向。
UNIT TEST: LITERAL REDIRECTIONS
单元测试:字面重定向
字面重定向易于测试。你可以用RedirectResult类的Url和Permanen属性来读取URL以及该重定向是永久的还是临时的。以下是清单12-21重定向的一个测试方法。
public void RedirectTest() {
// Arrange - create the controller
ExampleController target = new ExampleController();
// Act - call the action method
RedirectResult result = target.Redirect();
// Assert - check the result
Assert.IsFalse(result.Permanent);
Assert.AreEqual( " /Example/Index ", result.Url);
}
重定向到路由系统的一个URL
如果你要把用户重定向到应用程序的一个不同的部分,你需要确保你发送的URL符合之前章节所描述的URL模式。用字面URL进行重定向的问题是,对路由方案的任何修改,都意味着你需要检查你的代码,并对URL进行更新。
一种可选的替代办法是,你可以运用路由系统,以RedirectToRoute方法来生成有效的URL,该方法会生成RedirectToRouteResult的一个实例,如清单12-23所示。
Listing 12-23. Redirecting to a Routing System URL
return RedirectToRoute( new {
controller = " Example ",
action = " Index ",
ID = " MyID "
});
}
RedirectToRoute方法发布一个临时重定向。对于永久重定向,使用RedirectToRoutePermanent方法。这两个方法都以一个匿名类型为参数,其属性然后被传递给路由系统,以生成一个URL。此过程的更多细节请参阅第11章的“生成输出URL”小节。
UNIT TESTING: ROUTED REDIRECTIONS
单元测试:路由的重定向
以下是测试清单12-23动作方法的示例:
public void RedirectValueTest() {
// Arrange - create the controller
ExampleController target = new ExampleController();
// Act - call the action method
RedirectToRouteResult result = target.Redirect();
// Assert - check the result
Assert.IsFalse(result.Permanent);
Assert.AreEqual( " Example ", result.RouteValues[ " controller "]);
Assert.AreEqual( " Index ", result.RouteValues[ " action "]);
Assert.AreEqual( " MyID ", result.RouteValues[ " ID "]);
}
重定向到一个动作方法
通过使用RedirectToAction方法,你可以更雅致地重定向到一个动作方法。这只是RedirectToAction方法一个封装程序,让你指定动作方法和控制器的值,而不需要生成一个匿名类型,如清单12-24所示。
Listing 12-24. Redirecting Using the RedirectToAction Method
return RedirectToAction( " Index ");
}
如果你只指定一个动作方法,那么它假设你指向的是当前控制器的一个动作方法。如果你想重定向到另一个控制器,你需要以参数提供其名字,像这样:
return RedirectToAction("Index", "MyController");
还有一些其它的重载版本,你可以用来为URL生成提供额外的值。这些版本是用一个匿名类型来表示的,这有点破坏了便携方法,但仍然能使你的代码易于阅读。
注意:你为控制的动作方法所提供的值,在它们被传递给路由系统之前,是不会被检验的。你有责任要确保你所指定的目标是实际存在的。
RedirectToAction方法执行一个临时重定向。用RedirectToActionPermanent进行永久重定向。
PRESERVING DATA ACROSS A REDIRECTION
跨越重定向保留数据
重定向引发浏览器递交一个全新的HTTP请求,这意味着你没有访问原始请求的细节。如果你希望把一个请求的数据传递给下一个请求,你可以用Temp Data特性。
TempData类似于Session数据,只不过,TempData的值在被读取之后,被标记为删除,并在该请求已经被处理之后被删除。这对于你跨越重定向而保留的短期数据是一种理想的安排。以下是使用了RedirectToAction方法的一个简单的动作方法:
TempData["Message"] = "Hello";
TempData["Date"] = DateTime.Now;
return RedirectToAction( " Index ");
}
当这个方法处理一个请求时,它在TempData集合中设置了一些值,然后把用户的浏览器重定向到同一个控制器中的Index动作方法。你可以在目标动作方法中读回TempData的值,然后把它们传递给视图,像这样:
ViewBag.Message = TempData["Message"];
ViewBag.Date = TempData["Date"];
return View();
}
更直接的办法是在视图中读取这些值像这样:
ViewBag.Title = "Index";
}
< h2 >Index </ h2 >
The day is: @(((DateTime)TempData["Date"]).DayOfWeek)
< p />
The message is: @TempData["Message"]
在视图中读取这些值,意味着你不需要在动作方法中使用View Bag或View Data特性。然而,你必须把TempData结果转换成相应的类型。
利用Peek方法,你可以得到TempData的值,而不必把它标记为删除,像这样:
利用Keep方法,你可以保留一个否则将被删除的值(You can preserve a value that would otherwise be deleted by using the Keep method),像这样:
Keep方法不会永久保护一个值。如果这个值被再次读取,它将被再次标记为删除。如果你想存储一些条目以使它们不会被自动删除,请使用会话(Session)数据。
返回文本数据
除HTML之外,还有许多你希望web应用程序作为响应而生成的其它基于文本的数据格式。这些包括:
· XML, as well as RSS and Atom (which are specific subsets of XML)
XML,以及RSS和Atom(特殊的XML子集)
· JSON (usually for AJAX applications)
JSON(通常用于AJAX应用程序)
· CSV (for exporting tabular data)
CSV(用于输出表数据)
· Just plain text
纯文本
MVC框架对JSON有特别支持,我们很快给你演示。对所有其它数据类型,我们可以使用多用途的ContentResult动作结果。清单12-25提供了一个演示。
Listing 12-25. Returning Text Data from an Action Method
string message = " This is plain text ";
return Content(message, " text/plain ", Encoding.Default);
}
我们通过Controller.Content辅助方法生成了ContentResult,它有三个参数:
· 第一个是你想要发送的文本数据。
· 第二个是该响应的HTTP的content-type头的值。你可以很容易地在网上查寻这些值,或使用System.Net.Mime.MediaTypeNames类来得到一个值,对于纯文本,其值是text/palin。
· 最后一个参数指定编码方案,它将被用来把该文本转换成字节序列。
你可以忽略后两个参数,此时,框架假设该数据是HTML(其内容类型为text/html)。它将试图选择浏览器在形成这个请求时已经申明支持的编码格式。这允许你只返回文本,像这样:
事实上,你可以往前走一小步。如果你从一个动作方法返回的对象不是一个ActionResult,MVC框架将试图把该数据序列化成一个字符串值,并把它作为HTML发送给浏览器,如清单12-26所示。
Listing 12-26. Returning a Non-ActionResult Object from an Action Method
return " This is plain text ";
}
你可以从图12-4看到该结果在浏览器中是如何显示的。
图12-4. 显示以text/html的content-type值投递的纯文本的浏览器
UNIT TEST: CONTENT RESULTS
单元测试:内容结果
测试返回ContentResult的动作方法相当简单,只要你能够在该测试方法中有意义地比较文本即可。以下是针对清单12-25所示的动作方法的一个简单测试:
public void ContentTest() {
// Arrange - create the controller
ExampleController target = new ExampleController();
// Act - call the action method
ContentResult result = target.Index();
// Assert - check the result
Assert.AreEqual( " text/plain ", result.ContentType);
Assert.AreEqual( " This is plain text ", result.Content);
}
ContentResult.Content属性提供了对结果所包含的内容的访问,而ContentType属性可以被用来获得数据的MIME类型。ContentResult类也定义了一个ContentEnconding属性,但这在单元测试中常常是被忽略的,因为这个值通常是由MVC模型基于用户浏览器所提供的信息来确定的。
返回XML数据
从一个动作方法返回XML数据是很简单的,特别是在你使用LINQ to XML和XDocument API通过对象生成XML时。清单12-27提供了一个演示。
Listing 12-27. Generating XML in an Action Method
StoryLink[] stories = GetAllStories();
XElement data = new XElement( " StoryList ", stories.Select(e => {
return new XElement( " Story ",
new XAttribute( " title ", e.Title),
new XAttribute( " description ", e.Description),
new XAttribute( " link ", e.Url));
}));
return Content(data.ToString(), " text/xml ");
}
用来生成XML的StoryLink类是这样定义的:
public string Title { get; set;}
public string Description { get; set; }
public string Url { get; set; }
}
该动作方法的结果是一个XML片段:
< Story title ="First example story" description ="This is the first example story"
link ="/Story/1" />
< Story title ="Second example story" description ="This is the second example story"
link ="/Story/2" />
< Story title ="Third example story" description ="This is the third example story"
link ="/Story/3" />
</ StoryList >
提示:如果你不熟悉LINQ to XML以及XDocument API,作一些了解是值得的。它们提供了我们所见到过的与XML一起工作最简单且最雅致的方式。Adam在他与Joe Ratz合著的Pro LINQ in C# 2010一书中深度涉及了这一论题,该书也由Apress出版。
返回JSON数据
最近几年,在web应用程序中运用XML文档以及XML片段已经衰退,而有利于使用JavaScript Object Notation(脚本对象注释 — JSON)格式。JSON是描述层次数据结构的一种轻量级的、基于文本的格式。JSON数据是合法的JavaScript代码,意即它得到了所有主流web浏览器的支持,使它比XML更紧凑且更易于使用。JSON最常用于把数据从服务器发送到客户端以适应我们将在第19章深度涉及的AJAX查询。
MVC框架有内建的JsonResult类,它负责把.NET对象序列化成JSON格式。你可以用Controller.Json便携方法生成JsonResult对象,如清单12-28所示。
Listing 12-28. Creating JSON Data with the JsonResult Class
public JsonResult JsonData() {
StoryLink[] stories = GetAllStories();
return Json(stories);
}
本例使用你之前看到的同一个StoryLink类,但我们不需要操纵这些数据,因为序列化是由JsonResult类负责的。清单12-28的动作方法所生成的响应如下:
"Description":"This is the first example story","Url":"/Story/1"},
{"Title":"Second example story",
"Description":"This is the second example story","Url":"/Story/2"},
{"Title":"Third example story",
"Description":"This is the third example story","Url":"/Story/3"}]
我们已经格式化了JSON数据以使它更可读。如果你不熟悉JSON不必着急。我们将在第19章回到这一论题,并提供一些详细的示例。还有,更多JSON细节请访问http://www.json.org。
注意:出于安全性原因,JsonResult对象将只生成HTTP POST请求的响应。目的是防止通过跨网站请求把数据暴露给第三方(我们将在第21章进行解释)。我们喜欢用HttpPost注释生成JSON的动作方法,以作为这种行为的提醒,尽管这不是实质性的。我们将在第19章解释如何取消这种行为。
返回文件及二进制数据
FileResult是把二进制数据发送给浏览器的所有动作结果相关的抽象基类。MVC框架提供了三个内建的具体子类供你使用:
· FilePathResult直接从服务器的文件系统发送一个文件。
· FileContentResult发送内存字节数组的内容。
· FileStreamResult发送已经被打开的System.IO.Stream对象的内容。
你不需要担心要使用哪个类型,因为它们是通过Controller.File辅助方法的各个重载为你自动生成的。你将在以下小节中看到这些重载的演示。
发送一个文件
清单12-29演示如何发送一个磁盘文件。
Listing 12-29. Sending a File
string filename = @" c:\AnnualReport.pdf ";
string contentType = " application/pdf ";
string downloadName = " AnnualReport2011.pdf ";
return File(filename, contentType, downloadName);
}
该动作方法引发浏览器提示用户保存该文件,如图12-5所示。不同的浏览器以它们自己的方式处理文件下载。该图显示了Internet Explorer 8所表现的提示。
图12-5. 浏览器的“打开或保存”提示
我们在上例中所用的File方法有三个参数的重载,其描述如表12-3。
参数 | 必需的? | 类型 | 描述 |
---|---|---|---|
filename | Yes | string | 被发送的文件路径(在服务器的文件系统中) |
contentType | Yes | string | 用作为响应的content-type头的MIME类型。浏览器将用这个MIME类型信息来决定如何处理该文件。例如,如果你指定application/vnd.ms-excel,浏览器将意图打开Microsoft Excel文件。类似地,application/pdf响应应该在用户的PDF查看程序中打开。 |
fileDownloadName | No | string | 随响应发送的content-disposition头的值。当指定这个参数时,浏览器对下载文件应该总是弹出“保存或打开”提示。浏览器应该把这个值视为下载文件的文件名,而不管下载文件的URL是什么。 |
如果你忽略fileDownloadName,而且浏览器自己知道如何显示MIME类型(例如,所有浏览器都知道如何显示image/gif文件),那么浏览器将自己显示该文件。
如果你忽略fileDownloadName,而浏览器不知道如何显示这个MIME类型(例如,你可能指定了application/vnd.ms-excel),那么,浏览器将弹出一个“保存或打开”提示,基于当前的URL猜测合适的文件名(在Internet Explorer中,基于你所指定的MIME类型)。然而,所猜测的文件名对用户几乎都是无意义的,因为它可能有一个不相关的.mvc文件扩展名,或根本没扩展名。因此,在你期望出现“保存或打开”提示的情况下,应当确保总是指定了fileDownloadName。
注意:如果你指定了一个与contentType参数不匹配的fileDownloadName,结果是不可预测的(例如,如果你指定的文件名是AnnualReport.pdf,而MIME类型是application/vnd.ms-excel)。如果你不知道你要发送的文件对应的MIME类型,你可以指定application/octet-stream。其含义是“某种不明确的二进制文件”。它告诉浏览器自己去决定如何处理这个文件,这通常基于文件的扩展名。
发送字节数组
如果你在内存中已经有二进制数据,你可以用不同的File重载方法把它传输给浏览器,如清单12-30所示。
Listing 12-30. Sending a Binary Array
byte[] data = ... // Generate or fetch the file contents somehow
return File(data, "application/pdf", "AnnualReport.pdf");
}
我们在第9章的末尾,当发送从数据库接收而来的数据时,使用了这一技术。再一次地,你必须指定contentType,并可以选择性地指定fileDownloadName。浏览器将像你发送一个磁盘文件一样地处理它们。
发送流内容
如果你所处理的数据可以通过开放System.IO.Stream进行操作,你可以简单地把这个流传递给File方法的一个(重载)版本。这个流的内容将被读取并发送给浏览器。如清单12-31所示。
Listing 12-31. Sending the Contents of a Stream
Stream stream = ...open some kind of stream...
return File(stream, "text/html");
}
UNIT TEST: FILE RESULTS
单元测试:文件结果
底层的FileResult类定义了两个属性,它们通用于所有三种类型的文件结果(指前述FilePathResult、FileContentResult、和FileStreamResult — 译者注):ContentType属性返回数据的MIME类型,FileDownloadName属性返回提示用户保存所用的文件名。对于更详细的信息,你必须用FileResult的上述三个具体子类之一进行工作。
当用FilePathResult进行工作时,你可以得到已经通过FileName属性指定的文件名。以下测试是用于清单12-29所示的动作方法的:
public void FileResultTest() {
// Arrange - create the controller
ExampleController target = new ExampleController();
// Act - call the action method
FileResult result = target.AnnualReport();
// Assert - check the result
Assert.AreEqual( @" c:\AnnualReport.pdf ", ((FilePathResult)result).FileName);
Assert.AreEqual( " application/pdf ", result.ContentType);
Assert.AreEqual( " AnnualReport2011.pdf ", result.FileDownloadName);
}
其它的具体实现有等同的属性。当处理内存数据时,你可以通过FileContentResult.FileContents属性获得字节数组。FileStreamResult类定义了一个名为FileStream的属性,它返回被传递给结果构造器的System.IO.Stream。
返回错误及HTTP代码
我们要探究的最后一个内建的ActionResult类可以用来给客户端发送指定的错误消息和HTTP结果代码。大多数应用程序不需要这些特性(features),因为MVC框架将自动地生成这些种类的结果。然而,如果你需要对发送给客户端的响应有更直接的控制,它们可能是有用的。
发送一个特定的HTTP结果代码
利用HttpStatusCodeResult类,你可以把特定的HTTP状态码发送给浏览器。这个类没有对应的控制器辅助方法,因此,你必须直接对这个类进行实例化,如清单12-32所示
Listing 12-32. Sending a Specific Status Code
return new HttpStatusCodeResult( 404, " URL cannot be serviced ");
}
HttpStatusCodeResult的构造器参数是数字状态码和一个可选的描述消息。在这个清单中,我们返回了代码404,它表示请求资源不存在。
发送404结果
用更方便的HttpNotFoundResult类,我们可以获得清单12-32的同样效果,这个类派生于HttpStatusCodeResult,而且可以用控制器的HttpNotFound便携方法来生成,如清单12-33所示。
Listing 12-33. Generating a 404 Result
return HttpNotFound();
}
发送401结果
另一个特定HTTP状态码的封装程序类是HttpUnauthorizedResult,它返回401代码,用来指示一个未授权请求。清单12-34给出了一个演示。
Listing 12-34. Generating a 401 Result
return new HttpUnauthorizedResult();
}
在Controller类中没有辅助方法来生成HttpUnauthorizedResult的实例,因此你必须直接做。返回这个类实例的效果通常是把用户重定向到认证页面,如你在第9章所见到的那样。
UNIT TEST: HTTP STATUS CODES 单元测试:HTTP状态码
HttpStatusCodeResult类遵循了你在其它结果类型所看到的模式,它通过一组属性使它的状态可用。这里,StatusCode属性返回数值型HTTP状态码,而StatusDescription属性返回相关的描述字符串。以下测试方法是针对清单12-33动作方法的:
public void StatusCodeResultTest() {
// Arrange - create the controller
ExampleController target = new ExampleController();
// Act - call the action method
HttpStatusCodeResult result = target.StatusCode();
// Assert - check the result
Assert.AreEqual( 404, result.StatusCode);
}
生成自定义动作结果
内建的动作结果类足以应付应用程序和大多数情况,但当你有某些特殊需要时,你可以生成自己的自定义动作结果。在本小节中,我们将演示一个自定义动作结果,它从一组对象生成一个RSS文档。RSS是一种常用于发布频繁更新的条目集的形式,如订户感兴趣的标题。清单12-35演示了我们的RssActionResult类。
Listing 12-35. Creating a Custom Action Result
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Xml.Linq;
namespace ControllersAndActions.Infrastructure {
public abstract class RssActionResult : ActionResult {
}
public class RssActionResult<T> : RssActionResult {
public RssActionResult( string title, IEnumerable<T> data,
Func<T, XElement> formatter) {
Title = title;
DataItems = data;
Formatter = formatter;
}
public IEnumerable<T> DataItems { get; set; }
public Func<T, XElement> Formatter { get; set; }
public string Title { get; set; }
public override void ExecuteResult(ControllerContext context) {
HttpResponseBase response = context.HttpContext.Response;
// set the content type of the response
response.ContentType = " application/rss+xml ";
// get the RSS content
string rss = GenerateXML(response.ContentEncoding.WebName);
// write the content to the client
response.Write(rss);
}
private string GenerateXML( string encoding) {
XDocument rss = new XDocument( new XDeclaration( " 1.0 ", encoding, " yes "),
new XElement( " rss ", new XAttribute( " version ", " 2.0 "),
new XElement( " channel ", new XElement( " title ", Title),
DataItems.Select(e => Formatter(e)))));
return rss.ToString();
}
}
}
事实上,我们定义了两个类。第一个是名为RssActionResult的抽象类,它是ActionResult的子类。第二个是名为RssActionResult<T>的强类型类,它派生于RssActionResult。我们已经定义了两个类,以使我们能够生成返回抽象RssActionResult类的动作方法,但生成的是强类型子类的实例。
我们的自定义动作结果的构造器,RssActionResult<T>,有三个参数:我们要生成的RSS文档的标题、该文档包含的数据项集合、和一个委托,它将被用于把每个数据项传输到一个包括在RSS输出中的XML片段中。
提示:注意,我们把标题、数据项、以及委派暴露为public属性。这样能够方便地单元测试,以使我们能够确定结果的状态,而不需要调用ExecuteResult方法。
为了从抽象的ActionResult类进行派生,你必须提供ExecuteResult方法的一个实现。我们的例子使用LINQ和XDocument API来生成一个RSS文档。该文档被写到Response对象,它可以通过ControllerContext参数进行访问。清单12-36演示了使用我们的自定义动作结果的动作方法。
Listing 12-36. Using a Custom Action Result
StoryLink[] stories = GetAllStories();
return new RssActionResult<StoryLink>( " My Stories ", stories, e => {
return new XElement( " item ",
new XAttribute( " title ", e.Title),
new XAttribute( " description ", e.Description),
new XAttribute( " link ", e.Url));
});
}
为了使用我们的自定义动作结果,我们简单地生成了一个新强类型实例,并在其中传递了所需要的参数。在这个例子中,我们使用了之前示例的StoryLink类,我们所定义的委派(用Func)生成数据集中每个故事的XML,如图12-6所示。
图12-6. 自定义动作结果的输出
小结
控制器是MVC设计模式中的关键构建块之一。本章中,你已经看到了通过实现IController接口如何生成“原始的”控制器,以及通过从Controller类派生的办法生成更方便的控制器。你看到了动作方法在MVC框架控制器中所起的作用,以及它们如何易于单元测试。
下一章,我们将更深入地进入控制器的基础结构,目的是定制如何生成控制器及其行为,以使你能够制作你应用程序的行为方式。