1. 程式人生 > >(轉)從零開始學習ASP.NET MVC(三) Controller/Action 深入解析和應用

(轉)從零開始學習ASP.NET MVC(三) Controller/Action 深入解析和應用

一.摘要

一個Url請求經過了Routing處理後會呼叫Controller的Action方法. 中間的過程是怎樣的? Action方法中返回ActionResult物件後,如何到達View的? 本文將講解Controller的基本用法,  深入分析Controller的執行機制, 並且提供了建立所有型別Action的程式碼. 值得學習ASP.NET MVC時參考.

二.承上啟下

在上一篇文章中, 我已經學會了如何使用Routing獲取Controller和Action, 隨後的程式會呼叫Controller中的Action方法.

每個Action方法都要返回一個ActionResult物件. 一個Action會將資料傳遞給View,如圖:

image

三.Controller與Action的作用

1.職責

Controller負責將獲取Model資料並將Model傳遞給View物件.通知View物件顯示.

2.ASP.NET MVC中的Controller和Action

在ASP.NET MVC中, 一個Controller可以包含多個Action. 每一個Action都是一個方法, 返回一個ActionResult例項.

ActionResult類包括ExecuteResult方法, 當ActionResult物件返回後會執行此方法.

下面分層次的總結Controller 處理流程:

1. 頁面處理流程

傳送請求 –> UrlRoutingModule捕獲請求 –> MvcRouteHandler.GetHttpHandler() –> MvcHandler.ProcessRequest()

2.MvcHandler.ProcessRequest() 處理流程:

使用工廠方法獲取具體的Controller –> Controller.Execute() –> 釋放Controller物件

3.Controller.Execute() 處理流程

獲取Action –> 呼叫Action方法獲取返回的ActionResult –> 呼叫ActionResult.ExecuteResult() 方法

4.ActionResult.ExecuteResult() 處理流程

獲取IView物件-> 根據IView物件中的頁面路徑獲取Page類-> 呼叫IView.RenderView()

方法(內部呼叫Page.RenderView方法)

通過對MVC原始碼的分析,我們瞭解到Controller物件的職責是傳遞資料,獲取View物件(實現了IView介面的類),通知View物件顯示.

View物件的作用是顯示.雖然顯示的方法RenderView()是由Controller呼叫的,但是Controller僅僅是一個"指揮官"的作用, 具體的顯示邏輯仍然在View物件中.

需要注意IView介面與具體的ViewPage之間的聯絡.在Controller和View之間還存在著IView物件.對於ASP.NET程式提供了WebFormView物件實現了IView介面.WebFormView負責根據虛擬目錄獲取具體的Page類,然後呼叫Page.RenderView().

四.ActionResult解析

通過上面的流程,我們知道了ActionResult物件在整個流程中的作用.ActionResult是一個抽象類, 在Action中返回的都是其派生類.下面是我整理的RC2版本中提供的ActionResult派生類:

類名 抽象類 父類 功能
ContentResult 根據內容的型別和編碼,資料內容.
EmptyResult 空方法.
FileResult abstract 寫入檔案內容,具體的寫入方式在派生類中.
FileContentResult FileResult 通過 檔案byte[] 寫入檔案.
FilePathResult FileResult 通過 檔案路徑 寫入檔案.
FileStreamResult FileResult 通過 檔案Stream 寫入檔案.
HttpUnauthorizedResult 丟擲401錯誤
JavaScriptResult 返回javascript檔案
JsonResult 返回Json格式的資料
RedirectResult 使用Response.Redirect重定向頁面
RedirectToRouteResult 根據Route規則重定向頁面
ViewResultBase abstract 呼叫IView.Render()
PartialViewResult ViewResultBase 呼叫父類ViewResultBase 的ExecuteResult方法.
重寫了父類的FindView方法.
尋找使用者控制元件.ascx檔案
ViewResult ViewResultBase 呼叫父類ViewResultBase 的ExecuteResult方法.
重寫了父類的FindView方法.
尋找頁面.aspx檔案

目前ASP.NET MVC還沒有提供官方的ActionResult列表.上面的列表是我在原始碼中分析得出的.有些解釋的可能不夠清楚,請諒解.

下面我將列舉各個ActionResult的例項.

五.例項應用

1.新增Controller

安裝了ASP.NET MVC後, 在專案上點選右鍵會找到新增Controller項:

image

2.新增Action

