在Web Form 情況下,每一個 ASPX頁面既是一個文件,又是一個隊請求自包含的響應。而在 MVC 情況下,請求是由控制器類中的動作方法處理的,而且與硬碟上的文件沒有一對一的相互關係。 ASP.NET 平臺為了處理 MVC 的 URL,採用了路由系統,它主要有兩個功能: 考查一個輸入 URL(I ...
在Web Form 情況下,每一個 ASPX頁面既是一個文件,又是一個隊請求自包含的響應。而在 MVC 情況下,請求是由控制器類中的動作方法處理的,而且與硬碟上的文件沒有一對一的相互關係。
ASP.NET 平臺為了處理 MVC 的 URL,採用了路由系統,它主要有兩個功能:
- 考查一個輸入 URL(Incoming URL),並推出該請求想要的是哪一個控制器和動作。這正是接收到一個客戶端請求時,希望路由系統去做的事情。
- 生成一個輸出 URL(Outgoing URL),這些 URL 是在視圖渲染的 HTML 中出現的 URL,以便使用戶點擊這些鏈接時,調用一個特定的動作(此時,它又變成了輸入 URL)。
URL 模式
路由系統用一組路由來實現它的功能。這些路由共同組成了應用程式的URL架構(Schema)或方案(Scheme),這種URL架構(或方案)是應用程式能夠識別並能對之作出相應的一組URL。
每一條路由都包含一個URL模式(Pattern),用它與一個輸入的URL進行比較,如果該模式與這個URL匹配,那麼它(URL模式)便被路由系統用來對這個URL進行處理。
URL模式主要有連個關鍵行為:
- URL 模式是保守的(Conservative):只匹配與模式具有相同片段數的URL。
- URL模式是寬鬆的(Liberal):如果一個URL正好具有正確的片段數,該模式就會用來為片段變數提取值,而不管這個值可能是什麼。
如示例:
/Admin |
/Index |
|
|
First Segment |
Second Segment |
|
片段1 |
片段2 |
簡單路由的創建及註冊
定義路由的文件 RouteConfig.cs 文件是在 App_Start 文件夾中的。在其中定義的靜態 RegisterRoutes 是通過 Global.asax.cs 文件進行調用的,當啟動應用程式時,它建立了一些核心的 MVC 特性。
基本流程如下:
底層的 ASP.NET 平臺在 MVC 第一次啟動時調用Global.asax.cs —>Application_Start() ——> RouteConfig.cs—> RegisterRoutes()。
定義函數類似如下樣子:
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); }
Global.asax.cs中的Application_Start()函數調用方式如下方式:
RouteConfig.RegisterRoutes(RouteTable.Routes);
註冊路由的一個方便的方法是,使用 RouteCollection 類所定義的MapRoute方法。如:
routes.MapRoute("MyRoute", "{controller}/{action}");
也可以通過創建一個新的 Route 來實現,如:
Route myRoute = new Route("{controller}/{action}", new MvcRouteHandler()); routes.Add("MyRoute", myRoute);
上述兩種方式的效果是一樣的。
定義預設值
預設URL被表示成“~/”送給路由系統。由於URL模式是保守的(即它們只匹配指定片段數的URL),如果要改變這種預設行為,則需要使用預設值——當URL不包含與一個片段匹配的值時,便使用預設值。如下麵粗體字部分:
public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); // 通過創建一個新的 Route 實現路由的註冊 //Route myRoute = new Route("{controller}/{action}", new MvcRouteHandler()); //routes.Add("MyRoute", myRoute); // 使用 RouteCollection 類所定義的MapRoute方法實現路由的註冊(效果與上面方式相同) // 下麵第三個參數提供了一個包含預設路由的值的對象,當 URL 片段無匹配的值時(如片段少於定義給定的片段數時), // 便會使用預設值(片段數需要符合定義的路由片段數,太多時將不做匹配)。 routes.MapRoute("MyRoute", "{controller}/{action}", new { controller = "Home", action = "Index" }); } }
使用靜態URL片段
1、有時我們不僅需要URL模式的所有片段都是可變的,也會需要創建具體靜態片段的模式。比如當我們需要支持帶有某種首碼的URL,例如帶有Public首碼的URL:http://mydomain.com/Public/Home/Index。
如:routes.MapRoute("", "Public/{controller}/{action}", new { controller = "Home", action = "Index" });
上面這句話的意思是:將匹配具有三個片段的URL,但第一個必須是Public,其他兩個片段可以有任何值,並將被用於controller和action變了。如果沒有後兩個片段,則將使用預設值。
2、可以創建一個既有靜態也有可變元素片段的 URL 模式,如:
// 創建一個既有靜態也有可變元素片段的 URL 模式 // MapRoute 將在路由集合的末尾添加一條新的路由 // 該路由需要放在其他路由之前,原因是路由是按照他們在 RouteCollection 對象中出現的順序被運用的。 // 我們可以將一條路由按照指定的位置添加,但一般不採用這種方式,原因是讓路由以它們被定義的順序來 // 運用更容易理解運用於一個應用程式的路由。 // 因此,路由系統是先匹配最前面定義的路由,如果不能匹配,則繼續下一個,所以,最好先定義較具體的路 // 由,然後次之,以此類推。 routes.MapRoute("", "X{controller}/{action}");
假設如下顛倒順序:
routes.MapRoute("MyRoute", "{controller}/{action}", new { controller = "Home", action = "Index" });
routes.MapRoute("", "X{controller}/{action}");
那麼,第一條路由匹配任何具有0、1、2片段的URL,它將是被使用的一條。更具體的路由現在是列表的第二條,它將是不可到達的。新路由(第二條)去掉URL的前導“X”,但舊路由(第一條)卻不會這麼做。因此,像這樣的一條URL:
http://mydomain.com/XHome/Index
將以名為“XHome”的控制器為目標,而這是不存在的,因此會導致一個“404——未找到”錯誤被髮送給用戶。
3、使用靜態片段和預設值為特定的路由創建一個別名
如果已經公開發佈了URL方案,並且它與用戶形成了一種契約,此時創建別名是有用的。如果我們重構程式,則需要保留以前的URL格式。下麵示例給出瞭如何保留舊式URL方案的路由:
// 結合靜態片段和預設值為特定的路由創建一個別名 // 匹配第一個片段是 Shop 的任意兩片段 URL,action 的值取自第二個 URL 片段。 // 由於此 URL 模式未提供 controler 的可變片段,所以會使用提供的預設值(“Home”)。即對 // Shop 控制器上的一個動作的請求會被轉換成對 Home 控制器的請求。 routes.MapRoute("ShopSchema", "Shop/{action}", new { controller = "Home" });
如果更徹底的,我們可以為被重構且不再出現在控制器中的動作方法創建別名,如下:
routes.MapRoute("ShopSchema2", "Shop/OldAction", new { controller = "Home", action = "Index" });
定義自定義片段變數
在MVC中有三個片段變數的名稱是被保留的,不能用於自定義片段變數名,除此之外均可命名為自定義片段變數。這三個片段變數分別是:controller(控制器片段變數)、action(動作方法片段變數)和area(區域片段變數)。
自定義片段變數示例:
// 定義一個名為“id”的自定義變數(粗體字部分)。如果沒有與之對應的片段內容,則將使用預設值 routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "DefaultId" });
我們可以在動作方法中通過RouteData.Values屬性訪問任何一個片段變數。如下:
/// <summary> /// 獲取 URL 模式中自定義變數(“id”)的值,並用 ViewBag 將它傳遞給視圖。 /// </summary> /// <returns></returns> public ActionResult CustomVariable() { ViewBag.Controller = "Home"; ViewBag.Action = "CustomVariable"; ViewBag.CustomVariable = RouteData.Values["id"]; return View(); }
用自定義變數作為動作方法參數
在開發過程中除了可以使用RouteData.Values屬性訪問自定義路由變數,還可以以URL模式中的變數相匹配的名稱,來定義動作方法的參數。此時,MVC框架將把從URL獲得的值作為參數傳遞給動作方法。
如下麵的寫法:
/// <summary> /// 用自定義變數作為動作方法參數 /// </summary> /// <param name="id"> /// MVC 框架會嘗試將 URL 的值轉換成所定義的參數類型,這 /// 里將轉換成 string ,MVC 框的這以特性將方便開發者不必 /// 自行做轉換。 /// </param> /// <returns></returns> public ActionResult CustomVariable(string id) { ViewBag.Controller = "Home"; ViewBag.Action = "CustomVariable"; ViewBag.CustomVariable = id; return View(); }
定義可選URL片段
可選URL片段是指用戶不需要指定、但又未指定預設值的片段。
下麵示例通過將預設值設置為“UrlParameter.Optional”,便指明瞭一個片段變數是可選的。在下麵實例中,只有當輸入URL中存在相應片段時,id變數才被添加到變數集合中,當未為可選片段變數提供值時,對應的參數值將為null。
// 定義可選 URL 片段。該路由的效果是:無論是否提供id,都將進行匹配 routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional });
對應的動作方法如下:
public ActionResult CustomVariable(string id) { ViewBag.Controller = "Home"; ViewBag.Action = "CustomVariable"; // 檢查是否為一個可選片段變數提供了值 ViewBag.CustomVariable = id == null ? "<no value>" : id; return View(); }
註:該動作方法可以通過C#的可選參數的方式實現將片段變數的預設值從路由中分離,詳見“使用可選URL片段強制關註分離”。
- 使用可選URL片段強制關註分離
使用C#的可選參數及路由中的可選片段變數定義動作方法參數的預設值,以實現將片段變數的預設值從應用程式的路由中分離。如:
/// <summary> /// 使用 C# 的可選參數為動作方法參數定義預設值 /// </summary> /// <param name="id"></param> /// <returns></returns> public ActionResult CustomVariable(string id = "DefaultId") { ViewBag.Controller = "Home"; ViewBag.Action = "CustomVariable"; ViewBag.CustomVariable = id; return View(); }
與之對應的可選URL片段路由就是前面介紹過的“可選URL片段”(通過將預設值設置為UrlParameter.Optional來實現)。其效果與這條路由相同:routes.MapRoute("MyRoute", "{controller}/{action}/{id}",new { controller = "Home", action = "Index", id = "DefaultId" });。
定義可變長路由
通過指定“全匹配(catchall)”片段變數,並以星號(*)為首碼,便可以實現一個可變長路由。如:
// 定義可變長路由 routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional });
上述代碼中,斜體的片段變數定義中,前三個分別用於設置controller、action和id的值,後面的加粗斜體({*catchall})實現了可變長路由的定義,可以匹配任何URL,無論有多少片段,也不管其值是什麼。
由於由catchall捕獲的片段是以“片段/片段/片段”的形式表示的,因此,需要對這個字元串進行處理——將其分解成一個個的片段,在處理catchall變數時和處理自定義變數是一樣的,只是需要註意該變數的值可能是多個片段連成的一個單一的字元串,就像前面說的那種形式,當然是不需要擔心字元串中會存在前導或後導的“/”字元。
按命名空間區分控制器優先順序
如果在不同的命名空間存在同名控制器時,MVC框架將不知道該如何處理,即對象不明確。
假設在示例項目中添加名為AdditionalControllers的文件夾,並添加一個新的Home控制器,如下所示:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace UrlsAndRoutes.AdditionalControllers { public class HomeController : Controller { public ActionResult Index() { ViewBag.Controller = "Additional Controllers - Home"; ViewBag.Action = "Index"; return View("ActionName"); } } }
此時,運行程式,將會出現錯誤,原因就是在不同的命名空間下存在同名控制器,而MVC不會處理這種問題。但是,可以通過將這些命名空間表示成一個字元串數組的方式通知MVC框架要對指定的命名空間優先進行處理。如:
public static void RegisterRoutes(RouteCollection routes) { // 指定命名空間解析順序,MVC 將在處理其他命名空間之前優先處理 UrlsAndRoutes.AdditionalControllers 命名空間 routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new string[] { "UrlsAndRoutes.AdditionalControllers" }); }
如果要對一個命名空間的某個控制器給予優先,但又要解析另一個命名空間中的所以其他控制器,就要創建多條路由,原因是添加到一條路由的同一組字元串數組中的命名空間具有同等的優先順序。如:
public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute("AddControllerRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new string[] { "UrlsAndRoutes.AdditionalControllers" }); routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new string[] { "UrlsAndRoutes.Controllers" }); }
當明確請求第一個片段為Home的URL時,會運用第一條路由,並且會以AdditionalControllers文件夾中的Home控制器為目標。其他所有請求,包括未指定第一片段的那些請求,會由Controllers文件夾中的控制器處理。
當然,也可以通知MVC框架只處理指定的命名空間。如果沒有匹配的控制器,將不會查找其他命名空間下的控制器。如:
public static void RegisterRoutes(RouteCollection routes) { // 禁用備用命名空間 Route myRoute = routes.MapRoute("AddControllerRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new string[] { "UrlsAndRoutes.AdditionalControllers" }); myRoute.DataTokens["UseNamespaceFallback"] = false; }
為了禁止搜索其他命名空間的控制器,必須取得這個Route對象,並把DataTokens屬性集中的UseNamespaceFallback鍵值設置為“false”。其效果是,不能滿足AdditionalControllers文件夾中Home控制器的請求將失敗。
約束路由
用正則表達式約束路由
/// <summary> /// 使用正則表達式約束一條路由 /// </summary> /// <param name="routes"></param> public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new { controller = "^H.*" }, new string[] { "UrlsAndRoutes.Controllers" }); }
通過把約束作為參數傳遞給MapRoute方法,可以定義約束。約束被表示成一個匿名類型,該類型的屬性對應於想要進行約束的片段變數名。
註意,在執行時,預設值是在約束被檢測之前運用的。也就是說在匹配預設值的情況下,先匹配預設值,然後再進行約束的檢查。
將一條路由約束到一組指定的值
如果想限定URL片段只匹配一些指定的值,則可以通過豎線(|)字元來實現,如:
/// <summary> /// 將一條路由約束到一組指定的值 /// </summary> /// <param name="routes"></param> public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new { controller = "^H.*", action = "^Index$|^About&=$" }, new string[] { "UrlsAndRoutes.Controllers" }); }
這條約束合起來就是施加於action變數值的約束與施加於controller變數的約束相組合。它只匹配這樣的URL:controller變數以“H”字母打頭,而且action變數是“Index”或“About”。
使用HTTP方法約束路由
可以對路由進行以使他們只匹配指定的HTTP方法進行請求的URL。如:
/// <summary> /// 基於 HTTP 方法進行路由的約束 /// </summary> /// <param name="routes"></param> public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new { controller = "^H.*", action = "Index|About", httpMethod = new HttpMethodConstraint("GET") }, new string[] { "UrlsAndRoutes.Controllers" }); }
可以像下麵的方式方便的添加對HTTP其他方法的支持。如:
httpMethod = new HttpMethodConstraint("GET","POST")
定義自定義約束
可以通過實現IRouteConstraint介面,來定義自己的自定義約束。如:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Routing; namespace UrlsAndRoutes.Infrastructure { public class UserAgentConstratint : IRouteConstraint { private string _requiredUserAgent; public UserAgentConstratint(string agentParam) { _requiredUserAgent = agentParam; } public bool Match(HttpContextBase httpContext , Route route , string parameterName , RouteValueDictionary values , RouteDirection routeDirection) { return httpContext.Request.UserAgent != null && httpContext.Request.UserAgent.Contains(_requiredUserAgent); } } }
IRouteConstraint介面定義了Match方法,實現它可以用了實現對路由系統只是它的約束是否已得到滿足。其中參數提供了這些對象的訪問:客戶端請求、待評估路由、約束的參數名、從URL提取的片段變數,以及該請求要檢查的是輸入URL還是輸出URL的細節。上述自定義約束的使用方式見下列代碼:
/// <summary> /// 自定義路由約束的使用 /// </summary> /// <param name="routes"></param> public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute("ChromeRoute", "{*catchall}", new { controller = "Home", action = "Index" }, new { customConstraint = new UserAgentConstratint("Chrome") }, new string[] { "UrlsAndRoutes.AdditionalControllers" }); routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new string[] { "UrlsAndRoutes.Controllers" }); }
上例的約束的作用是:第一條約束路由時期只匹配來自用戶代理字元串含有Chrome的瀏覽器的請求。如果此路由匹配,那麼該請求將被髮送給AdditionalControllers文件夾中定義的Home控制器的Index動作方法,而不管所請求的URL具有什麼樣的結構或內容。第二條路由將匹配其他所有請求,並以Controllers文件夾中的控制器為目標。
最終的效果是Chrome瀏覽器最終只能訪問應用程式的同一個位置。需要註意的是不建議對應用程式進行限制,以使他只支持一種瀏覽器,該示例只是提供了一種得到有關字母的辦法。同時,該示例只是為了演示自定義路由約束。