Asp.Net MVC4 之Url路由
MVC4常見路由的處理方式
//直接方法過載+匿名物件 routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); //構造路由然後新增 Route myroute = newRoute("{controller}/{action}", new MvcRouteHandler()); routes.Add("MyRoute0", myroute); //跨名稱空間路由 routes.MapRoute( "AddContollerRoute", "Home/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional },new[] { "URLsAndRoutes.AdditionalControllers" } ); routes.MapRoute( "MyRoute1", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new[] { "URLsAndRoutes.Controllers" } ); //可變長度路由 + 正則表示式匹配路由 routes.MapRoute( "MyRoute2", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new { controller = "^H.*", action = "^Index$|^About$" }, new[] { "URLsAndRoutes.Controllers" } ); //指定請求方法 routes.MapRoute("MyRoute3", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new { controller = "^H.*", action = "Index|About", httpMethod = new HttpMethodConstraint("GET") }, new[] { "URLsAndRoutes.Controllers" } );
先來看下面兩個個url,對比一下:
- http://xxx.yyy.com/Admin/UserManager.aspx
- http://xxx.yyy.com/Admin/DeleteUser/1001
對於第1個Url,假設它與伺服器上的檔案有直接的關係,那麼伺服器在接受客戶端請求並將對應的檔案傳送給客戶端。我們大概可以猜到它是對使用者管理的一個頁面,它的物理檔案UserManager.aspx在網站根目錄下面的Admin資料夾中。而第2個url,在不知道Mvc路由以及Url重寫時,很難猜到這個Url背後具體有些什麼,前提條件是基於.Net框架開發的Web專案。 那麼在這裡,我們引入Asp.Net Mvc Url路由這個概念,也正是本文所要闡述的主題,Url路由模組是負責對映從瀏覽器請求到特定的控制器動作。
基於上面提到的Url路由以及其作用,我們就大概能猜到第2個Url背後有些啥了。 自然而然的Admin就是控制器了,DeleteUser是控制器裡面的動作及Action了,1001就是Action的引數。到這裡我們對Url路由有一個簡單的認識,那麼接著看下面一組url,假設Home是控制器,Index是控制器裡面的Action,至於1001或者2345這類資料我們暫且約定為引數:
- http://xxx.yyy.com
- http://xxx.yyy.com/Home/1001
- http://xxx.yyy.com/Index/1001
- http://xxx.yyy.com/Home/Index/1001/2345
- http://xxx.yyy.com/System/Home/Index/1001
按照約定,從上面的幾組Url中可以看出,有的缺控制器,有的缺Action,有的帶有好幾個引數,有的又莫名的多出了控制器、Action、引數之外的東西,那麼他們能正確的訪問嗎?
註冊路由
在vs2012裡面新建一個asp.net mvc4 web 應用程式專案,可以在專案的根目錄下App_Start裡面看到RouteConfig檔案,在這個檔案裡面就可以新增url路由了。在檔案裡面可以看到MapRoute 這個方法,它是RouteCollection的擴充套件方法,並且含有多個過載,下面看看MapRoute方法的引數,這裡選引數最多的那個:
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces);
從上面的程式碼片段大概可以看出該方法含有路由名稱、路由的Url、預設值、約束、首先查詢路由所在的名稱空間,該方法是返回對對映路由的引用。不過在RouteConfig檔案中我們可以看到新增路由的程式碼:
routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } );
稍微分析一下這段程式碼可以知道,在路由集合裡面添加了一個名為"Default"的路由,並且有一個預設的路由指向,指向名稱為Home的控制器,並且Action為Index,可以明顯的看到這裡 "id = UrlParameter.Optional" 的寫法,它的意思就是說Action的引數可以不需要使用者來指定,可以預設。
多個引數如何傳遞
之前在一個QQ群裡面見到一兄弟在問,類似這樣的Url“http://xxx.yyy.com/Home/Index/1001/2345/tauruswu”在路由裡面怎麼配置?其實這個也很簡單,我們只需要將路由配置、以及Action稍作調整。
routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}/{*catchall}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } );
這個是調整之後的路由配置,只需要在Url後面再加上"{*catchall}"即可,那麼Action調整之後如下
public ActionResult Index(string id,string catchall) { ViewBag.Message = "修改此模板以快速啟動你的 ASP.NET MVC 應用程式。"; return View(); }
在Action裡面定義了一個引數"catchall"用來接收Url中除了Id之外其他所有的引數值,那麼上面那個Url中接收的引數就是“2345/tauruswu”,這樣看起來好不好了,不過我覺得怪怪的,有沒有更好的解決方法了?Url有必要寫成那麼長嗎?這個問題在後續文章中會涉及到。
你也許會犯的錯誤
不知各位兄弟在剛剛接觸MVC的時候,有沒有碰到過這樣的問題,如下圖
那麼這個錯誤是如何引起的了?程式碼是這麼寫的
namespace MvcDebug.Controllers { public class HomeController : Controller { public ActionResult Index(string id,string catchall) { ViewBag.Message = "修改此模板以快速啟動你的 ASP.NET MVC 應用程式。"; return View(); } } } namespace MvcDebug.Controllers.Tauruswu { public class HomeController : Controller { public ActionResult Index(string id, string catchall) { ViewBag.Message = "修改此模板以快速啟動你的 ASP.NET MVC 應用程式。"; return View(); } } }
我們再看英文提示大概就是說匹配出了多個控制器為"Home"的型別,在它的提示中也告訴了我們解決方法,說在MapRoute方法中使用"namespaces"這個引數,我們先將這個放在一邊,將丟擲這個錯誤的原始碼給找出來,具體的原始碼在DefaultControllerFactory這個類中,看類名就能猜出它的作用是什麼了。
private Type GetControllerTypeWithinNamespaces(RouteBase route, string controllerName, HashSet<string> namespaces) { // Once the master list of controllers has been created we can quickly index into it ControllerTypeCache.EnsureInitialized(BuildManager); ICollection<Type> matchingTypes = ControllerTypeCache.GetControllerTypes(controllerName, namespaces);
switch (matchingTypes.Count) { case 0: // no matching types return null; case 1: // single matching type return matchingTypes.First(); default: // multiple matching types throw CreateAmbiguousControllerException(route, controllerName, matchingTypes); } }
上面粗體標出的程式碼,因為我們在路由中沒有配置“namespaces”這個引數,這段程式碼的意思就是通過控制器名稱獲取所匹配的控制器型別集合,當獲取到集合資料之後就開始Case了,很明顯,這裡集合的數目是2,自然就拋錯了。
問題出來了,該如何解決?在錯誤提示中說要用到“namespaces”這個引數,我們能不能告訴MVC解析引擎,在解析控制器名稱時,能不能對某些名稱空間進行優先處理,事實上是可以的,只需要在配置路由的地方稍微調整一下
routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}/{*catchall}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, namespaces: new[] { "MvcDebug.Controllers.Tauruswu" } );
上面標粗的程式碼的意思是說優先解析“MvcDebug.Controllers.Tauruswu”這個名稱空間裡面的控制器。
路由約束
對於路由約束,我個人覺得這個可能在實際開發中用的不是很多,既然MapRoute方法提供了constraints這個引數及約束,那麼在某些特定的場合肯定能發揮它的作用。在MVC中提供了三種路由約束方案,分別是: 1)正則表示式 ,2)http方法 ,3)自定義約束 。下面我們分別介紹下這三種約束的使用方法。
1)正則表示式 ,在路由配置中,我們做了這樣的規則,只匹配Controller名稱以H開頭的
routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}/{*catchall}", defaults: new { controller = "Demo", action = "Index", id = UrlParameter.Optional }, constraints: new { controller = "^H.*" }, namespaces: new[] { "MvcDebug.Controllers.Tauruswu" } );
2) http方法 ,我們將路由配置稍作修改
routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}/{*catchall}", defaults: new { controller = "Demo", action = "Index", id = UrlParameter.Optional }, constraints: new { httpMethod = new HttpMethodConstraint("POST"), }, namespaces: new[] { "MvcDebug.Controllers.Tauruswu" } );
然後在對應的Action上面打個標記
[HttpGet] public ActionResult Index() { ViewBag.Message = "修改此模板以快速啟動你的 ASP.NET MVC 應用程式。"; return View(); }
你們說這樣行不行了?
3) 自定義約束,如果說上面兩種需求還是不能滿足你,那麼我們可以自定義約束。我們翻看HttpMethodConstraint這個類,可以看到它是繼承IRouteConstraint這個介面,其定義是
public interface IRouteConstraint { bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection); }
裡面只有一個布林型別的方法,關於這個例子,是從網上借鑑過來的,如下
public class CustomConstraint : IRouteConstraint { private string requiredAgent; public CustomConstraint(string agentArgs) { this.requiredAgent = agentArgs; } public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { return httpContext.Request.UserAgent != null && httpContext.Request.UserAgent.Contains(requiredAgent); } }
這段程式碼的意思就是檢查客戶端請求的UserAgent屬性值,看它是否含有一個被傳遞給建構函式的值。那麼我們將路由作如下修改
routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}/{*catchall}", defaults: new { controller = "Demo", action = "Index", id = UrlParameter.Optional }, constraints: new { customConstraint = new CustomConstraint("IE"), }, namespaces: new[] { "MvcDebug.Controllers.Tauruswu" } );
很顯然,這個只能在IE遊覽器下面遊覽。
如何建立自定義路由處理程式
在翻閱MapRoute方法原始碼的時候,看到了這麼一段
Route route = new Route(url, new MvcRouteHandler()) { Defaults = CreateRouteValueDictionary(defaults), Constraints = CreateRouteValueDictionary(constraints), DataTokens = new RouteValueDictionary() }; .....
routes.Add(name, route);
在Route例項化的時候,它是用到了“MvcRouteHandler“這個類,該類繼承”IRouteHandler“介面,如果我們不用系統裡面已經定義好的路由處理方案,我們要自己來實現一套?改怎麼下手,此時只需要繼承”IRouteHandler“這個介面並實現”GetHttpHandler“方法即可。
最後在新增路由時,像這樣操作
routes.Add(new Route("DemoUrl",new DemoRouteHandler());
當我們在遊覽器裡面請求/DemoUrl這個地址時,就會用到我們自定義的處理程式,在實際開發當中,如果真的要用到自定義路由處理程式,那麼我們就要實現很多原本框架所實現的空能,雖然這給我們帶來了很大的擴充套件空間,但是又不可控。
總結
Url路由系統是通過請求地址進行解析從而得到以目標Controller/Action名稱為核心的路由資料,Url路由系統是建立在Asp.net 之上,我們在除錯System.Web.Routing的原始碼時候可以得知。在這裡我們由淺入深的瞭解了路由系統,接下來我們會講到控制器以及Action,也是最為核心的東西。