詳解ASP.NET MVC的請求生命周期
本文的目的旨在詳細描述ASP.NET MVC請求從開始到結(jié)束的每一個過程。我希望能理解在瀏覽器輸入URL并敲擊回車來請求一個ASP.NET MVC網(wǎng)站的頁面之后發(fā)生的任何事情。
為什么需要關(guān)心這些?有兩個原因。首先是因為ASP.NET MVC是一個擴展性非常強的框架。例如,我們可以插入不同的ViewEngine來控制網(wǎng)站內(nèi)容呈現(xiàn)的方式。我們還可以定義控制器生成和分配到某個請求的方式。因為我想發(fā)掘任何ASP.NET MVC頁面請求的擴展點,所以我要來探究請求過程中的一些步驟。
其次,如果你對測試驅(qū)動開發(fā)佷感興趣,當為控制器寫單元測試時,我們就必須理解控制器的依賴項。在寫測試的時候,我們需要使用諸如Typemock Isolator或Rhino Mocks的Mock框架來模擬某些對象。如果不了解頁面請求生命周期就不能進行有效的模擬。
生命周期步驟概覽
當我們對ASP.NET MVC網(wǎng)站發(fā)出一個請求的時候,會發(fā)生5個主要步驟:
步驟1:創(chuàng)建RouteTable
當ASP.NET應(yīng)用程序***次啟動的時候才會發(fā)生***步。RouteTable把URL映射到Handler。
步驟2:UrlRoutingModule攔截請求
第二步在我們發(fā)起請求的時候發(fā)生。UrlRoutingModule攔截了每一個請求并且創(chuàng)建和執(zhí)行合適的Handler。
步驟3:執(zhí)行MvcHandler
MvcHandler創(chuàng)建了控制器,并且把控制器傳入ControllerContext,然后執(zhí)行控制器。
步驟4:執(zhí)行控制器
控制器檢測要執(zhí)行的控制器方法,構(gòu)建參數(shù)列表并且執(zhí)行方法。
步驟5:調(diào)用RenderView方法
大多數(shù)情況下,控制器方法調(diào)用RenderView()來把內(nèi)容呈現(xiàn)回瀏覽器。Controller.RenderView()方法把這個工作委托給某個ViewEngine來做。
現(xiàn)在讓我們來詳細研究每一個步驟:
步驟1:創(chuàng)建RouteTable
當我們請求普通ASP.NET應(yīng)用程序頁面的時候,對于每一個頁面請求都會在磁盤上有這樣一個頁面。例如,如果我們請求一個叫做SomePage.aspx的頁面,在WEB服務(wù)器上就會有一個叫做SomePage.aspx的頁面。如果沒有的話,會得到一個錯誤。
從技術(shù)角度說,ASP.NET頁面代表一個類,并且不是普通類。ASP.NET頁面是一個Handler。換句話說,ASP.NET頁面實現(xiàn)了IhttpHandler接口并且有一個ProcessRequest()方法用于在請求頁面的時候接受請求。ProcessRequest()方法負責(zé)生成內(nèi)容并把它發(fā)回瀏覽器。
因此,普通ASP.NET應(yīng)用程序的工作方式佷簡單明了。我們請求頁面,頁面請求對應(yīng)磁盤上的某個頁面,這個頁面執(zhí)行ProcessRequest()方法并把內(nèi)容發(fā)回瀏覽器。
ASP.NET MVC應(yīng)用程序不是以這種方式工作的。當我們請求一個ASP.NET MVC應(yīng)用程序的頁面時,在磁盤上不存在對應(yīng)請求的頁面。而是,請求被路由轉(zhuǎn)到一個叫做控制器的類上。控制器負責(zé)生成內(nèi)容并把它發(fā)回瀏覽器。
當我們寫普通ASP.NET應(yīng)用程序的時候,會創(chuàng)建很多頁面。在URL和頁面之間總是一一對應(yīng)進行映射。每一個頁面請求對應(yīng)相應(yīng)的頁面。
相反,當我們創(chuàng)建ASP.NET MVC應(yīng)用程序的時候,創(chuàng)建的是一批控制器。使用控制器的優(yōu)勢是可以在URL和頁面之間可以有多對一的映射。例如,所有如下的URL都可以映射到相同的控制器上。
http://MySite/Products/1 http://MySite/Products/2 http://MySite/Products/3 |
這些URL映射到一個控制器上,通過從URL中提取產(chǎn)品ID來顯示正確的產(chǎn)品。這種控制器方式比傳統(tǒng)的ASP.NET方式更靈活。控制器方式可以產(chǎn)品更顯而易見的URL。
那么,某個頁面請求是怎么路由到某個控制器上的呢?ASP.NET MVC應(yīng)用程序有一個叫做路由表(Route Table)的東西。路由表映射某個URL到某個控制器上。
一個應(yīng)用程序有一個并且只會有一個路由表。路由表在Global.asax文件中創(chuàng)建。清單1包含了在使用Visual Studio新建ASP.NET MVC Web應(yīng)用程序時默認的Global.asax文件。
清單 1 – Global.asax
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace TestMVCArch
{
public class GlobalApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
// Note: Change the URL to "{controller}.mvc/{action}/{id}" to enable
// automatic support on IIS6 and IIS7 classic mode
routes.Add(new Route("{controller}/{action}/{id}", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new { action = "Index", id = "" }),
});
routes.Add(new Route("Default.aspx", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }),
});
}
protected void Application_Start(object sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);
}
}
}
#p#
應(yīng)用程序的路由表由RouteTable.Routes的靜態(tài)屬性表示。這個屬性表示了路由對象的集合。在清單1列出的Global.asax文件中,我們在應(yīng)用程序***啟動時為路由表增加兩個路由對象(Application_Start()方法在***次請求網(wǎng)站頁面的時候被調(diào)用一次)。
路由對象負責(zé)把URL映射到Handler。在清單1中,我們創(chuàng)建了兩個路由對象。這2個對象都把URL映射到MvcRouteHandler。***個路由映射任何符合{controller}/{action}/{id}模式的URL到MvcRouteHandler。第二個路由映射某個URL Default.aspx到MvcRouteHandler。
順便說一下,這種新的路由構(gòu)架可以脫離ASP.NET MVC獨立使用。Global.asax文件映射URL到MvcRouteHandler。然而,我們可以選擇把URL路由到不同類型的Handler上。這里說的路由構(gòu)架包含在一個叫做System.Web.Routing.dll的獨立程序集中。我們可以脫離MVC使用路由。
步驟2:UrlRoutingModule攔截請求
當我們對ASP.NET MVC應(yīng)用程序發(fā)起請求的時候,請求會被UrlRoutingModule HTTP Module攔截。HTTP Module是特殊類型的類,它參與每一次頁面請求。例如,傳統(tǒng)ASP.NET包含了FormsAuthenticationModule HTTP Module用來使用表單驗證實現(xiàn)頁面訪問安全性。
UrlRoutingModule攔截請求后做的***件事情就是包裝當前的HttpContext為HttpContextWrapper2對象。HttpContextWrapper2類和派生自HttpContextBase的普通HttpContext類不同。創(chuàng)建的HttpContext的包裝可以使使用諸如Typemock Isolator或Rhino Mocks的Mock對象框進行模擬變得更簡單。
接著,Module把包裝后的HttpContext傳給在之前步驟中創(chuàng)建的RouteTable。HttpContext包含了URL、表單參數(shù)、查詢字符串參數(shù)以及和當前請求關(guān)聯(lián)的cookie。如果在當前請求和路由表中的路由對象之間能找到匹配,就會返回路由對象。
如果UrlRoutingModule成功獲取了RouteData對象,Module然后就會創(chuàng)建表示當前HttpContext和RouteData的RouteContext對象。Module然后實例化基于RouteTable的新HttpHandler,并且把RouteContext傳給Handler的構(gòu)造函數(shù)。
對于ASP.NET MVC應(yīng)用程序,從RouteTable返回的Handler總是MvcHandler(MvcRouteHandler返回MvcHandler)。只要UrlRoutingModule匹配當前請求到路由表中的路由,就會實例化帶有當前RouteContext的MvcHandler。
Module進行的***一步就是把MvcHandler設(shè)置為當前的HTPP Handler。ASP.NET應(yīng)用程序自動調(diào)用當前HTTP Handler的ProcessRequest()方法然后轉(zhuǎn)入下一步。
步驟3:執(zhí)行MvcHandler
在之前的步驟中,表示某個RouteContext的MvcHandler被設(shè)置作為當前的HTTP Handler。ASP.NET應(yīng)用程總是會發(fā)起一系列的事件,包括Star、BeginRequest、PostResolveRequestCache、 PostMapRequestHandler、PreRequestHandlerExecute和EndRequest事件(非常多的應(yīng)用程序事件——對于完整列表,請查閱Visual Studio 2008文檔中的HttpApplication類)。
之前內(nèi)容中描述的所有東西都在PostResolveRequestCache和PostMapRequestHandler中發(fā)生。當前HTTP Handler的ProcessRequest()方法在PreRequestHandlerExecute事件之后被調(diào)用。
當之前內(nèi)容中創(chuàng)建的MvcHandler對象的ProcessRequest()被調(diào)用的時候,會創(chuàng)建一個新的控制器。控制器由ControllerFactory創(chuàng)建。由于我們可以創(chuàng)建自己的ControllerFactory,所以這又是一個可擴展點。默認的ControllerFactory名字相當合適,叫做DefaultControllerFactory。
RequestContext以及控制器的名字被傳入ControllerFactory.CreateController()方法來獲得一個控制器。然后,從RequestContext和控制器構(gòu)造ControllerContext對象。***,調(diào)用控制器類的Execute()方法。在調(diào)用Execute()方法的時候會給方法傳入ControllerContext。
步驟4:執(zhí)行控制器
Execute()方法首先創(chuàng)建TempData對象(在Ruby On Rails中叫做Flash對象)。TempData可以用于保存下次請求必須的臨時數(shù)據(jù)(TempData和會話狀態(tài)差不多,不長期占用內(nèi)存)。
接著,Execute()方法構(gòu)建請求的參數(shù)列表。這些參數(shù)從請求參數(shù)中提取,將會被作為方法的參數(shù)。參數(shù)會被傳入執(zhí)行的控制器方法。
Execute()通過對控制器類進行反射來找到控制器的方法。控制器類是我們寫的。Execute()方法找到了我們控制器類中的方法后就執(zhí)行它。Execute()方法不會執(zhí)行被裝飾NonAction特性的方法。
至此,就進入了自己應(yīng)用程序的代碼。
步驟5:調(diào)用RenderView方法
通常,我們的控制器方法***會調(diào)用RenderView()或RedirectToAction()方法。RenderView()方法負責(zé)把視圖(頁面)呈現(xiàn)給瀏覽器。
當我們調(diào)用控制器RenderView()方法的時候,調(diào)用會委托給當前ViewEngine的RenderView()方法。ViewEngine是另外一個擴展點。默認的ViewEngine是WebFormViewEngine。然而,我們可以使用諸如Nhaml的其它ViewEngine。
WebForm的ViewEngine.RenderView()方法創(chuàng)建了一個叫做ViewLocator的類來尋找視圖。然后,它使用BuildManager來創(chuàng)建ViewPage類的實例。然后,如果頁面有ViewData就會設(shè)置ViewData。***,ViewPage 的RenderView()方法被調(diào)用。
ViewPage類從System.Web.UI.Page基類(和用于傳統(tǒng)ASP.NET的頁面一樣)派生。RenderView()方法做的***一個工作就是調(diào)用頁面類的ProcessRequest()。調(diào)用視圖的ProcessRequest()生成內(nèi)容的方式和普通ASP.NET頁面生成內(nèi)容的方式一致。
可擴展點
ASP.NET MVC生命周期在設(shè)計的時候包含了很多可擴展點。我們可以自定義通過插入自定義類或覆蓋既有類來自定義框架的行為。下面是這些擴展點的概要:
路由對象:當我們創(chuàng)建路由表的時候,調(diào)用RouteCollection.Add()方法來增加新的路由對象。Add()方法接受了RouteBase對象。我們可以通過派生RouteBase基類來實現(xiàn)自己的路由對象。
MvcRouteHandler :當創(chuàng)建MVC應(yīng)用程序的時候,我們把URL映射到MvcRouteHandler對象上。然而,我們可以把URL映射到實現(xiàn)IRouteHandler接口的任何類上。路由類的構(gòu)造函數(shù)接受任何實現(xiàn)IRouteHandler接口的對象。
MvcRouteHandler.GetHttpHandler() : MvcRouteHandler 類的GetHttpHandler()方法是virtual方法。默認情況下,MvcRouteHandler返回MvcHandler。如果愿意的話,我們可以覆蓋GetHttpHandler()方法來返回不同的Handler。
ControllerFactory :我們可以通過System.Web.MVC.ControllerBuilder.Current.SetControllerFactory()方法指定一個自定義類來創(chuàng)建自定義的控制器工廠。控制器工廠負責(zé)為某個控制器名和RequestContext返回控制器。
控制器:我們可以通過實現(xiàn)Icontroller接口來實現(xiàn)自定義控制器。這個接口只有一個Execute(ControllerContext controllerContext)方法。
ViewEngine:我們可以為控制器指定自定義的ViewEngine。通過為公共的Controller.ViewEngine屬性指定ViewEngine來把ViewEngine指定給控制器。ViewEngine必須實現(xiàn)IviewEngine接口,接口只有一個方法:RenderView(ViewContext viewContext)。
ViewLocator :ViewLocator把視圖名映射到實際視圖文件上。我們可以通過WebFormViewEngine.ViewLocator的屬性來執(zhí)行自定義的ViewLocator。
【編輯推薦】