ASP.NET的路由系統:路由對映
總的來說,我們可以通過RouteTable的靜態屬性Routes得到一個基於應用的全域性路由表,通過上面的介紹我們知道這是一個型別的RouteCollection的集合物件,我們可以通過呼叫它的MapPageRoute進行路由對映,即註冊URL模板與某個物理檔案的匹配關係。路由註冊的核心就是在全域性路由表中新增一個Route物件,該物件的絕大部分屬性都可以通過MapPageRoute方法的相關引數來指定。接下來我們通過實現演示的方式來說明路由註冊的一些細節問題。
目錄
一、變數預設值
二、約束
三、對現成檔案的路由
四、註冊路由忽略地址
五、直接新增路由物件
我們已前面介紹的關於獲取天氣預報資訊的路由地址,我們在建立的ASP.NET Web應用中建立一個Weather.aspx頁面,不過我們並不打算在該頁面中呈現任何天氣資訊,而是將基於該頁面的路由資訊打印出來。該頁面主體部分的HTML如下所示,我們不僅將基於當前頁面的RouteData物件的Route和RouteHandler屬性型別輸出來,還將儲存於Values和DataTokens字典的變數顯示出來。
1: <body>
2: <form id="form1" runat="server">
3:<div>
4: <table>
5: <tr>
6: <td>Route:</td>
7: <td><%=RouteData.Route != null? RouteData.Route.GetType().FullName:"" %></td>
8: </tr>
9: <tr>
10: <td>RouteHandler:</td>
11: <td><%=RouteData.RouteHandler != null? RouteData.RouteHandler.GetType().FullName:"" %></td>
12: </tr>
13: <tr>
14: <td>Values:</td>
15: <td>
16: <ul>
17: <%foreach (var variable in RouteData.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 RouteData.DataTokens)
29: {%>
30: <li><%=variable.Key%>=<%=variable.Value%></li>
31: <% }%>
32: </ul>
33: </td>
34: </tr>
35: </table>
36: </div>
37: </form>
38: </body>
在新增的Global.asax檔案中,我們將路由註冊操作定義在Application_Start方法中。如下面的程式碼片斷所示,對映到weather.aspx頁面的URL模板為{areacode}/{days}。在呼叫MapPageRoute方法的時候,我們還為定義在URL模板的兩個變數定義了預設值以及正則表示式。除此之外,我們還在註冊的路由物件上附加了兩個變數,表示對變數預設值的說明(defaultCity:BeiJing;defaultDays:2)。
1: public class Global : System.Web.HttpApplication
2: {
3: protected void Application_Start(object sender, EventArgs e)
4: {
5: var defaults = new RouteValueDictionary { { "areacode", "010" }, { "days", 2 }};
6: var constaints = new RouteValueDictionary { { "areacode", @"0\d{2,3}" }, { "days", @"[1-3]{1}" } };
7: var dataTokens = new RouteValueDictionary { { "defaultCity", "BeiJing" }, { "defaultDays", 2 } };
8:
9: RouteTable.Routes.MapPageRoute("default", "{areacode}/{days}", "~/weather.aspx", false, defaults, constaints, dataTokens);
10: }
11: }
一、變數預設值
由於我們為定義在URL模板中表示區號和天數的變數定義了預設值(areacode:010;days:2),如果我們希望返回北京地區未來兩天的天氣,可以直接訪問應用根地址,也可以只指定具體區號,或者同時指定區號和天數。如下圖所示,當我們在瀏覽器位址列中輸入上述三種不同的URL會得到相同的輸出結果。
從下圖所示的路由資訊我們可以看到,預設情況下RouteData的Route屬性型別正是Route,而RouteHandler屬性則一個是PageRouteHandler物件,我們會在本章後續部分對PageRouteHandler進行詳細介紹。通過地址解析出來的變數被儲存數Values屬性中,而在進行路由註冊過程為Route物件DataTokens屬性定義的變數被轉移到了RouteData的同名屬性中。[例項原始碼下載]
二、約束
我們以電話區號代表對應的城市,為了確保使用者在的請求地址中提供有效的區號,我們通過正則表示式(“0\d{2,3}”)對其進行了約束。此外,我們只能提供未來3天以內的天氣情況,我們同樣通過正則表示式(“[1-3]{1}”)是對請求地址中表示天數的變數進行了約束。如果請求地址中的內容不能符合相關變數段的約束條件,則意味著對應的路由物件與之不匹配。
對於本例來說,由於我們只註冊了唯一的路由物件,如果請求地址不能滿足我們定義的約束條件,則意味著找不到一個具體目標檔案,會返回404錯誤。如下圖所示,由於在請求地址中指定了不合法的區號(01)和天數(4),我們直接在瀏覽器介面上得到一個HTTP 404錯誤。
對於約束,除了可以通過字串的形式為某個變數定義相應的正則表示式之外,我們還可以指定一個實現了IRouteConstraint介面的型別的物件對整個請求進行約束。如下面的程式碼片斷所示,IRouteConstraint具有唯一的方法Match用於定義約束的邏輯,該方法的5個引數分別表示:HTTP上下文、當前路由物件、約束的名稱(儲存約束物件的RouteValueDictionary的Key)、解析被匹配URL得到的變數集合以及表示路由的方向。
1: public interface IRouteConstraint
2: {
3: bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection);
4: }
5: public enum RouteDirection
6: {
7: IncomingRequest,
8: UrlGeneration
9: }
所謂路由的方向表示是針對請求匹配(入棧)還是針對URL的生成(出棧),分別通過如上所示的列舉型別RouteDirection的兩個列舉值表示。具體來說,當呼叫路由物件的GetRouteData和GetVirtualPathData方法時,列舉值IncomingRequest和UrlGeneration分別被採用。
ASP.NET路由系統的應用程式設計介面中定義瞭如下一個實現了IRouteConstraint介面的HttpMethodConstraint型別。顧名思義,HttpMethodConstraint提供針對HTTP方法(GET、POST、PUT、DELTE等)的約束。我們可以通過HttpMethodConstraint為路由物件設定一個允許的HTTP方法列表,只有方法名稱在這個指定的列表中的HTTP請求才允許被路由。這個被允許被路由的HTTP方法列表對於HttpMethodConstraint的只讀屬性AllowedMethods,並在建構函式中初始化。
1: public class HttpMethodConstraint : IRouteConstraint
2: {
3: public HttpMethodConstraint(params string[] allowedMethods);
4: bool IRouteConstraint.Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection);
5: public ICollection<string> AllowedMethods { get; }
6: }
同樣是針對我們演示的例子,我們在進行路由註冊的時候通過如下的代表應用了一個型別為HttpMethodConstraint的約束,並將允許的HTTP方法設定為POST,意味著被註冊的Route物件僅限於路由POST請求。
1: public class Global : System.Web.HttpApplication
2: {
3: protected void Application_Start(object sender, EventArgs e)
4: {
5: var defaults = new RouteValueDictionary { { "areacode", "010" }, { "days", 2 } };
6: var constaints = new RouteValueDictionary { { "areacode", @"0\d{2,3}" }, { "days", @"[1-3]{1}" }, { "httpMethod", new HttpMethodConstraint("POST") } };
7: var dataTokens = new RouteValueDictionary { { "defaultCity", "BeiJing" }, { "defaultDays", 2 } };
8:
9: RouteTable.Routes.MapPageRoute("default", "{areacode}/{days}", "~/weather.aspx", false, defaults, constaints, dataTokens);
10: }
11: }
現在我們採用與註冊的URL模板相匹配的地址(/010/2)來訪問Weather.aspx頁面,依然會得到如下圖所示的404錯誤。[例項原始碼下載]
三、對現有檔案的路由
在成功註冊路由的情況下,如果我們按照傳統的方式訪問一個物理檔案(比如.asxp、.css或者.js等),在請求地址滿足某個路由的URL模板模式的情況下,ASP.NET是否還是正常實施路由呢?我們不妨通過我們的例項還測試一下。為了讓針對某個物理檔案的訪問地址也滿足註冊路由物件的URL模板模式,我們需要按照如下的方式將上面定義的關於正則表示式約束刪除。
1: public class Global : System.Web.HttpApplication
2: {
3: protected void Application_Start(object sender, EventArgs e)
4: {
5: var defaults = new RouteValueDictionary { { "areacode", "010" }, { "days", 2 }};
6: //var constaints = new RouteValueDictionary { { "areacode", @"0\d{2,3}" }, { "days", @"[1-3]{1}" } };
7: var dataTokens = new RouteValueDictionary { { "defaultCity", "BeiJing" }, { "defaultDays", 2 } };
8:
9: RouteTable.Routes.MapPageRoute("default", "{areacode}/{days}", "~/weather.aspx", false, defaults, null, dataTokens);
10: }
11: }
當我們通過傳統的方式來訪問存放於根目錄下的weather.aspx頁面時會得到如下圖所示的結果。從介面上的輸出結果我們不難看出,雖然請求地址完全滿足我們註冊路由物件的URL模板模式,但是ASP.NET並沒有對請求地址實施路由。原因很簡單,如果中間發生了路由,基於頁面的RouteData的各項屬性都不可能為空。[例項原始碼下載]
那麼是否意味著如果請求地址對應著一個現存的物理檔案,ASP.NET就會自動忽略路由呢?實則不然,或者說不對現有檔案實施路由僅僅預設採用的行為。是否對現有檔案實施路由取決於代表全域性路由表的RouteCollection物件的RouteExistingFiles屬性,該屬性預設情況下為False,我們可以將此屬性設定為True使ASP.NET路由系統忽略現有物理檔案的存在,總是按照註冊的路由表進行路由。為了演示這種情況下,我們對Global.asax檔案作了如下的改動,在進行路由註冊之前將RouteTable的Routes屬性代表的RouteCollection物件的RouteExistingFiles屬性設定為True。
1: public class Global : System.Web.HttpApplication
2: {
3: protected void Application_Start(object sender, EventArgs e)
4: {
5: RouteTable.Routes.RouteExistingFiles = true;
6: var defaults = new RouteValueDictionary { { "areacode", "010" }, { "days", 2 } };
7: var dataTokens = new RouteValueDictionary { { "defaultCity", "BeiJing" }, { "defaultDays", 2 } };
8:
9: RouteTable.Routes.MapPageRoute("default", "{areacode}/{days}", "~/weather.aspx", false, defaults, null<