ASP.NET MVC路由擴展:路由映射
上周我寫了三篇文章(一、二、三)詳細地介紹了ASP.NET的路由係統。ASP.NET的路由係統旨在通過注冊URL模板與物理文件之間的映射進而實現請求地址與文件路徑之間的分離,但是對於ASP.NET MVC應用來說,請求的目標不再是一個具體的物理文件,而是定義在某個Controller類型中的Action方法。出於自身路由特點的需要,ASP.NET對ASP.NET的路由係統進行了相應的擴展。
目錄
一、基本路由映射
二、實例演示:注冊路由映射與查看路由信息
三、基於Area的路由映射
1、AreaRegistration與AreaRegistrationContext
2、AreaRegistration的緩存
3、實例演示:查看基於Area路由信息
通過前麵的介紹我們知道基於某個物理文件的路由映射通過調用代表全局路由表的RouteTable的靜態屬性Routes(一個RouteCollection對象)的MapPageRoute方法來完成,為了實現針對目標Controller和Action的路由,ASP.NET MVC針對RouteCollection類型定義了一係列的擴展方法以實現文件路徑無關的路由映射,這些擴展方法定義在RouteCollectionExtensions類型中。如下麵的代碼片斷所示,RouteCollectionExtensions定義了兩組方法,方法IgnoreRoute用於注冊不需要進行路由的URL模板,對應於RouteCollectionExtensions的Ignore方法;仿佛MapRoute用於進行基於URL模板的路由注冊,對應於RouteCollectionExtensions的MapPageRoute方法。
1: public static class RouteCollectionExtensions
2: {
3: //其他成員
4: public static void IgnoreRoute(this RouteCollection routes, string url);
5: public static void IgnoreRoute(this RouteCollection routes, string url, object constraints);
6:
7: public static Route MapRoute(this RouteCollection routes, string name, string url);
8: public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults);
9: public static Route MapRoute(this RouteCollection routes, string name, string url, string[] namespaces);
10: public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints);
11: public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, string[] namespaces);
12: public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces);
13: }
由於ASP.NET MVC的路由注冊與具體的物理文件無關,所以MapRoute方法中。與直接定義在RouteCollectionExtensions中的Ignore和MapPageRoute方法不同的是,。這主要是為了編程上的便利,使得我們可以通過匿名類型的方式來指定這兩個參數值。該方法在內部會通過反射的方式得到指定對象所有屬性值,並轉換為RouteValueDictionary對象,其屬性名和屬性值作為字典元素的Key和Value。
對於ASP.NET MVC來說,最終需要通過在請求地址中指定的Controller名稱來創建具體的Controller實例。由於Controller名稱 僅僅對應著類型的名稱,Controller的成功實例化的前提是我們能夠正確地解析出它的具體類型,所以我們需要使用了命名空間。在調用MapRoute方法的時候我們可以通過字符串數組類型的參數namespaces來指定一個命名空間的列表。對於注冊的命名空間,可以指定一個代表完整命名空間的字符串,也可以使用“”作為通配符。
。MapRoute方法沒有為初始化Route對象的DataTokens屬性提供相應的參數,如果沒有指定命名空間列表,所有通過該方法添加的Route對象的DataTokens屬性總是一個空的RouteValueDictionary對象。
對於針對定義在某個Controller中的某個Action的請求,如果注冊的路由表與之匹配,具體匹配的某個路由對象的GetRouteData被調用並返回一個具體的RouteData對象。根據對請求地址進行解析得到的目標Controller和Action的名稱必須包含在該RouteData的Values屬性對應的RouteValueDictionary對象中,其對應的Key分別為和。
ASP.NET MVC通過定義在RouteCollectionExtensions中的擴展方法MapRoute進行路由映射,為了讓讀者對此有一個深刻的認識,我們來進行一個簡單的實例演示。我們依然沿用之前關於獲取天氣信息的場景,看看通過這種方式進行注冊的Route對象針對匹配的HTTP請求返回怎樣的RouteData對象。[源代碼從這裏下載]
我們在創建的ASP.NET Web應用(不是ASP.NET MVC應用)添加一個Web頁麵(Default.aspx),並按照之前的方式以內聯代碼的方式直接將RouteData的相關屬性顯示出來,頁麵主體部分的HTML如下所示。需要注意的是我們顯示的RouteData是從定義的方法GetRouteData方法獲取的,而不是對應於當前頁麵的RouteData屬性。
1: <body>
2: <form id="form1" runat="server">
3: <div>
4: <table>
5: <tr>
6: <td>Route:</td>
7: <td><%=GetRouteData().Route != null? GetRouteData().Route.GetType().FullName:"" %></td>
8: </tr>
9: <tr>
10: <td>RouteHandler:</td>
11: <td><%=GetRouteData().RouteHandler != null? GetRouteData().RouteHandler.GetType().FullName:"" %></td>
12: </tr>
13: <tr>
14: <td>Values:</td>
15: <td>
16: <ul>
17: <%foreach (var variable in GetRouteData().Values)
18: {%>
19: <li><%=variable.Key%>=<%=variable.Value%></li>
20: <% }%>
21: </ul>
22: </td>
23: </tr>
24: <tr>
25: <td>DataTokens:</td>
26: <td>
27: <ul>
28: <%foreach (var variable in GetRouteData().DataTokens)
29: {%>
30: <li><%=variable.Key%>=<%=variable.Value%></li>
31: <% }%>
32: </ul>
33: </td>
34: </tr>
35: </table>
36: </div>
37: </form>
38: </body>
我們將GetRouteData方法定義在當前頁麵的後台代碼中。如下麵的代碼片斷所示,我們手工創建了一個HttpRequest和HttpResponse對象,HttpRequest的請求的地址為“”(3721是本Web應用對應的端口號)。根據這兩個對象創建了HttpContext對象,並以此創建一個HttpContextWrapper對象。最終我們將其作為參數調用RouteTable的Routes屬性的GetRouteData方法並返回。這個方法實際上就是模擬注冊的路由表針對相對地址為“/0512/3”的HTTP請求的路由處理。
1: public partial class Default : System.Web.UI.Page
2: {
3: private RouteData routeData;
4: public RouteData GetRouteData()
5: {
6: if (null != routeData)
7: {
8: return routeData;
9: }
10: HttpRequest request = new HttpRequest("default.aspx", "https://localhost:3721/0512/3", null);
11: HttpResponse response = new HttpResponse(new StringWriter());
12: HttpContext context = new HttpContext(request, response);
13: HttpContextBase contextWrapper = new HttpContextWrapper(context);
14: return routeData = RouteTable.Routes.GetRouteData(contextWrapper);
15: }
16: }
具體的路由映射依然定義在添加的Global.asax文件中。如下麵的代碼片斷所示,我們通過調用RouteTable的Routes屬性的MapRoute方法注冊了一個采用“”作為URL模板的路由對象,並指定了默認變量、約束和命名空間列表。
1: public class Global : System.Web.HttpApplication
2: {
3: protected void Application_Start(object sender, EventArgs e)
4: {
5: object defaults = new { areacode = "010", days = 2, defaultCity="BeiJing", defaultDays=2};
6: object constraints = new { areacode = @"0\d{2,3}", days = @"[1-3]{1}"};
7: string[] namespaces = new string[] { "Artech.Web.Mvc", "Artech.Web.Mvc.Html" };
8: RouteTable.Routes.MapRoute("default", "{areacode}/{days}", defaults, constraints, namespaces);
9: }
10: }
如果我們現在在瀏覽器中訪問Default.aspx頁麵,會得到下圖所示的結果,從中我們可以得到一些有用的信息:
- 與調用RouteCollection的MapPateRoute方法進行路由映射不同的是,這個得到的RouteData對象的RouteHandler屬性是一個System.Web.Mvc.MvcRouteHandler對象。
- 在MapRoute方法中通過defaults參數指定的兩個與URL匹配無關的變量(defaultCity=BeiJing;defaultDays=2)體現在RouteData的Values屬性中。這意味著如果我們沒有在URL模板中為Controller和Action的名稱定義相應的變量({controller}和{action}),也可以將它們定義成默認變量。
- DataTokens屬性中包含一個Key值為Namespaces值為字符數組的元素,我們不難猜出它對應著我們指定的命名空間列表。
對於一個較大規模的Web應用,我們可以從功能上通過Area將其劃分為較小的單元。每個Area相當於一個獨立的子係統,具有一套包含Models、Views和Controller在內的目錄結構和配置文件。一般來說,每個Area具有各自的路由規則(URL模版上一般會體現Area的名稱),而基於Area的路由映射通過AreaRegistration進行注冊。
AreaRegistration與AreaRegistrationContext
基於Area的路由映射通過AreaRegistration進行注冊。如下麵的代碼片斷所示,AreaRegistration是一個抽象類,抽象隻讀屬性AreaName返回當前Area的名稱,而抽象方法RegisterArea用於實現基於當前Area的路由注冊。
1: public abstract class AreaRegistration
2: {
3: public static void RegisterAllAreas();
4: public static void RegisterAllAreas(object state);
5:
6: public abstract void RegisterArea(AreaRegistrationContext context);
7: public abstract string AreaName { get; }
8: }
AreaRegistration定義了兩個抽象的靜態RegisterAllAreas方法重載,參數state用於傳遞給具體AreaRegistration的數據。當RegisterAllArea方法執行的時候,它先,通過反射得到所有實現了接口IController的類型,並通過反射創建相應的AreaRegistration對象。對於每個AreaRegistration對象,一個AreaRegistrationContext對象被創建出來並作為參數調用它們的RegisterArea方法。
如下麵的代碼片斷所示,AreaRegistrationContext的隻讀屬性AreaName表示Area的名稱,屬性Routes是一個代表路由表的RouteCollection對象,而State是一個用戶自定義對象,它們均通過構造函數進行初始化。具體來說,對於最初通過調用AreaRegistration的靜態方法RegisterAllAreas創建的AreaRegistrationContext對象,AreaName來源於當前AreaRegistration對象的同名屬性,Routes則對應著RouteTable的靜態屬性Routes表示的全局路由表,而在調用RegisterAllAreas方法指定的參數(state)作為AreaRegistrationContext對象的State參數。
1: public class AreaRegistrationContext
2: {
3: public AreaRegistrationContext(string areaName, RouteCollection routes);
4: public AreaRegistrationContext(string areaName, RouteCollection routes, object state);
5:
6: public Route MapRoute(string name, string url);
7: public Route MapRoute(string name, string url, object defaults);
8: public Route MapRoute(string name, string url, string[] namespaces);
9: public Route MapRoute(string name, string url, object defaults, object constraints);
10: public Route MapRoute(string name, string url, object defaults, string[] namespaces);
11: public Route MapRoute(string name, string url, object defaults, object constraints, string[] namespaces);
12:
13: public string AreaName { get; }
14: public RouteCollection Routes { get; }
15: public object State { get; }
16: public ICollection<string> Namespaces { get; }
17: }
AreaRegistrationContext的隻讀屬性Namespaces表示一組優先匹配的命名空間(當多個同名的Controller類型定義在不同的命名空間中)。當針對某個具體AreaRegistration的AreaRegistrationContext被創建的時候,。換言之,。
AreaRegistrationContext定義了一係列的MapRoute用於進行路由映射注冊,方法的使用以及參數的含義與定義在RouteCollectionExtensions類型中的同名擴展方法一致。在這裏需要特別指出的是,。
當我們通過Visual Studio的ASP.NET MVC項目模版創建一個Web應用的時候,在的Global.asax文件中會生成如下的代碼通過調用AreaRegistration的靜態方法RegisterAllAreas實現對所有Area的注冊,也就是說針對所有Area的注冊發生在應用啟動的時候。
1: public class MvcApplication : System.Web.HttpApplication
2: {
3: protected void Application_Start()
4: {
5: AreaRegistration.RegisterAllAreas();
6: }
7: }
AreaRegistration的緩存
Area的注冊(主要是基於Area的路由映射注冊)通過具體的AreaRegistration來實現。在應用啟動的時候,為了實現對所有Area的注冊,需要遍曆通過調用BuildManager的靜態方法GetReferencedAssemblies方法得到的程序集列表,並通過從中找到所有AreaRegistration類型。如果一個應用涉及到太多的程序集,這個過程可能會耗費很多時間,為了提供性能,基於AreaRegistration類型列表的緩存被采用。
注:BuildManager的靜態方法GetReferencedAssemblies返回所有頁編譯都必須引用的程序集引用的列表,這包括包含 Web.config 文件的<system.web>/<compilation>/<assemblies>配置節中指定的用於編譯Web應用所使用的程序集和從 App_Code 目錄中的自定義代碼生成的程序集以及其他頂級文件夾中的程序集。
ASP.NET MVC對AreaRegistration類型列表的緩存是基於文件的。具體來說,當通過程序集加載和反射得到了所有的AreaRegistration類型列表後,會將其進行序列化並被保存為一個XML物理文件,這個名為MVC-AreaRegistrationTypeCache.xml的XML文件被存放在ASP.NET的臨時目錄下,具體的路徑如下。其中第一個針對寄宿於IIS中的Web應用,後者針對直接通過Visual Studio Developer Server作為宿主的應用。
- %Windir%\Microsoft.NET\Framework\v{version}\Temporary ASP.NET Files\{appname}\...\...\UserCache\
- %Windir%\Microsoft.NET\Framework\v{version}\Temporary ASP.NET Files\root\...\...\UserCache\
下麵的XML片斷體現了這個作為所有AreaRegistration類型緩存的XML文件的結構,從中我們可以看到所有的AreaRegistration類型名稱,連同它所在的托管模塊和程序集名稱都被保存了下來。當調用AreaRegistration的靜態方法RegisterAllAreas被調用之後,係統會試圖加載該文件,如果該文件存在並且具有期望的結構,那麼將不在通過程序集加載和反射來解析AreaRegistration的類型,而是直接對文件內容進行反序列化從而得到所有AreaRegistration類型的列表。
1: <?xml version="1.0" encoding="utf-8"?>
2: <!--This file is automatically generated. Please do not modify the contents of this file.-->
3: <typeCache lastModified="3/22/2012 2:58:47 PM" mvcVersionId="80365b23-7a1d-42b2-9e7d-cc6f5694c6d1">
4: <assembly name="Artech.Admin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
5: <module versionId="07be22a1-781d-4ade-bd22-34b0850445ef">
6: <type>Artech.Admin.AdminAreaRegistration</type>
7: </module>
8: </assembly>
9: <assembly name="Artech.Portal, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
10: <module versionId="7b0490d4-427e-43cb-8cb5-ac1292bd4976">
11: <type>Artech.Portal.PortalAreaRegistration</type>
12: </module>
13: </assembly>
14: </typeCache>
實例演示:查看基於Area路由信息
通過AreaRegistration實現的針對Area的路由注冊具有一些特殊的細節差異,我們通過實例演示的方式來說明。我們直接使用前麵創建的演示實例,並在項目中創建一個自定義的WeatherAreaRegistration。如下麵的代碼片斷所示,WeatherAreaRegistration繼承自抽象基類AreaRegistration,表示Area名稱的AreaName屬性返回“Weahter”。在實現路由注冊的RegisterArea方法中我們調用AreaRegistrationContext對象的MapRoute方法注冊了一個URL模版為“"的路由對象。默認變量值、約束也被相應地提供。[源代碼從這裏下載]
1: public class WeatherAreaRegistration : AreaRegistration
2: {
3: public override string AreaName
4: {
5: get { return "Weather"; }
6: }
7: public override void RegisterArea(AreaRegistrationContext context)
8: {
9: object defaults = new { areacode = "010", days = 2, defaultCity = "BeiJing", defaultDays = 2 };
10: object constraints = new { areacode = @"0\d{2,3}", days = @"[1-3]{1}" };
11: context.MapRoute("weatherDefault", "weather/{areacode}/{days}", defaults, constraints);
12: }
13: }
我們在Global.asax的Application_Start方法中按照如下的方式調用AreaRegistration的靜態方法RegisterAllAreas實現對所有Area的注冊。按照我們在上麵介紹的Area注冊原理,對於第一次RegisterAllAreas方法的調用,會自動加載所有引用的程序集來獲取所有的AreaRegistration(當然就包括我們上麵定義的WeatherAreaRegistration),最後通過反射創建相應的對象並調用RegisterArea方法。
1: public class Global : System.Web.HttpApplication
2: {
3: protected void Application_Start(object sender, EventArgs e)
4: {
5: AreaRegistration.RegisterAllAreas();
6: }
7: }
對於定義在Default.aspx頁麵後台代碼中用於進行路由匹配和獲取路由信息的GetRouteData方法中,我們對創建的HttpRequest對象略加修改,使請求地址符合通過WeatherAreaRegistration注冊的路由規則()。
1: public partial class Default : System.Web.UI.Page
2: {
3: private RouteData routeData;
4: public RouteData GetRouteData()
5: {
6: if (null != routeData)
7: {
8: return routeData;
9: }
10: HttpRequest request = new HttpRequest("default.aspx", "https://localhost:3721/weather/0512/3", null);
11: HttpResponse response = new HttpResponse(new StringWriter());
12: HttpContext context = new HttpContext(request, response);
13: HttpContextBase contextWrapper = new HttpContextWrapper(context);
14: return routeData = RouteTable.Routes.GetRouteData(contextWrapper);
15: }
16: }
在瀏覽器中訪問Default.aspx頁麵,我們會得到如圖2-10所示的結果。通過AreaRegistration注冊的路由對象得到的RouteData的不同之處主要反映在其DataTokens屬性上。如下圖所示,除了表示命名空間列表的元素,DataTokens屬性表示的RouteValueDictionary還具有兩個額外的元素,其中一個Key為“”的元素代表Area的名稱,另一個Key為“”的元素具有一個布爾類型的值表示是否需要使用後備的命名空間來解析Controller的類型。
如果調用AreaRegistrationContext的MapRoute方法是顯式指定了命名空間,或者說對應的AreaRegistration定義在某個命名空間下,這個名稱為“UseNamespaceFallback”的DataToken元素的值為False;反之為True。進一步來說,。也就是說,後者是前者的一個後備,前者具有更高的優先級。
AreaRegistration類型所示在命名空間也不說直接作為最終RouteData的DataTokens中的命名空間,而是在此基礎上加上“”後綴。如果對本實例得到得到包含RouteData的DataTokens集合中的命名空間,你會發現其值為“WebApp.*”(WebApp是定義WeatherAreaRegistration的命名空間)。
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-26 12:04:07