下面這個類提供了返回各種型別的ActionResult的Action例項:

public class DemoController : Controller
{

    /// <summary>
    /// http://localhost:1847/Demo/ContentResultDemo
    /// </summary>
    /// <returns></returns>
    public ActionResult ContentResultDemo()
    {
        string contentString = "ContextResultDemo!";
        return Content(contentString);
    }

    /// <summary>
    /// http://localhost:1847/Demo/EmptyResultDemo
    /// </summary>
    /// <returns></returns>
    public ActionResult EmptyResultDemo()
    {
        return new EmptyResult();
    }

    /// <summary>
    /// http://localhost:1847/Demo/FileContentResultDemo
    /// </summary>
    /// <returns></returns>
    public ActionResult FileContentResultDemo()
    {
        FileStream fs = new FileStream(Server.MapPath(@"/resource/Images/1.gif"), FileMode.Open, FileAccess.Read);
        byte[] buffer = new byte[Convert.ToInt32(fs.Length)];
        fs.Read(buffer, 0, Convert.ToInt32(fs.Length));
        return File(buffer, @"image/gif");
    }

    /// <summary>
    /// http://localhost:1847/Demo/FilePathResultDemo
    /// </summary>
    /// <returns></returns>
    public ActionResult FilePathResultDemo()
    {
        //可以將一個jpg格式的影象輸出為gif格式
        return File(Server.MapPath(@"/resource/Images/2.jpg"), @"image/gif");
    }

    /// <summary>
    /// http://localhost:1847/Demo/FileStreamResultDemo
    /// </summary>
    /// <returns></returns>
    public ActionResult FileStreamResultDemo()
    {
        FileStream fs = new FileStream(Server.MapPath(@"/resource/Images/1.gif"), FileMode.Open, FileAccess.Read);
        return File(fs, @"image/gif");
    }

    /// <summary>
    /// http://localhost:1847/Demo/HttpUnauthorizedResultDemo
    /// </summary>
    /// <returns></returns>
    public ActionResult HttpUnauthorizedResultDemo()
    {
        return new HttpUnauthorizedResult();
    }

    /// <summary>
    /// http://localhost:1847/Demo/JavaScriptResultDemo
    /// </summary>
    /// <returns></returns>
    public ActionResult JavaScriptResultDemo()
    {
        return JavaScript(@"alert(""Test JavaScriptResultDemo!"")");
    }

    /// <summary>
    /// http://localhost:1847/Demo/JsonResultDemo
    /// </summary>
    /// <returns></returns>
    public ActionResult JsonResultDemo()
    {
        var tempObj = new { Controller = "DemoController", Action = "JsonResultDemo" };
        return Json(tempObj);
    }

    /// <summary>
    /// http://localhost:1847/Demo/RedirectToRouteResultDemo
    /// </summary>
    /// <returns></returns>
    public ActionResult RedirectToRouteResultDemo()
    {
        return RedirectToAction(@"FileStreamResultDemo");
    }

    /// <summary>
    /// http://localhost:1847/Demo/PartialViewResultDemo
    /// </summary>
    /// <returns></returns>
    public ActionResult PartialViewResultDemo()
    {
        return PartialView();
    }

    /// <summary>
    /// http://localhost:1847/Demo/RedirectToRouteResultDemo
    /// </summary>
    /// <returns></returns>
    public ActionResult ViewResultDemo()
    {
        //如果沒有傳入View名稱, 預設尋找與Action名稱相同的View頁面.
        return View();
    }

}

在文章最後提供有完整例項程式碼下載.

六.Controller 深入分析

在研究Controller/Action的流程過程中, 發現了ASP.NET MVC一些問題.

1.Routing元件與MVC框架的結合

Routing元件和ASP.NET MVC並不是一個專案, 在ASP.NET MVC中僅僅是使用了Routing元件, 在原始碼中是通過dll的方式引用的.Routing元件已經包含在.net framework 3.5 sp1中了.而ASP.NET MVC還未出正式版.

那麼ASP.NET MVC是如何應用Routing元件的呢?

Routing元件獲取了Url中的資料後, 會將資料儲存在一個 RouteData 物件中.並將請求傳遞給一個實現了IRouteHandler介面的物件. 在Asp.net MVC中提供的MvcRouteHandler類實現了此介面, Routing 將請求傳遞給MvcRouteHandler的GetHttpHandler方法.下面是原始碼:

