了解ASP.NET MVC几种ActionResult的本质:EmptyResult & ContentResult
定义在Controller中的Action方法大都返回一个ActionResult对象。ActionResult是对Action执行结果的封装,用于最终对请求进行响应。ASP.NET MVC提供了一系列的ActionResult,它们本质上是通过怎样的方式来响应请求的呢?这是这个系列着重讨论的主题。[本文已经同步到《How ASP.NET MVC Works?》中]
目录
一、ActionResult对请求的响应
二、EmptyResult
三、ContentResult
四、实例演示:执行返回类型为非ActionResult的Action方法得到的ActionResult对象
五、实例演示:通过ContentResult实现主题定制
一、ActionResult对请求的响应
HTTP是一个单纯的采用请求/回复消息交换模式的网络协议,Web服务器在接收并处理来自客户端的请求后会根据处理结果对请求予以响应。对于来自客户端的访问请求,最终的处理体现在针对目标Action方法的执行,我们可以在定义Action方法的时候人为地控制对请求的响应。如果下面的代码片断所示,抽象类Controller具有一个只读的Response属性表示当前的HttpResponse,我们可以直接利用它来实现对请求的响应。我们也可以间接地通过表示当前HTTP上下文的HttpContext属性和表示Controller上下文的ControllerContext属性来获取用于响应请求的HttpResponse对象。
1: public abstract class Controller : ControllerBase, ...
2: {
3: //其他成员
4: public HttpResponseBase Response { get; }
5: public HttpContextBase HttpContext { get; }
6: }
7:
8: public abstract class ControllerBase : IController
9: {
10: //其他成员
11: public ControllerContext ControllerContext { get; set; }
12: }
原则上讲,我们可以利用HttpResponse对请求响应作百分之一百地控制,但是我们一般并不这么做,而是将针对请求的响应实现在一个ActionResult对象中。如下面的代码片断所示,ActionResult是一个抽象类型,最终的请求响应实现在抽象方法ExecuteResult方法中。
1: public abstract class ActionResult
2: {
3: //其他成员
4: public abstract void ExecuteResult(ControllerContext context);
5: }
顾名思义,ActionResult就是执行Action的结果。ActionInvoker在完成对Action方法的执行后,如果返回一个ActionResult对象,ActionInvoker会将当前Controller上下文作为参数调用其ExecuteResult方法。View的最终呈现是通过ActionResult的子类ViewResult来完成的,除了ViewResult,ASP.NET MVC还为我们定义了额外一些具体的ActionResult。
二、EmptyResult
上面我们谈到Action方法返回的ActionResult对象被ActionInvoker调用以实现对当前请求的响应,其实这种说法不够准确。不论Action方法是否具有返回值,也不论它的返回值是什么类型,ActionInvoker最终都会创建相应的ActionResult对象。如果Action方法返回类型为void,或者返回值为Null,最终生成的就是一个EmptyResult对象。
如下面的代码片断所示,在重写的ExecuteResult方法中EmptyResult其实什么都没有做,所以EmptyResult是一个“空”的ActionResult。EmptyResult的设计体现了一种设计思想:我们采用一种管道式的设计来完成针对某类请求的处理,比如ASP.NET MVC针对请求的处理流程是“Action方法的执行=〉根据执行结果生成ActionResult=〉执行ActionResult”,但是这个流程不适合某些特殊的请求(比如Action方法不具有返回值或者返回值为Null,那么后面的两个环节可以忽略),我们对这些例外的场景进行一些适配工作使我们可以按照统一的方式来处理所有的请求,所以EmptyResult在这里起到了一个适配器的作用。
1: public class EmptyResult : ActionResult
2: {
3: public override void ExecuteResult(ControllerContext context)
4: {
5: }
6: }
三、ContentResult
ContentResult使ASP.NET MVC按照我们指定的内容对请求予以响应。如下面的代码片断所示,我们可以利用ContentResult的Content属性以字符串的形式指定响应的内容,另外两个属性ContentEncoding和ContentType则用于指定字符编码方式和媒体类型(MIME类型)。抽象类Controller定义了如下三个受保护的Content方法重载根据指定的内容、编码和媒体类型创建相应的ContentResult。
1: public class ContentResult : ActionResult
2: {
3: public override void ExecuteResult(ControllerContext context);
4:
5: public string Content { get; set; }
6: public Encoding ContentEncoding { get; set; }
7: public string ContentType { get; set; }
8: }
9:
10: public abstract class Controller : ControllerBase, ...
11: {
12: //其他成员
13: protected ContentResult Content(string content);
14: protected ContentResult Content(string content, string contentType);
15: protected virtual ContentResult Content(string content, string contentType, Encoding contentEncoding);
16: }
在重写的ExecuteResult方法中,ContentResult利用作为参数的ControllerContext对象得到当前HttpContext的HttpResponse对象,并借助它将指定的内容按照希望的编码和媒体类型对请求进行响应,具体的实现如下面的代码片断所示。
1: public class ContentResult : ActionResult
2: {
3: //其他成员
4: public override void ExecuteResult(ControllerContext context)
5: {
6: HttpResponseBase response = context.HttpContext.Response;
7: if (!string.IsNullOrEmpty(this.ContentType))
8: {
9: response.ContentType = this.ContentType;
10: }
11: if (this.ContentEncoding != null)
12: {
13: response.ContentEncoding = this.ContentEncoding;
14: }
15: if (this.Content != null)
16: {
17: response.Write(this.Content);
18: }
19: }
20: }
上面我们说过,ASP.NET MVC为了能够采用相同的流程来处理所有的请求,不论是Action是否具有返回值,具有怎样的返回值,ActionInvoker都会创建相应的ActionResult。对于不具有返回值或者返回Null的Action方法调用来说,最终创建的是一个EmptyResult对象,那么如果返回值不是一个ActionResult对象,ActionInvoker最终会创建怎样一个ActionResult对象呢?
实际上对于一个非Null的返回值,ActionInvoker采用这样的方式来创建相应的ActionResult:如果返回对象是一个ActionResult,直接返回该对象,否则将对象转换成字符串并以此创建一个ContentResult对象。ControllerActionInvoker根据Action方法的返回指生成相应ActionResult的逻辑体现在如下一个受保护的虚方法CreateActionResult中,最后一个参数(actionReturnValue)表示Action方法的返回值。而另一个受保护的InvokeActionMethod负责执行Action方法并返回响应的ActionResult对象,该方法在执行Action方法得到返回值后通过调用CreateActionResult方法返回相应的ActionResult对象。
1: public class ControllerActionInvoker : IActionInvoker
2: {
3: //其他成员
4: protected virtual ActionResult InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters);
5: protected virtual ActionResult CreateActionResult(ControllerContext controllerContext, ActionDescriptor actionDescriptor, object actionReturnValue);
6: }
四、实例演示:执行返回类型为非ActionResult的Action方法得到的ActionResult对象
我们可以通过一个简单的实例来验证ActionInvoker针对Action方法返回值对ActionResult的创建逻辑。在一个ASP.NET MVC应用中我们定义了如下一个HomeController,其中定义了4个无参数的Action方法。Foo返回一个RedirectResult对象,Bar的返回类型为viod,Baz返回值为Null,而Qux则返回一个double类型的数字。
1: public class HomeController : Controller
2: {
3: //其他成员
4: public ActionResult Foo()
5: {
6: return new RedirectResult("http://www.asp.net");
7: }
8: public void Bar(){ }
9: public ActionResult Baz()
10: {
11: return null;
12: }
13: public double Qux()
14: {
15: return 1.00;
16: }
17: }
然后我们在HomeController定义如下一个Action方法Index。在该方法中我们通过反射的方式调用ActionInvoker的GetControllerDescriptor方法得到用于描述当前Controller的ControllerDescriptor对象。然后调用ControllerDescriptor的FindAction方法得到用于描述上述四个Action的ActionDescriptor对象。最后我们同样采用反射的方式调用ActionInvoker的InvokeActionMethod方法执行这4个Action并得到4个ActionResult对象。我们将4个得到ActionResult连同对应的ActionDescriptor对象构建一个Dictionary<ActionDescriptor, ActionResult>对象,并作为Model呈现在默认的View中。
1: public class HomeController : Controller
2: {
3: //其他成员
4: public ActionResult Index()
5: {
6: Dictionary<ActionDescriptor, ActionResult> actionResults = new Dictionary<ActionDescriptor, ActionResult>();
7: MethodInfo getControllerDescriptor = this.ActionInvoker.GetType().GetMethod("GetControllerDescriptor", BindingFlags.Instance | BindingFlags.NonPublic);
8: ControllerDescriptor controllerDescriptor = (ControllerDescriptor)getControllerDescriptor.Invoke(this.ActionInvoker, new object[] { ControllerContext });
9: MethodInfo invokeActionMethod = this.ActionInvoker.GetType().GetMethod("InvokeActionMethod", BindingFlags.Instance | BindingFlags.NonPublic);
10:
11: string[] actions = new string[] { "Foo", "Bar", "Baz", "Qux" };
12: Array.ForEach(actions, action =>
13: {
14: ActionDescriptor actionDescriptor = controllerDescriptor.FindAction(ControllerContext, action);
15: ActionResult actionResult = (ActionResult)invokeActionMethod.Invoke(this.ActionInvoker, new object[] { ControllerContext, actionDescriptor, new Dictionary<string, object>() });
16: actionResults.Add(actionDescriptor, actionResult);
17: });
18: return View(actionResults);
19: }
20: }
如下所示的是Action方法Index对应View的定义,IDictionary<ActionDescriptor, ActionResult>作为该View的Model类型。在该View中我们将存在于字典中的ActionResult对象的类型和对应的Action名称以表格的形式呈现出来。
1: @model IDictionary<ActionDescriptor, ActionResult>
2: <html>
3: <head>
4: <title>ActionResults</title>
5: </head>
6: <body>
7: <table rules="all">
8: <tr><th>ActionName</th><th>ActionResult</th></tr>
9: @foreach (var item in Model)
10: {
11: <tr>
12: <td>@item.Key.ActionName</td><td>@item.Value.GetType().Name</td>
13: </tr>
14: }
15: </table>
16: </body>
17: </html>
运行该程序后会在浏览器中得到如下图所示的输出结果,我们可以看到返回类型为void的Action方法Bar和返回值为Null的Action方法Baz执行后得到的都是一个EmptyResult对象。而返回非ActionResult(double类型)类型的Action方法Qux执行之后返回的是一个ContentResult。
五、实例演示:通过ContentResult实现主题定制
由于可以通过ContentResult的ContentType属性指定媒体类型,所以我们不仅仅可以利用它来返回最终会在浏览器中显示的文本,还可以返回其他一些类型的文本内容,比如JavaScript脚本(“text/javascript”)和CSS样式(“text/css”)等。通过ContentResult我们可以实现“静态文本的动态化”,也就是说我们可以在某个Action中根据当前的请求动态地生成一些文本(比如CSS样式),而这些文本内容原本是定义在静态文本文件中。
在接下来的这个实例演示中,我们将利用ContentResult实现对界面主题的定制。实现的机制非常简单:让一个返回类型为ContentResult的Action方法返回基于当前主题的CSS样式,而当前的主题通过一个可持久化的Cookie保存下来。我们在一个ASP.NET MVC应用中定义了如下一个HomeController,其Action方法Css返回一个表示CSS样式的ContentResult。在该Action方法中,我们从请求中提取表示主题的Cookie,并根据它生成基于当前主题的CSS样式(这里仅仅设置了字体类型和大小)。
1: public class HomeController : Controller
2: {
3: //其他成员
4: public ActionResult Css()
5: {
6: HttpCookie cookie = Request.Cookies["theme"] ?? new HttpCookie("theme","default");
7: switch (cookie.Value)
8: {
9: case "Theme1": return Content("body{font-family: SimHei; font-size:1.2em}", "text/css");
10: case "Theme2": return Content("body{font-family: KaiTi; font-size:1.2em}", "text/css");
11: default: return Content("body{font-family: SimSong; font-size:1.2em}", "text/css");
12: }
13: }
14: }
我们在HomeController中定义了如下两个Index方法,无参的Index方法(针对HTTP-GET请求)从预定义Cookie中提取当前的主题(如果没有则采用默认的主题default)并以ViewBag的形式传递给View。另一个应用HttpPostAttribute特性的Index方法中接收用户提交的主题名称并设置为响应的Cookie,同样通过ViewBag的形式 保存当前的主题名称。两个Index方法最终都将默认的View呈现出来。
1: public class HomeController : Controller
2: {
3: //其他成员
4: public ActionResult Index()
5: {
6: HttpCookie cookie = Request.Cookies["theme"] ?? new HttpCookie("theme", "default");
7: ViewBag.Theme = cookie.Value;
8: return View();
9: }
10:
11: [HttpPost]
12: public ActionResult Index(string theme)
13: {
14: HttpCookie cookie = new HttpCookie("theme", theme);
15: cookie.Expires = DateTime.MaxValue;
16: Response.SetCookie(cookie);
17: ViewBag.Theme = theme;
18: return View();
19: }
20: }
通过Css方法 的定义看出我们定义了三个主题(Theme1、Theme2和Default),它们采用不同的中文字体(黑体、楷体和宋体)。Action方法Index对应View具有如下一个表单,该表单中为这三种主题添加了相应的RadioButton使用户可以对主题进行定制。这个View最核心的部分是用于引用CSS文件的<link>元素,可以看到它的href属性指向的地址正是对应着HomeController的Action方法Css,也就是说最终用于控制页面样式的CSS是通过调用该Action得到的。
1: <html>
2: <head>
3: <title>主题设置</title>
4: <link type="text/css" rel="Stylesheet" href="@Url.Action("Css")" />
5: </head>
6: <body>
7: @using(Html.BeginForm())
8: {
9: string theme = ViewBag.Theme.ToString();
10: @Html.RadioButton("theme", "Default", theme == "Default")<span>默认主题(宋体)</span><br/>
11: @Html.RadioButton("theme", "Theme1", theme == "Theme1")<span>主题1(黑体)</span><br/>
12: @Html.RadioButton("theme", "Theme2", theme == "Theme2")<span>主题2(楷体)</span><br />
13: <input type="submit" value="保存" />
14: }
15: </body>
16: </html>
现在我们直接运行我们的程序,并在出现的“主题设置”界面中设置不同的主题,界面的样式(字体)将会根据我们选择的主题而动态改变,具体的显示效果如下图所示。
了解ASP.NET MVC几种ActionResult的本质:EmptyResult & ContentResult
了解ASP.NET MVC几种ActionResult的本质:FileResult
了解ASP.NET MVC几种ActionResult的本质:JavaScriptResult & JsonResult
了解ASP.NET MVC几种ActionResult的本质:HttpStatusCodeResult & RedirectResult/RedirectToRouteResult