定义在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: });