IRouteHandler介面:

    public interface IRouteHandler
    {
        IHttpHandler GetHttpHandler(RequestContext requestContext);
    }

MvcRouteHandler類:

    public class MvcRouteHandler : IRouteHandler {
        protected virtual IHttpHandler GetHttpHandler(RequestContext requestContext) {
            return new MvcHandler(requestContext);
        }

        #region IRouteHandler Members
        IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext) {
            return GetHttpHandler(requestContext);
        }
        #endregion
    }

曾經我認為IRouteHandler是多餘的, 用IHttpHandler就夠了. 現在知道了為何要定義這個介面. 主要是為了傳遞RouteData物件.GetHttpHandler方法需要一個RequestContext 物件.RequestContext 是 System.Web.Routing程式集中的類, 裡面除了處理請求需要的HttpContextBase物件,還包括了一個RouteData物件.

RequestContext類:

 public class RequestContext
    {
        public RequestContext(HttpContextBase httpContext, RouteData routeData);
        public HttpContextBase HttpContext { get; }
        public RouteData RouteData { get; }
    }

Routing元件在Web.Config中註冊了一個HttpModule: System.Web.Routing.UrlRoutingModule, 而不是HttpHandler:

<add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>

可惜看不到這個類的原始碼. 所有請求最後都是要傳遞給IHttpHandler物件處理, 主要的工作是編譯頁面, 所以我猜測這個Module將請求截獲後通過IRouteHandler介面物件獲取一個HttpHandler, 然後將處理移交給獲取到的HttpHandler.

ASP.NET MVC 中實現了IHttpHandler介面的類是MvcHandler, MvcRouteHandler.GetHttpHandler方法就是返回一個MvcHandler物件. MvcHandler類的建構函式需要傳入一個RequestContext物件. 實現的IHttpHandler介面方法處理過程中都需要依賴這個物件.

但是微軟在這裡的處理有一些不足. MvcHandler雖然實現了IHttpHandler介面但是不能被當作IHttpHandler介面使用. 因為IHttpHandler中沒有定義RequestContext屬性, 如果一個MvcHandler物件此屬性沒有賦值則會出錯, 也沒有將預設的無引數建構函式設定為private, 所以理論上可以很隨意的例項化一個MvcHandler而不為其RequestContext屬性賦值.

IRouteHandler想實現的語意是: 返回一個具有RequestContext屬性的IHttpHandler物件.

但是最後的實現結果是: 提供"返回IHttpHandler物件"的方法,  此方法接收RequestContext物件引數.

還需要注意ControllerContext類. 在Controller的處理過程中使用此物件作為儲存上下文資料的容器.下面是這幾個類的包含關係:

image

可以看到在ControllerContext中包含了RequestContext物件,但是又將RequestContext物件中的兩個屬性提取到自己的類中.如果僅僅是為了使用方便而這麼做, 個人認為不是一個好的設計.資料物件的儲存職責也應該明確,使用ControllerContext.RequestContext.RouteData 的方式更容易被人理解.

PS:這種方式類似於方法內聯.對於屬性JIT為了效率會幫助我們做內聯.而僅僅是為了使用方便.

2.IView 與 View物件的關係

所以從系統的角度上看, 實現了IView介面的物件才是View.

但是從實現效果上看, 具體的aspx或者ascx頁面才是View.

當第一次看到IView介面時我認為它應該是"View角色"需要實現的介面. 但是結果並不是這樣.

在我們的系統中View物件應該是aspx或者ascx檔案. 而且並不是所有的ActionResult都需要找到aspx或者ascx檔案, 事實上只有PartialViewResult 和 ViewResult 才會去尋找View物件.其他的ActionResult要麼是返回檔案, 要麼是跳轉等等.

那麼兩者的關係到底是怎樣的? 其實其中的過程需要牽扯到這幾個介面和類:

IViewEngine, ViewEngineResult, ViewEngineCollection

ViewEngine是View引擎, ViewEngineCollection是一個引擎集合,裡面儲存了各種尋找View的引擎.但是在目前的原始碼中只有WebFormViewEngine : VirtualPathProviderViewEngine : IViewEngine

這一系列WebForm使用的引擎.引擎的作用有兩個:

1.尋找Page/使用者控制元件的路徑

2.根據路徑建立IView物件.也就是根據頁面的物理檔案建立IView介面物件.

而且目前實現了IView介面的物件也只有一個:

WebFormView

