瞭解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>
現在我們直接執行我們的程式,並在出現的“主題設定”介面中設定不同的主題,介面的樣式(字型)將會根據我們選擇的主題而動態改變,具體的顯示效果如下圖所示。