文章內容
上個章節我們講到了,可以在HttpModules初始化之前動態新增Route的方式來自定義自己的HttpHandler,最終接管請求的,那MVC是這麼實現的麼?本章節我們就來分析一下相關的MVC原始碼來驗證一下我們的這個問題。
先建立一個MVC3的Web Application,選擇預設的模板以便建立以後就預設包含HomeController和AccountController。我們知道MVC要先接管請求才能通過這些Controller來處理,那我們先去Global.asax.cs檔案裡看程式碼(定義接管請求要在初始化HttpModule之前,所以只能到這裡來找程式碼(或者是利用WebActivator之類的特性來動態新增),Global.asax.cs檔案裡程式碼很少,但是有我們需要的東西,首先在Application_Start的方法裡發現一行程式碼:
RegisterRoutes(RouteTable.Routes);
這行程式碼,看呼叫的方法名稱RegisterRoutes是註冊Route的意思,但是為什麼引數卻是全域性的RouteTable.Routes集合呢?找到RegisterRoutes方法來看看具體的內容:
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults ); }
該方法有2行程式碼,第一行是忽略一個Route(我們先不看這個),第二行是使用MapRoute方法註冊一個新的Route,預設是對映到Home Controller的Index Action上,我們可能想到了,RouteCollection(也就是剛才傳入的RouteTable.Routes)的MapRoute方法就是提供我們所說的接管請求的入口,但是如何把MVC自己的HttpHandler傳進去的呢?我們Go to一下這個MapRoute方法(需要安裝ReShaper來查詢MVC的原始碼),調整到了MVC的RouteCollectionExtensions類,發現MapRoute並不是RouteCollection自帶的方法,而是在MVC原始碼裡提供的一個擴充套件方法,程式碼如下:
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces) { if (routes == null) { throw new ArgumentNullException("routes"); } if (url == null) { throw new ArgumentNullException("url"); } Route route = new Route(url, new MvcRouteHandler()) { Defaults = new RouteValueDictionary(defaults), Constraints = new RouteValueDictionary(constraints), DataTokens = new RouteValueDictionary() }; if ((namespaces != null) && (namespaces.Length > 0)) { route.DataTokens["Namespaces"] = namespaces; } routes.Add(name, route); return route; }
該程式碼的主要作用是new一個新的Route,然後將該Route新增到我們剛才提到的靜態集合RouteTable.Routes裡,以便後期查詢Handler的時候使用,OK,這一步符合我們前面章節的分析。
接下來看,Route是如何new出來的,程式碼裡的引數傳入的分別是我們知道的url,以及一個MVCRouteHandler的例項,這一步也符合我們前面的分析,那我們來看一下MVCRouteHandler的GetHttpHandler方法是如何實現的獲取MVCHandler的:
public class MvcRouteHandler : IRouteHandler { private IControllerFactory _controllerFactory; public MvcRouteHandler() { } public MvcRouteHandler(IControllerFactory controllerFactory) { _controllerFactory = controllerFactory; } protected virtual IHttpHandler GetHttpHandler(RequestContext requestContext) { requestContext.HttpContext.SetSessionStateBehavior(GetSessionStateBehavior(requestContext)); return new MvcHandler(requestContext); } protected virtual SessionStateBehavior GetSessionStateBehavior(RequestContext requestContext) { string controllerName = (string)requestContext.RouteData.Values["controller"]; IControllerFactory controllerFactory = _controllerFactory ?? ControllerBuilder.Current.GetControllerFactory(); return controllerFactory.GetControllerSessionBehavior(requestContext, controllerName); } #region IRouteHandler Members IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext) { return GetHttpHandler(requestContext); } #endregion }
看以上的粗體程式碼,MvcRouteHandler在實現了IRouteHandler的GetHttpHandler,該方法呼叫了MvcRouteHandler自身定義的GetHttpHandler虛方法,而在這個虛方法裡我們看到了一個非常重要而又期待已久的程式碼——返回MvcHandler的例項,大概看一下MvcHandler這個類,得到它就是我們所猜想的:繼承於IHttpHandler介面的一個類,並且也繼承了 IHttpAsyncHandler介面,我們先不管MvcHandler內部是如何實現的,但我們前面幾章節的全部分析終於得到了驗證,也就說在這裡得到了Mvc的專用處理Handler,然後呼叫它的BeginProcessRequest方法進入Mvc自身的Pipeline進行處理了。
至此,我們終於弄明白了Mvc在整個ASP.NET Runtime是如何接管請求的了,也應該大概清楚整個ASP.NET Runtime的執行機制了,至於MvcHandler的實現方式,我們會在後面的很多章節逐一給大家分析每行程式碼,今天我們還有一個小任務,那就是:看完了Mvc的實現機制,我們能否自己來寫一個自定義的HttpHandler通過Route動態註冊進去,來實現我們自己的自定義擴充套件,我們來嘗試著做一下吧。
第一步:建立HttpHandler類
public class TomHandler : IHttpHandler { public TomHandler(RequestContext requestContext) { // do nothing } public virtual void ProcessRequest(HttpContext context) { string url = context.Request.Url.AbsoluteUri; context.Response.Write("當前地址為:" + url); context.Response.End(); // 這裡我們什麼都不做,只輸出URL地址 } public virtual bool IsReusable { get { return false; } } }
第二步:建立RouteHandler類
public class TomRouteHandler : IRouteHandler { IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext) { return new TomHandler(requestContext); } }
在GetHttpHandler實現裡,返回我們定義的TomHandler例項。
第三步:註冊我們的Route和RouteHandler
protected void Application_Start(object sender, EventArgs e) { Route route = new Route("tom/{other}", new TomRouteHandler()); RouteTable.Routes.Add(route); }
我們設定成,只要訪問tom資料夾下的任意檔案或者子目錄,都提示該URL。下面是我的測試結果:
訪問:Http//localhost/tom/
結果:沒有提示我們預想的結果(原因是不符合我們的規則)
訪問:Http//localhost/tom/123/
結果:輸出正常(說明TomHandler已經接管了該請求)
訪問:Http//localhost/tom/123.aspx?id=123
結果:輸出正常(也說明TomHandler已經接管了該請求)
在建立真實的tom資料夾,然後在裡面建立一個 index.html檔案(內容為123),然後訪問Http//localhost/tom/index.html,規則符合我們的Route定義,但輸出結果卻不是我們預期的結果,而是123,怎麼回事?還記得上一章節裡談到的RouteCollection的GetRouteData方法麼?該方法是先判斷URL對應的檔案是否真實存在,如果存在就直接輸出,如果不存在就再來找RouteData的資料,這就解釋了上面的index.html路徑為什麼不是我們期望結果的原因了吧?。
注:如果你建立一個index.aspx檔案,並在index.aspx.cs檔案裡寫Response.Write程式碼輸出123的話,該檔案也會按照aspx頁面的正常週期來執行(也就是說輸出123字串),如果你在<system.web>裡的httpHandlers節點用remove命令把*.aspx的匹配設定去掉,那結果就只會輸出index.aspx這個檔案裡的字串了(包括裡面內嵌的任何C#程式碼)。
最後總結了,到這裡,我們已經知道了MvcHandler是如何接管請求的了,而且自己也做了一個簡單的例子來驗證這套機制。重新回顧一下前面的這麼多篇文章,我們應該大概對ASP.NET RunTime, Pipeline以及ASP.NET MVC切入點應該有個整體的瞭解了。
同步與推薦
本文已同步至目錄索引:MVC之前的那點事兒系列
MVC之前的那點事兒系列文章,包括了原創,翻譯,轉載等各型別的文章,如果對你有用,請推薦支援一把,給大叔寫作的動力。