WebFormViewEngine 根據頁面路徑, 將一個頁面地址轉化為一個WebFormView物件,也就是一個IView介面物件.

至此IView介面和Page頁面類仍然沒有任何關係, IView物件只是儲存了頁面的物理路徑.

接著在IView的Render事件中,根據物理路徑建立了一個頁面的object例項,注意看這一段程式碼:

  object viewInstance = BuildManager.CreateInstanceFromVirtualPath(ViewPath, typeof(object));
            if (viewInstance == null) {
                throw new InvalidOperationException(
                    String.Format(
                        CultureInfo.CurrentUICulture,
                        MvcResources.WebFormViewEngine_ViewCouldNotBeCreated,
                        ViewPath));
            }

            ViewPage viewPage = viewInstance as ViewPage;
            if (viewPage != null) {
                RenderViewPage(viewContext, viewPage);
                return;
            }

            ViewUserControl viewUserControl = viewInstance as ViewUserControl;
            if (viewUserControl != null) {
                RenderViewUserControl(viewContext, viewUserControl);
                return;
            }

viewInstance 就是通過物理路徑建立的頁面物件.但是他的型別是object, 而且程式嘗試將其分別轉化為ViewPage物件和ViewUserControl物件.

我想很多人都看到了這裡的設計不足.現在我們只能"約定": 所有的MVC中的頁面物件都必須繼承自ViewPage或者ViewUserControl類, 否則程式就會出錯.產生這種不足的原因就是IView介面和ViewPage沒有任何的耦合性, 完全是硬編碼進去的.

為什麼不讓頁面直接實現IView介面? 然後嘗試將頁面轉化為IView介面物件, 而不是ViewPage, 這樣才是好的設計. 其實微軟知道什麼是好的設計, 我猜測他們遇到的困難是Page物件和IView介面的衝突. 因為兩者都需要Render. 如果在IView中定義自己的Render名稱, 那就意味著ASP.NET MVC開發小組要自己處理頁面的顯示邏輯, 而現在ASP.NET WebForm模式下面的頁面顯示引擎又不能複用, 重新開發自己的一套顯示引擎成本又太大, 才出此下策.

以上只是猜測.這種設計的缺陷雖然可以接受, 但是真的是讓我好幾天陷入了看不懂程式碼的痛苦之中.還好, 現在可以解脫了.

七.如何在MVC專案中使用MVC原始碼專案

另外在為了跟蹤實現過程, 我將ASP.NET MVC的原始碼專案新增到了例項專案中, 其中有一些需要注意的地方:

1. 將例項專案中的System.Web.Mvc引用刪除, 改成專案引用.

2. 需要在Web.Config中註釋掉程式集引用:

<compilation debug="true">
            <assemblies>
                <add assembly="System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
                <add assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
                <add assembly="System.Web.Abstractions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
                <add assembly="System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
                <!-- <add assembly="System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>-->
                <add assembly="System.Data.DataSetExtensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
                <add assembly="System.Xml.Linq, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
                <add assembly="System.Data.Linq, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
            </assemblies>

註釋掉的程式集存在於GAC中, 但是我們現在不希望使用GAC中的程式集, 而是引用專案.

3. 將View目錄下的Web.Config中的所有System.Web.Mvc相關的 PublicKeyToken 都修改為 null:

<pages
        validateRequest="false"
        pageParserFilterType="System.Web.Mvc.ViewTypeParserFilter, System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
        pageBaseType="System.Web.Mvc.ViewPage, System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
        userControlBaseType="System.Web.Mvc.ViewUserControl, System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
      <controls>
        <add assembly="System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" namespace="System.Web.Mvc" tagPrefix="mvc" />
      </controls>
    </pages>

八.總結

首先很抱歉在本系列文章開篇時承諾的每日一篇僅僅堅持了2天.具體原因就不解釋了.這篇文章的出爐歷時半個月, 並且經歷了ASP.NET MVC版本從RC到RC2的演變. 在檢視MVC原始碼上花費了大量的時間, 希望付出的努力能夠為大家研究學習ASP.NET MVC帶來幫助. 我也會把這一系列的文章寫完, 關於ASP.NET MVC還有太多的地方沒有學習.

例項原始碼下載地址:

http://files.cnblogs.com/zhangziqiu/Asp.net-MVC-3-Demo.rar

Tag標籤: ASP.NET MVC,MVC,Controller,Routing,Action