定製路由系統 路由系統是靈活可配置的,當然還可以通過下麵這兩種方式定製路由系統,來滿足其他需求。 1、 通過創建自定義的RouteBase實現; 2、 通過創建自定義路由處理程式實現。 創建自定義的RouteBase實現 創建自定義的RouteBase實現,需要實現一個RouteBase的派生類,而 ...
定製路由系統
路由系統是靈活可配置的,當然還可以通過下麵這兩種方式定製路由系統,來滿足其他需求。
1、 通過創建自定義的RouteBase實現;
2、 通過創建自定義路由處理程式實現。
創建自定義的RouteBase實現
創建自定義的RouteBase實現,需要實現一個RouteBase的派生類,而這需要實現以下兩個方法:
- GetRouteData(HttpContextBase httpContext):這是入站URL進行匹配的工作機制。框架依次對RouteTable.Routes的每個條目調用這個方法,直到其中之一返回一個非空值。
- GetVirtualPath(RequestContext requestContext,RouteValueDictionary values):這是出站URL生成的工作機制。框架依次對RouteTable.Routes的每一個條目調用這個方法,直到其中之一返回一個非空值。
為了演示這種自定義方式,這裡創建了一個RouteBase的派生類。我們假設這樣的一個需求環境:需要把一個現有的應用程式遷移到MVC框架,但不論出於什麼原因,我們需要相容之前的URL,那就可以通過這種方式來實現,當然可以通過規則的路由系統來處理——這裡不對這種方式進行討論。
首先,創建一個處理舊式路由請求的控制器,將其命名為:LegacyController,如:
using System.Web.Mvc; namespace UrlsAndRoutes.Controllers { /// <summary> /// 用以處理舊式 URL 請求的控制器 /// </summary> public class LegacyController : Controller { public ActionResult GetLegacyURL(string legacyURL) { // 應用程式遷移到 MVC 之前,請求是針對文件的,因此,實際上是需要在這裡處理被請求的文件。但這裡 // 只簡單說明一下自定義 RouteBase 的實現原理,所以,此處僅在視圖中顯示這個 URL。 return View((object)legacyURL); } } }
上面代碼對View方法中的參數做了轉換,如果不轉換,則C#編譯器會誤認為要將參數作為要指定渲染的視圖的名稱的字元串(View方法的一個重載版本的實現)。下麵是這個動作方法的視圖GetLegacyURL.cshtml:
@model string @{ ViewBag.Title = "GetLegacyURL"; Layout = null; } <h2>GetLegacyURL</h2> The URL requested was:@Model
1、對輸入URL進行路由
在Infrastructure文件夾中創建一個LegacyRoute類,其內容如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; namespace UrlsAndRoutes.Infrastructure { public class LegacyRoute : RouteBase { private string[] urls; public LegacyRoute(params string[] targetUrls) { } public override RouteData GetRouteData(HttpContextBase httpContext) { RouteData result = null; string requestedURL = httpContext.Request.AppRelativeCurrentExecutionFilePath; if (urls.Contains(requestedURL, StringComparer.OrdinalIgnoreCase)) { result = new RouteData(this, new MvcRouteHandler()); result.Values.Add("controller", "Legacy"); result.Values.Add("action", "GetLegacyURL"); result.Values.Add("legacyURL", "requestedURL"); } return result; } public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) { return null; } } }
註冊一條路由,以使其使用新建的這個RouteBase派生類:
public static void RegisterRoutes(RouteCollection routes) { // 註冊自定義的 RouteBase 實現 routes.Add(new LegacyRoute("~/articles/Windows_3.1_Overview.html", "~/old/.NET_1.0_Class_Library")); }
2、生成輸出URL
在LegacyRoute中實現GetVirtualPath方法以使其能夠支持輸出URL的生成。如:
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) { VirtualPathData result = null; if (values.ContainsKey("legactURL") && urls.Contains((string)values["legacyURL"], StringComparer.OrdinalIgnoreCase)) { // 如果存在一個匹配,將會創建一個 VirtualPathData 對象,在其中傳遞一個對當前對象的引用和出站 URL。由於路由系統已經預先將 // 字元“/”附加到了這個URL,因此,必須從生成的 URL 上刪除這個前導字元。 result = new VirtualPathData(this, new UrlHelper(requestContext).Content((string)values["legacyURL"]).Substring(1)); } return null; }
在ActionName.cshtml視圖中添加下麵這段代碼,以使其能禮儀自定義路由生成輸出URL:
<div> @* 經由自定義路由生成一個輸出 URL *@ This is a URL: @Html.ActionLink("Click me", "GetLegacyURL", new { legacyURL = "~/articles/Windows_3.1_Overview.html" }) </div>
上面代碼將產生一個這樣的a元素:
<a href=”/articles/Windows_3.1_Overview.html”>Click me</a>
用legacyURL屬性創建的匿名類型被轉換到了含有同名鍵的RouteValueDictionary類中。
創建自定義路由處理程式
路由已經依賴這個MvcRouteHandler了,因為MvcRouteHandler把路由系統連接到了MVC框架。但通過實現IRouteHandler介面,路由系統仍允許自定義自己的路由處理程式,如下麵的示例:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Routing; namespace UrlsAndRoutes.Infrastructure { public class CustomRouteHandler : IRouteHandler { public IHttpHandler GetHttpHandler(RequestContext requestContext) { return new CustomHttpHandler(); } } public class CustomHttpHandler : IHttpHandler { public bool IsReusable { get { return false; } } public void ProcessRequest(HttpContext context) { context.Response.Write("Hello"); } } }
IRouteHandler介面的目的是提供生成IHttpHandler介面的實現,且由它負責對請求進行處理。在該介面的MVC實現中,主要負責這幾項工作:查找控制器、調用動作方法、渲染視圖,並將結果寫入到響應中。當然,這裡的實現要簡單的多,此處僅將單詞“Hello”寫到客戶端,且只是文本形式。要想得到最終效果,需要在RouteConfig.cs文件中註冊這個自定義處理程式:
public static void RegisterRoutes(RouteCollection routes) { // 註冊自定義路由處理程式 routes.Add(new Route("SayHello", new CustomRouteHandler())); }
使用區域
MVC框架支持將Web應用程式組織成一些區域(Area),每個區域代表應用程式的一個功能端,如管理、結算、客戶支持等等。這使得代碼的管理很有用,尤其是大型項目,如果對所有控制器、視圖和模型只使用一組文件夾,那將會是很難於管理的。
創建區域
可以直接對項目右鍵,選擇“添加”->“區域”進行添加。還可以在當前的區域中創建其他區域。在剛剛的操作之後,項目中將會出現如下這樣的區域文件夾結構:
通過Areas/Admin文件夾,可以看出這是一個小型的MVC項目。其中有“Controllers”、“Models”和“Views”的文件夾。前兩個是空的,但“Views”文件夾含有一個“Shared”文件夾和一個Web.config視圖引擎配置文件(這裡暫不對視圖引擎進行討論)。
另外,這裡還多了一個AdminAreaRegistration.cs文件,如:
using System.Web.Mvc; namespace UrlsAndRoutes.Areas.Admin { public class AdminAreaRegistration : AreaRegistration { public override string AreaName { get { return "Admin"; } } public override void RegisterArea(AreaRegistrationContext context) { context.MapRoute( "Admin_default", "Admin/{controller}/{action}/{id}", new { action = "Index", id = UrlParameter.Optional } ); } } }
從清單中可以看出,該類中的RegisterArea方法註冊了一個URL模式為Admin/{controller}/{action}/{id}的路由。當然,也可以在該方法中定義該區域專用的其他路由。
註意:如果要給路由賦名,必須確保這些名稱在整個應用程式而不僅僅是某一區域中是唯一的。
由於在Global.asax的Application_Start方法中已經對路由的註冊進行過處理,因此,不需要在開發的過程中採取其他措施來確保該註冊方法會被調用:
public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); } }
上面代碼中對靜態方法AreaRegistration.RegisterAllAreas的調用,會導致MVC框架對應用程式的所有類進行遍歷,找出派生於AreaRegistration的所有類,並調用這些類上的RegisterArea方法。
註意:不用修改Application_Start方法中與路由相關的語句順序。如果在AreaRegistration.RegisterAllAreas之前調用RegisterRoutes,那麼會在區域路由之前定義路由。由於路由系統是按順序評估的,這意味著對區域控制器的請求有可能會用不正確的路由進行匹配。
註:AreaRegistrationContext類中的MapRoute方法會自動把註冊的路由限制到包含該區域控制器的命名空間。也就是說,當某區域創建控制器時,必須把它放在其預設的命名空間中;否則,路由系統將無法找到它。
填充區域
在上一節“創建區域”一節中,已經知道在一個區域中可以創建控制器、視圖以及模型等。下麵,通過創建一個名為HomeController的控制器類,來演示應用程式中區域之間的分離:
在下圖中的Controllers文件夾中右鍵添加一個空的控制器:HomeController
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace UrlsAndRoutes.Areas.Admin.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); } } }
為了演示,對該控制器中Index動作方法右擊,並添加相應的視圖,添加後的視圖將在:Areas/Admin/View/Home路徑中。
視圖內容如下:
@{ ViewBag.Title = "Index"; Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-with" /> <title>Index</title> </head> <body> <div> <h2>Admin Area Index</h2> </div> </body> </html>
從上面介紹可以看出,在一個區域內的工作方式與在一個MVC項目主區中工作是相當類似的。在項目中創建某個項的工作流也是相同的。其效果如下(導航路徑:Admin/Home/Index):
解析不明確的控制器問題
區域可能不像它們所展示的那樣是自包含的。當一個區域被註冊時,所定義的任何路由都被限制到與這個區域關聯的命名空間之中。這也是能夠請求/Admin/Home/Index,並得到WorkingWithAreas.Admin.Controllers命名空間中HomeController類的原因。
然而,在RouteConfig.cs的RegisterRoutes方法中定義的路由卻不受類似的限制。作為提醒,這裡給出了示例應用程式此時的路由配置:
public static void RegisterRoutes(RouteCollection routes) { routes.Add(new Route("SayHello", new CustomRouteHandler())); routes.Add(new LegacyRoute("~/articles/Windows_3.1_Overview.html", "~/old/.NET_1.0_Class_Library")); routes.MapRoute("MyRoute", "{controller}/{action}"); routes.MapRoute("MyOtherRoute", "App/{action}", new { controller = "Home" }); }
名為“MyRoute”的路由把來自瀏覽器的輸入URL轉換為Home控制器上的Index動作。這時會收到一個錯誤的消息,因為沒有為這條路由設置命名空間的約束,所以MVC框架會看到兩個HomeController類。為瞭解決這一問題,需要在所有可能導致衝突的路由中,將主控制器命名空間列為優先,如:
public static void RegisterRoutes(RouteCollection routes) { routes.Add(new Route("SayHello", new CustomRouteHandler())); routes.Add(new LegacyRoute("~/articles/Windows_3.1_Overview.html", "~/old/.NET_1.0_Class_Library")); routes.MapRoute("MyRoute", "{controller}/{action}",null,new[] {“UrlsAndRoutes.Controllers”}); routes.MapRoute("MyOtherRoute", "App/{action}", new { controller = "Home" }, new[] {“UrlsAndRoutes.Controllers”}); }
上面代碼中加粗部分將項目控制器作為了優先。當然也可以對某個區域中的控制器實現優先。
生成對區域動作的鏈接
對與同一區域中的動作,無需採取特殊的步驟來創建指向這些動作的鏈接。MVC框架會檢測當前請求涉及的特定區域,然後出站URL生成將只在該區域定義的路由中查找一個匹配。如將下麵代碼添加到Admin區域的視圖。
@Html.ActionLink("Click me", "About")
會生成以下HTML:
<a href=”/Admin/Home/About”>Click me</a>
為了對不同區域中的動作或根本無區域的動作創建一條鏈接,必須創建一個名為“area”的變數,並用它指定區功能變數名稱,如:
@Html.ActionLink("Click me to go to another area", "Index", new { area = "Support" })
因此,area被保留為片段變數名。假設創建了名為Support的區域,並有對應的標準路由定義,則將生成如HTML:
<a href=”/Support/Home”>Click me to go to another area</a>
如果想鏈接到頂級控制器(/Controllers文件夾中的一個控制器)上的一個動作,那麼應該把area指定為空字元串,如:
@Html.ActionLink("Click me to go to another area", "Index", new {area = ""})
URL方案最佳做法
1、 使URL整潔和人性化
下麵摘抄一些生成友好URL的簡單的綱要:
- 設計URL來描述它們的內容,而不是應用程式的實現細節。使用/Articles/AnnualReport,而不是使用/Website_v2/CachedContentServer/FromCache/AnnualReport。
- 儘可能採用內容標題而不是ID號,使用/Articles/AnnualReport,而不是/Articles/2392。如果必須使用一個ID號(以區別具有同樣標題的條目或避免通過標題查找一個條目時,需要多餘的資料庫查詢步驟),那麼兩者都有(如:/Articles/2392/AnnualReport)。這需要多打一些字元,但更要意義,並會改善搜索引擎排列。
- 不用對HTML頁面使用文件擴展名(如,.aspx或.mvc),但對特殊文件類型要用擴展名(如,.jpg、.pdf、.zip等)。如果是適當的設置了MIME類型,Web瀏覽器不會在意文件的擴展名,但人們卻希望對PDF文件用.pdf擴展名。
- 創建一種層次感(如:/Products/Menswear/Shirts/Red),這樣,容易讓人猜出父目錄的URL。
- 不區分大小寫。ASP.NET路由系統預設是不區分大小寫的。
- 避免使用符合、代碼和字元序列。需要用單詞分隔符時,可以使用短橫(如:/my-great-article)。下劃線是不友好的,而URL編碼的空格是奇特的(/my+great+article)或令人討厭的(/my%20great%20article)。
- 不用修改URL。打破鏈接等於失去商務。當確實需要修改URL時,通過永久重定向(301)儘可能長時間的繼續支持舊式的URL方案。
- 具有一致性。在整個應用程式中採用一種URL格式。URL應簡短、易於輸入、可剪輯(人性化可剪輯),且持久穩定,而且它們應該形象化網站結構。
2、GET和POST:選用正確的一個
一般來說,GET請求應該被用於所有隻讀信息檢索,而POST請求應該被用於各種修改應用程式狀態的操作。用標準的術語說,GET請求用於安全交互(除信息檢索外無其他影響),而POST請求用於不安全交互(作出決定或修改某些東西)。GET請求是可設定地址的——所有信息都包含在URL中,因此它可以設為書簽並鏈接到這些地址。(這些約定是由全球互聯網聯盟(W3C)在http://www.w3.org/Products/rfc2616/rfc2616-sec9.html上設定的)