咱們知道在ASP.NET Web Forms中,一個URL請求每每對應一個aspx頁面,一個aspx頁面就是一個物理文件,它包含對請求的處理。html
而在ASP.NET MVC中,一個URL請求是由對應的一個Controller中的Action來處理的,由URL Routing來告訴MVC如何定位到正確的Controller和Action。正則表達式
籠統的講,URL Routing包含兩個主要功能:解析URL 和 生成URL,本文將圍繞這兩個大點進行講解。瀏覽器
本文目錄服務器
讓咱們從下面這樣一個簡單的URL開始:mvc
http://mysite.com/Admin/Indexasp.net
在域名的後面,默認使用「/」來對URL進行分段。路由系統經過相似於 {controller}/{action} 格式的字符串能夠知道這個URL的 Admin 和 Index 兩個片斷分別對應Controller和Action的名稱。dom
默認狀況下,路由格式中用「/」分隔的段數是和URL域名的後面的段數是一致的,好比,對於{controller}/{action} 格式只會匹配兩個片斷。以下表所示:ide
URL路由是在MVC工程中的App_Start文件夾下的RouteConfig.cs文件中的RegisterRoutes方法中定義的,下面是建立一個空MVC項目時系統生成的一個簡單URL路由定義:函數
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 } ); }
靜態方法RegisterRoutes是在Global.asax.cs文件中的Application_Start方法中被調用的,除了URL路由的定義外,還包含其餘的一些MVC核心特性的定義:網站
protected void Application_Start() { AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); }
RouteConfig.RegisterRoutes方法中傳遞的是 RouteTable 類的靜態 Routes 屬性,返回一個RouteCollection的實例。其實,「原始」的定義路由的方法能夠這樣寫:
public static void RegisterRoutes(RouteCollection routes) { Route myRoute = new Route("{controller}/{action}", new MvcRouteHandler()); routes.Add("MyRoute", myRoute); }
建立Route對象時用了一個URL格式字符串和一個MvcRouteHandler對象做爲構造函數的參數。不一樣的ASP.NET技術有不一樣的RouteHandler,MVC用的是MvcRouteHandler。
這種寫法有點繁瑣,一種更簡單的定義方法是:
public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute("MyRoute", "{controller}/{action}"); }
這種方法簡潔易讀,通常咱們都會用這種方法定義路由。
做爲演示,咱們先來準備一個Demo。建立一個標準的MVC應用程序,而後添加三個簡單的Controller,分別是HomeController、CustomerController和AdminController,代碼以下:
public class HomeController : Controller { public ActionResult Index() { ViewBag.Controller = "Home"; ViewBag.Action = "Index"; return View("ActionName"); } }
public class CustomerController : Controller { public ActionResult Index() { ViewBag.Controller = "Customer"; ViewBag.Action = "Index"; return View("ActionName"); } public ActionResult List() { ViewBag.Controller = "Customer"; ViewBag.Action = "List"; return View("ActionName"); } }
public class AdminController : Controller { public ActionResult Index() { ViewBag.Controller = "Admin"; ViewBag.Action = "Index"; return View("ActionName"); } }
在 /Views/Shared 文件夾下再給這三個Controller添加一個共享的名爲 ActionName.cshtml 的 View,代碼以下:
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>ActionName</title> </head> <body> <div>The controller is: @ViewBag.Controller</div> <div>The action is: @ViewBag.Action</div> </body> </html>
咱們把RouteConfig.cs文件中項目自動生成的URL Rounting的定義刪了,而後根據前面講的路由定義知識,咱們本身寫一個最簡單的:
public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute("MyRoute", "{controller}/{action}"); }
程序運行,URL定位到 Admin/Index 看看運行結果:
這個Demo輸出的是被調用的Controller和Action名稱。
在上面咱們必須把URL定位到特定Controller和Action,不然程序會報錯,由於MVC不知道去執行哪一個Action。 咱們能夠經過指定默認值來告訴MVC當URL沒有給出對應的片斷時使用某個默認的值。以下給controller和action指定默認值:
routes.MapRoute("MyRoute", "{controller}/{action}", new { controller = "Home", action = "Index" });
這時候若是在URL中不提供action片斷的值或不提供controller和action兩個片斷的值,MVC將使用路由定義中提供的默認值:
它的各類匹配狀況以下表所示:
注意,對於上面的URL路由的定義,咱們能夠只給action一個片斷指定默認值,可是不能只給controller一個片斷指定默認值,即若是咱們給Controller指定了默認值,就必定也要給action指定默認值,不然URL只有一個片斷時,這個片斷匹配給了controller,action將找不到匹配。
並非全部的片斷都是用來做爲匹配變量的,好比,咱們想要URL加上一個名爲Public的固定前綴,那麼咱們能夠這樣定義:
routes.MapRoute("", "Public/{controller}/{action}", new { controller = "Home", action = "Index" });
這樣,請求的URL也須要一個Public前綴與之匹配。咱們也能夠把靜態的字符串放在大括號之外的任何位置,如:
routes.MapRoute("", "X{controller}/{action}", new { controller = "Home", action = "Index" });
在一些狀況下這種定義很是有用。好比當你的網站某個連接已經被用戶廣泛記住了,但這一塊功能已經有了一個新的版本,但調用的是不一樣名稱的controller,那麼你把原來的controller名稱做爲如今controller的別名。這樣,用戶依然使用他們記住的URL,而導向的倒是新的controller。以下使用Shop做爲Home的一個別名:
routes.MapRoute("ShopSchema", "Shop/{action}", new { controller = "Home" });
這樣,用戶使用原來的URL能夠訪問新的controller:
contrlloer和action片斷變量對MVC來講有着特殊的意義,在定義一個路由時,咱們必須有這樣一個概念:contrlloer和action的變量值要麼能從URL中匹配獲得,要麼由默認值提供,總之一個URL請求通過路由系統交給MVC處理時必須保證contrlloer和action兩個變量的值都有。固然,除了這兩個重要的片斷變量,咱們也可從經過自定義片斷變量來從URL中獲得咱們想要的其它信息。以下自定義了一個名爲Id的片斷變量,並且給它定義了默認值:
routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "DefaultId" });
咱們在HomeController中增長一個名爲CustomVariable的ACtion來演示一下如何取自定義的片斷變量:
public ActionResult CustomVariable() { ViewBag.Controller = "Home"; ViewBag.Action = "CustomVariable"; ViewBag.CustomVariable = RouteData.Values["id"]; return View("ActionName"); }
能夠經過 RouteData.Values[segment] 來取得任意一個片斷的變量值。
再稍稍改一下ActionName.cshtml 來看一下咱們取到的自定義片斷變量的值:
... <div>The controller is: @ViewBag.Controller</div> <div>The action is: @ViewBag.Action</div> <div>The custom variable is: @ViewBag.CustomVariable</div> ...
將URL定位到 /Home/CustomVariable/Hello 將獲得以下結果:
自定義的片斷變量用處很大,也很靈活,下面介紹一些常見的用法。
咱們能夠將自定義的片斷變量看成參數傳遞給Action方法,以下所示:
public ActionResult CustomVariable(string id) { ViewBag.Controller = "Home"; ViewBag.Action = "CustomVariable"; ViewBag.CustomVariable = id; return View("ActionName"); }
效果和上面是同樣的,只不過這樣省去了用 RouteData.Values[segment] 的方式取自定義片斷變量的麻煩。這個操做背後是由模型綁定來作的,模型綁定的知識我將在後續博文中進行講解。
指定自定片斷變量爲可選,即在URL中能夠不用指定片斷的值。以下面的定義將Id定義爲可選:
routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional });
定義爲可選之後,須要對URL中沒有Id這個片斷值的狀況進行處理,以下:
public ActionResult CustomVariable(string id) { ViewBag.Controller = "Home"; ViewBag.Action = "CustomVariable"; ViewBag.CustomVariable = id == null ? "<no value>" : id; return View("ActionName"); }
當Id是整型的時候,參數的類型須要改爲可空的整型(即int? id)。
爲了省去判斷參數是否爲空,咱們也能夠把Action方法的id參數也定義爲可選,當沒有提供Id參數時,Id使用默認值,以下所示:
public ActionResult CustomVariable(string id = "DefaultId") { ViewBag.Controller = "Home"; ViewBag.Action = "CustomVariable"; ViewBag.CustomVariable = id; return View("ActionName"); }
這樣其實就是和使用下面這樣的方式定義路由是同樣的:
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 });
這個路由定義的匹配狀況以下所示:
使用*catchall,將匹配的任意數量的片斷,但咱們須要本身經過「/」分隔catchall變量的值來取得獨立的片斷值。
經過正則表達式,咱們能夠制定限制URL的路由規則,下面的路由定義限制了controller片斷的變量值必須以 H 打頭:
routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new { controller = "^H.*" } );
定義路由約束是在MapRoute方法的第四個參數。和定義默認值同樣,也是用匿名類型。
咱們能夠用正則表達式約束來定義只有指定的幾個特定的片斷值才能進行匹配,以下所示:
routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new { controller = "^H.*", action = "^Index$|^About$" } );
這個定義,限制了action片斷值只能是Index或About,不區分大小寫。
咱們還能夠限制路由只有當以某個特定的Http請求方式才能匹配。以下限制了只能是Get請求才能進行匹配:
routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new { controller = "^H.*", httpMethod = new HttpMethodConstraint("GET") } );
經過建立一個 HttpMethodConstraint 類的實例來定義一個Http請求方式約束,構造函數傳遞是容許匹配的Http方法名。這裏的httpMethod屬性名不是規定的,只是爲了區分。
這種約束也能夠經過HttpGet或HttpPost過濾器來實現,後續博文再講到濾器的內容。
若是標準的路由約束知足不了你的需求,那麼能夠經過實現 IRouteConstraint 接口來定義本身的路由約束規則。
咱們來作一個限制瀏覽器版本訪問的路由約束。在MVC工程中添加一個文件夾,取名Infrastructure,而後添加一個 UserAgentConstraint 類文件,代碼以下:
public class UserAgentConstraint : IRouteConstraint { private string requiredUserAgent; public UserAgentConstraint(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方法,返回的bool值告訴路由系統請求是否知足自定義的約束規則。咱們的UserAgentConstraint類的構造函數接收一個瀏覽器名稱的關鍵字做爲參數,若是用戶的瀏覽器包含註冊的關鍵字才能夠訪問。接一來,咱們須要註冊自定的路由約束:
public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute("ChromeRoute", "{*catchall}", new { controller = "Home", action = "Index" }, new { customConstraint = new UserAgentConstraint("Chrome") } ); }
下面分別是IE10和Chrome瀏覽器請求的結果:
並非全部的URL都是請求controller和action的。有時咱們還須要請求一些資源文件,如圖片、html文件和JS庫等。
咱們先來看看能不能直接請求一個靜態Html文件。在項目的Content文件夾下,添加一個html文件,內容隨意。而後把URL定位到該文件,以下圖:
咱們看到,是能夠直接訪問一靜態資源文件的。
默認狀況下,路由系統先檢查URL是否是請求靜態文件的,若是是,服務器直接返回文件內容並結束對URL的路由解析。咱們能夠經過設置 RouteCollection的 RouteExistingFiles 屬性值爲true 讓路由系統對靜態文件也進行路由匹配,以下所示:
public static void RegisterRoutes(RouteCollection routes) { routes.RouteExistingFiles = true; routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }); }
設置了routes.RouteExistingFiles = true後,還須要對IIS進行設置,這裏咱們以IIS Express爲例,右鍵IIS Express小圖標,選擇「顯示全部應用程序」,彈出以下窗口:
點擊並打開配置文件,Control+F找到UrlRoutingModule-4.0,將這個節點的preCondition屬性改成空,以下所示:
<add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" preCondition=""/>
而後咱們運行程序,再把URL定位到以前的靜態文件:
這樣,路由系統經過定義的路由去匹配RUL,若是路由中沒有定義該靜態文件的匹配,則會報上面的錯誤。
一旦定義了routes.RouteExistingFiles = true,咱們就要爲靜態文件定義路由,以下所示:
public static void RegisterRoutes(RouteCollection routes) { routes.RouteExistingFiles = true; routes.MapRoute("DiskFile", "Content/StaticContent.html", new { controller = "Customer", action = "List", }); routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }); }
這個路由匹配Content/StaticContent.html的URL請求爲controller = Customer, action = List。咱們來看看運行結果:
這樣作的目的是爲了能夠在Controller的Action中控制對靜態資源的請求,而且能夠阻止對一些特殊資源文件的訪問。
設置了RouteExistingFiles屬性爲true後,咱們要爲容許用戶請求的資源文件進行路由定義,若是每種資源文件都去定義相應的路由,就會顯得很繁瑣。
咱們能夠經過RouteCollection類的IgnoreRoute方法繞過路由定義,使得某些特定的靜態文件能夠由服務器直接返回給給瀏覽器,以下所示:
public static void RegisterRoutes(RouteCollection routes) { routes.RouteExistingFiles = true; routes.IgnoreRoute("Content/{filename}.html"); routes.MapRoute("DiskFile", "Content/StaticContent.html", new { controller = "Customer", action = "List", }); routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }); }
這樣,只要是請求Content目錄下的任何html文件都能被直接返回。這裏的IgnoreRoute方法將建立一個RouteCollection的實例,這個實例的Route Handler 爲 StopRoutingHandler,而不是 MvcRouteHandler。運行程序定位到Content/StaticContent.html,咱們又看到了以前的靜態面面了。
前面講的都是解析URL的部分,如今咱們來看看如何經過路由系統在View中生成URL。
在View中生成URL的最簡單方法就是調用Html.ActionLink方法,以下面在 Views/Shared/ActionName.cshtml 中的代碼所示:
... <div>The controller is: @ViewBag.Controller</div> <div>The action is: @ViewBag.Action</div> <div> @Html.ActionLink("This is an outgoing URL", "CustomVariable") </div> ...
這裏的Html.ActionLink方法將會生成指向View對應的Controller和第二個參數指定的Action,咱們能夠看看運行後頁面是如何顯示的:
通過查看Html源碼,咱們發現它生成了下面這樣的一個html連接:
<a href="/Home/CustomVariable">This is an outgoing URL</a>
這樣看起來,經過Html.ActionLink生成URL彷佛並無直接在View中本身寫一個<a>標籤更直接明瞭。 但它的好處是,它會自動根據路由配置來生成URL,好比咱們要生成一個指向HomeContrller中的CustomVariable Action的鏈接,經過Html.ActionLink方法,只須要給出對應的Controller和Action名稱就行,咱們不須要關心實際的URL是如何組織的。舉個例子,咱們定義了下面的路由:
public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute("NewRoute", "App/Do{action}", new { controller = "Home" }); routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }); }
運行程序,咱們發現它會自動生成下面這樣的鏈接:
<a href="/App/DoCustomVariable">This is an outgoing URL</a>
因此咱們要生成指向某個Action的連接時,最好使用Html.ActionLink方法,不然你很難保證你手寫的鏈接就能定位到你想要的Action。
上面咱們給Html.ActionLink方法傳遞的第二個參數只告訴了路由系統要定位到當前View對應的Controller下的Action。Html.ActionLink方法可使用第三個參數來指定其餘的Controller,以下所示:
<div> @Html.ActionLink("This targets another controller", "Index", "Admin") </div>
它會自動生成以下連接:
<a href="/Admin">This targets another controller</a>
有時候咱們想在鏈接後面加上參數以傳遞數據,如 ?id=xxx 。那麼咱們能夠給Html.ActionLink方法指定一個匿名類型的參數,以下所示:
<div> @Html.ActionLink("This is an outgoing URL", "CustomVariable", new { id = "Hello" }) </div>
它生成的Html以下:
<a href="/Home/CustomVariable/Hello">This is an outgoing URL</a>
經過Html.ActionLink方法生成的連接是一個a標籤,咱們能夠在方法的參數中給標籤指定Html屬性,以下所示:
<div> @Html.ActionLink("This is an outgoing URL", "Index", "Home", null, new {id = "myAnchorID", @class = "myCSSClass"}) </div>
這裏的class加了@符號,是由於class是C#關鍵字,@符號起到轉義的做用。它生成 的Html代碼以下:
<a class="myCSSClass" href="/" id="myAnchorID">This is an outgoing URL</a>
前面的都是生成相對路徑的URL連接,咱們也能夠經過Html.ActionLink方法生成完整的標準連接,方法以下:
<div> @Html.ActionLink("This is an outgoing URL", "Index", "Home", "https", "myserver.mydomain.com", " myFragmentName", new { id = "MyId"}, new { id = "myAnchorID", @class = "myCSSClass"}) </div>
這是Html.ActionLink方法中最多參數的重載方法,它容許咱們提供請求的協議(https)和目標服務器地址(myserver.mydomain.com)等。它生成的連接以下:
<a class="myCSSClass" id="myAnchorID" href="https://myserver.mydomain.com/Home/Index/MyId#myFragmentName" > This is an outgoing URL</a>
用Html.ActionLink方法生成一個html連接是很是有用而常見的,若是要生成URL字符串(而不是一個Html連接),咱們能夠用 Url.Action 方法,使用方法以下:
<div>This is a URL: @Url.Action("Index", "Home", new { id = "MyId" }) </div>
它顯示到頁面是這樣的:
咱們能夠根據某個特定的路由來生成咱們想要的URL,爲了更好說明這一點,下面給出兩個URL的定義:
public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute("MyRoute", "{controller}/{action}"); routes.MapRoute("MyOtherRoute", "App/{action}", new { controller = "Home" }); }
對於這樣的兩個路由,對於相似下面這樣的寫法:
@Html.ActionLink("Click me", "Index", "Customer")
始終會生成這樣的連接:
<a href="/Customer/Index">Click me</a>
也就是說,永遠沒法使用第二個路由來生成App前綴的連接。這時候咱們須要經過另外一個方法Html.RouteLink來生成URL了,方法以下:
@Html.RouteLink("Click me", "MyOtherRoute","Index", "Customer")
它會生成以下連接:
<a Length="8" href="/App/Index?Length=5">Click me</a>
這個連接指向的是HomeController下的Index Action。但須要注意,經過這種方式來生成URL是不推薦的,由於它不能讓咱們從直觀上看到它生成的URL指向的controller和action。因此,非到萬不得已的狀況纔會這樣用。
一般咱們通常在View中才會去生成URL,但也有時候咱們須要在Action中生成URL,方法以下:
public ViewResult MyActionMethod() { string myActionUrl = Url.Action("Index", new { id = "MyID" }); string myRouteUrl = Url.RouteUrl(new { controller = "Home", action = "Index" }); //... do something with URLs... return View(); }
其中 myActionUrl 和 myRouteUrl 將會被分別賦值 /Home/Index/MyID 和 / 。
更多時候咱們會在Action方法中將客戶端瀏覽器重定向到別的URL,這時候咱們使用RedirectToAction方法,以下:
public RedirectToRouteResultMyActionMethod() { return RedirectToAction("Index"); }
RedirectToAction的返回結果是一個RedirectToRouteResult類型,它使MVC觸發一個重定向行爲,並調用指定的Action方法。RedirectToAction也有一些重載方法,能夠傳入controller等信息。也可使用RedirectToRoute方法,該方法傳入的是object匿名類型,易讀性強,如:
public RedirectToRouteResult MyActionMethod() { return RedirectToRoute(new { controller = "Home", action = "Index", id = "MyID" }); }
下面是一些使用URL的建議:
參考:
《Pro ASP.NET MVC 4 4th Edition》