一、路由
1、HttpApplication中的ASP.NET MVC
.Net 3.5 引入了System.Web.Routing程序集,通過Url Routing的機制,可以實現將一個虛擬路徑的請求映射到一個Action方法上。
在Asp.net MVC中,Route類指定Asp.net應用程序中針對虛擬路徑請求的處理方式,可以為每種URL模式創建一個Route對象。Route類定義如下:
public class Route : RouteBase
為了完成針對請求的路由工作,在Asp.net MVC中引入了稱為路由表的數據結構來定義各種URL到實際處理程序之間的映射。在Asp.net MVC中,這個路由表的類型為RouteTable。RouteTable的Routes是一個static類型的屬性,它的類型是RouteCollection,從類的命名就可以看出,這是一個強類型的Route對象集合,用來表示應用程序中所有的路由。
public class RouteTable { public static RouteCollection Routes { get; } }
為了在HttpApplication的處理管道中將普通的請求處理轉換到MVC的處理中,為了將URL映射到MVC的處理程序中,UrlRoutingModule注冊了HttpApplication的如下兩個事件,使得請求進入Asp.net MVC的處理中。
- PostResolveRequestCache事件
- PostMapRequestHandler事件
2、創建RouteTable
一個網站應用程序只會有一個路由表,針對請求的路由表必須在請求真正被處理之前提前創建,默認情況下,創建路由表的工作將在Global.asax.cs文件中的Application_Start中完成。
Asp.net MVC 3中的默認Global.asax.cs:
public class MvcApplication : System.Web.HttpApplication { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); } public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( "Default", // 路由名稱 "{controller}/{action}/{id}", // 帶有參數的 URL new { controller = "Home", action = "Index", id = UrlParameter.Optional } // 參數默認值 ); } protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); } }
應用程序使用的路由表由RouteTable表示。RouteTable的Routes屬性表示了路由對象的集合。
路由表使用RouteTable表示,RouteTable的Routes屬性表示了路由對象的集合。在上邊的Global.asax.cs文件中,我們在應用程序首次處理請求之前未路由表增加了兩個路由對象。
在路由表內部通過路由對象來表示URL到Handler的映射。在上面的代碼中,我們創建了兩個路由對象。
- 第一個路由,這是一個用於忽略特殊請求的路由,這個路由忽略所有擴展名為.axd的請求,這些請求將被按照經典的方式進行處理。這樣,經典的一些特殊醒醒的請求,如Trace.axd,WebResource.axd等將被路由忽略,還會按照經典的方式進行處理。
- 第二個路由,映射任何符合{controller}/{action}/{id}模式的UTL到MvcRouteHandler。其中第二個路由還提供了一個默認的參數。
一般來說,對於ASP.NET MVC網站程序,一般不會請求有着.aspx擴展名的地址,而會請求一個有意義的虛擬地址,ASP.NET MVC通過路由表,將這個請求轉發到一個叫做控制器的類上,控制器負責生成內容並把它發回瀏覽器。
3、在IIS 6.0和IIS 7中的配置
- 在IIS 6.0中,需要通過請求的擴展名將不同的請求映射到不同的應用程序擴展中,例如,對於經典的ASP.NET網站,我們需要將.aspx擴展名映射到aspnet_isapi.dll中。這個映射在我們安裝.Net的時候,就已經由安裝程序設置到IIS的應用程序配置中了。對於我們剛剛創建的有意義的URL來說,地址中根本沒有擴展名,對於這些特殊的請求,我們可以通過通配符應用程序映射將所有的請求都映射到.Net網站應用程序中來。
- 在IIS 7中,網站應用程序被分為兩種類型運行模式:經典模式和集成模式。在應用程序池配置界面中可以對運行模式進行調整的。在集成模式下,.Net網站可以參與到IIS處理過程中,對於MVC項目來說,不需要進行文件擴展名的映射配置,就可以將請求傳遞到.Net網站應用程序中。IIS7中默認使用的是集成管線模式,默認情況下經典模式也被支持,所以就有兩套擴展名與處理程序的映射配置:一套用於經典模式,一套用於集成模式。
4、從URL到Route
在ASP.NET MVC中,從URL到RouteData的映射通過Route對象表示,需要首先在RouteTable中注冊Route信息,RouteTable中保存了當前應用程序的路由信息。具體來說,RouteTable的靜態屬性Routes包含了當前應用程序用於從URL映射到處理請求的所有路由信息。添加到路由表中的路由順序非常重要,應用程序將會從前往后地在路由表中查找匹配當前請求的路由對象,所以,短的、特殊的路由應該加入到路由表的前方,一般化的路由應該在后面加入。當應用程序啟動的時候,我們需要將所有的映射添加到路由表中,這個工作一般在Global.asax中進行。
所有的Route必須派生自基類RouteBase,其定義如下:
public abstract class RouteBase {protected RouteBase(); public abstract RouteData GetRouteData(HttpContextBase httpContext); public abstract VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values); }
每個路由對象必須能夠通過GetRouteData獲取對應的路由數據,而GetVirtualPath用來檢測請求參數,查看當前的路由對象是否匹配這個請求。
在Route中,實際的路由數據被以RouteData的類型保存。
public class RouteData { public RouteData(); public RouteData(RouteBase route, IRouteHandler routeHandler); public RouteValueDictionary DataTokens { get; } public RouteBase Route { get; set; } public IRouteHandler RouteHandler { get; set; } public RouteValueDictionary Values { get; } public string GetRequiredString(string valueName); }
我們使用的Route派生自RouteBase,提供了多種不同的構造函數,使得我們可以以不同的方式來構建Route對象。
- URL請求參數的模板
- routeHandler這個請求的處理器對象
- 默認的處理參數
另外,Constraints屬性提供了約束URL的信息,用於限制URL的范圍。
通過RouteCollection的MapRoute擴展方法能夠加入一個路由,這個方法提供了6個重載來方便我們添加路由。使用這種方式加入的路由將會使用一個默認的路由處理程序MvcRouteHandler來處理路由。
不使用MapRoute,而是自定義一個Route對象,將會允許我們有更大的靈活性。
RouteTable.Routes.Add( new Route( "{controller}/{action}/{id}", new RouteValueDictionary( new{ Action = "Index", id = (string)null }), new ShowRouteHandler() ) );
在大型網站中,往往存在眾多的Controller,從ASP.NET 2.0開始,提供了Area用來對大型網站的支持。Area用於對Controller進行邏輯分組,這個問題也同樣通過Route進行了映射,這時候的URL如下所示:
"AreaName/{controller}/{action}/{id}"
在ASP.NET MVC中通過在Route中增加一個Area屬性來解決這個問題,這個屬性通過接口IRouteWithArea來指定。
namespace System.Web.Mvc{ using Systen; public interface IRouteWithArea{ string Atra{ get; } } }
不過也可以來自DataTokens中的area來表示Area名稱。
DataTokens["area"]
含有Area的網站,默認生成了一個名為XXXXAreaRegistration.cs的代碼文件,用於注冊含有Area的URL映射。注冊含有Area的路由如下所示,這個類派生自AreaRegistration,通過重寫AreaName提供了當前Area的名字,這樣,以后就可以通過這個名字來匹配Area,然后在這個Area中查詢相應的控制器。
public class BlogAreaRegistration : AreaRegistration { public override string AreaName { get{ return "Blog"; } }
public override void RegisterArea(AreaRegistrationContext context) { context.MapRoute("Blog_default","Blog/{controller}/{action}/{id}",new { action = "Index",id = UrlParameter.Optional }) } }
在Global.asax中增加了注冊含有Area的路由:
protected void Application_Start() { //注意順序不能顛倒 //先定義了帶有區域的路由 AreaRegistration.RegisterAllAreas(); //后注冊沒有區域的路由 RegisterRoutes(RouteTable.Routes); }
我們也可以通過MapRoute提供一個默認值,當URL中沒有提供這部分信息的時候,將使用默認值作為當前的值。
routes.MapRoute( "Default", // 路由名稱 "{controller}/{action}/{id}", // 帶有參數的 URL new { controller = "Home", action = "Index", id = UrlParameter.Optional } // 參數默認值 );
如,當沒有輸入Controller將使用Home代替,Action=>Index,id=>""。
5、Routing
當PostMapRequestHandler事件觸發的時候,ASP.NET已經完成了經典的獲取處理程序的操作。但是,顯然不可能通過傳統方式獲取ASP.NET MVC的處理程序,UrlRoutingModule將檢查通過HttpContext對象的Items傳遞的路由對象,如果成功獲取的話,那么,將通過路由對象的處理程序來重新設置當前的處理程序。
對於ASP.NET MVC程序來說,將會創建一個實現IRouteHandler接口的對象,這個對象將被用來作為處理程序使用。不過,此時的處理程序並不像ASP.NET時代,直接完成處理請求,而是用於獲取一個真正的處理程序,定義在命名空間Systen.Web.Routing下的接口IRouteHandler用於完成這個任務。
public interface IRouteHandler { IHttpHandler GetHttpHandler(RequestContext requestContext); }
在ASP.NET MVC中,默認的處理對象如下:
public class MvcRouteHandler : IRouteHandler
默認情況下,MvcRouteHandler將會創建一個MvcHandler對象開始處理過程。
public class MvcHandler : IHttpAsyncHandler,IHttpHandler,IRequiresSessionState
如果在創建路由表的時候不使用MapRoute,而是通過創建Route對象的方法,那么,可以自行指定特定的路由處理程序。
6、RequestContext的前世今生
從ASP.NET 3.5SP1開始,在ASP.NET中定義了一組新的類型,以增強對於測試的支持。這組新的類型通過分別提供一個抽象的基類,使得我們可以簡單的創建一個用於測試的派生類來完成測試工作。
表示請求參數的基類HttpRequestBase定義了經典的HttpRequest同樣的成員,但它現在是一個抽象類,允許我們繼承
public abstract class HttpRequestBase
在ASP.NET MVC使用的是它的派生類HttpRequestWrapper
public class HttpRequestWrapper : HttpRequestBase
類似的其他對象如下表所示:
經典類型 | MVC抽象基類 | 實現 |
HttpRequest | HttpRequestBase | HttpRequestWrapper |
HttpResponse | HttpResponseBase | HttpResponseWrapper |
HttpApplicationState | HttpApplicationStateBase | HttpApplicationStateWrapper |
HttpServerUtility | HttpServerUtilityBase | HttpServerUtilityWrapper |
HttpSessionState | HttpSessionStateBase | HttpSessionStateWrapper |
HttpContext | HttpContextBase | HttpContextWrapper |
需要注意的是,在ASP.NET MVC中,經過Routing之后,相比經典的ASP.NET模式,增加了Routing信息,經典的HttpContext中沒有Routing信息,RequestContext在HttpContext的基礎上,增加了當前的Routing數據。
public class RequestContext { public HttpContextBase HttpContext{ get; internal set;} public RouteData RouteData { get; internal set; } }
二、控制器
在默認情況下,MvcRouteHandler是標准的路由處理程序,這個處理程序將會創建一個MvcHandler的對象實例,在構造函數中,將會把當前的請求參數對象傳遞給這個實際的處理對象來處理當前的請求,MvcHandler是一個標准的處理程序,但是,它唯一的構造函數需要一個RequestContext類型的參數,所以,並不能被注冊到網站的處理程序列表中。
在MVC模式下,將會通過IControllerFactory接口的對象來獲取當前請求的控制器對象。
namespace System.Web.Mvc{ using Sustem.Web.Routing; public interface IControllerFactory{ IController CreateController(RequestContext requestContext,string controllerName); void ReleaseController(IController controller); } }
實現IControllerFactory接口的對象是控制器的創建工廠,這個工廠通過ControllerBuilder提供給MvcHandler使用。ControllerBuilder的Current屬性獲取當前的ControllerBuild對象實例,這個類提供了兩個方法用戶設置或者獲取當前的控制器工廠。
public void SetControllerFactory(IControllerFactory controllerFactory) public IControllerFactory GetControllerFactory()
1、控制器工廠
控制器工廠必須實現接口IControllerFactory,其定義在System.Web.Mvc下:
using System.Web.Routing namespace System.Web.Mvc{ public interface IControllerFactory{ IController CreateController(RequestContext requestContext,string controllerName); void ReleaseController(IController controller); } }
2、使用自定義的控制器工廠
在MVC中獲得控制器工廠的方式是借助於ControllerBuilder,這個類使用典型的單例模式創建,我們可以通過其Current屬性獲取這個唯一對象的引用。
默認情況下,它將會通過DefaultControllerFactory來創建控制器對象,通常情況下,
- ControllerBuilder.Current.GetControllerFactory方法來獲取當前的Controller工廠對象實例
- ControllerBuilder.Current.SetControllerFactory方法來設置自定義的Controller工廠
3、為Controller類傳遞構造函數的參數
默認情況下,Controller類需要提供默認構造函數,因為DefaultControllerFactory將會通過反射來創建Controller對象的實例。如果我們定義的Controller需要構造函數來創建,或者通過某個IOC的容器來管理Controller,那么可以通過自定義的ControllerFactoru來實現。
4、Controller的繼承關系
IController是一個非常簡單的接口,僅僅定義了一個方法Execute,用來完成針對請求的處理。
using System.Web.Routing; namespace System.Web.Mvc { public interface IController { void Execute(RequestContext requestContext); } }
在MVC中,首先使用ControllerBase實現了IController,並實現了基本的處理邏輯,而真正的處理方法ExecuteCore留到了派生類Controller實現出來。ControllerBase中對接口Execute的實現如下:
protected virtual void Execute(RequestContext requestContext) { if (requestContext == null) { throw new ArgumentNullException("requestContext"); } VerifyExecuteCalledOnce(); Initialize(requestContext); ExecuteCore(); } protected abstract void ExecuteCore();
在派生類Controller中,通過ActionInvoker屬性的InvokeAction方法,實現最終對於Action的調用。這個ActionInvoker實際上是一個ControllerActionInvoker對象實例。
protected override void ExecuteCore() { PossiblyLoadTempData(); try{ string actionName = RouteData.GetRequiredString("action"); if(!ActionInvoker.InvokeAction(ControllerContext,actionName)) { HandleUnknownAction(actionName); } } finally{ PossiblySaveTempData(); } }