了解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實現主題定製
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。
上麵我們談到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使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對象,那麼?
實際上對於一個非Null的返回值,ActionInvoker采用這樣的方式來創建相應的ActionResult:。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: }
我們可以通過一個簡單的實例來驗證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("https://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的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
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-25 17:03:59