正如ASP.NET MVC名字所揭示的一樣,是以模型-視圖-控制設計模式構建在ASP.NET基礎之上的WEB應用程序,我們需要創建相應的程序類來協調處理,完成從客戶端請求到結果相應的整個過程:
VS2012中一個典型的MVC工程結構是這樣的:
Controllers文件夾下存放控制類,Models文件下是業務數據模型類,Views文件下則是類似於aspx的視圖文件。在傳統ASP.NET form的應用程序中,客戶端的請求最后都映射到磁盤上對應路徑的一個aspx的頁面文件,而MVC程序中所有的網絡請求映射到控制類的某一個方法,我們就從控制類說起,而在講控制類前,必須要講的是URL路由。
注冊URL路由
我們在瀏覽器中請求鏈接 http://mysite.com/Home/Index,MVC認為是這樣的URL模式(默認路徑映射):
{controller}/{action}
也就是說上面的請求會被映射到Home控制類的Index方法,MVC命名規則中控制類必須以Controller結尾,所以Home控制類應該是HomeController:
public class HomeController : Controller { public ActionResult Index() { return View(); } }
MVC是根據什么將上面的請求映射到控制類的相應方法的呢?答案就是路由表,Global.asax在應用程序啟動時會調用路由配置類來注冊路徑映射:
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); } }
路徑映射的配置類則在App_Start目錄下:
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 } ); } }
routes.MapRoute()添加了一個URL路由到路由表中,URL的映射模式是"{controller}/{action}/{id}",controller和action我們已經清楚,id則是請求中額外的參數,比如我們的請求可以是 http://mysite.com/Home/Index/3,對應的action方法可以是:
public ActionResult Index(int id=1) { return View(); }
在傳遞到Index方法時參數id會被賦值3(保存在RouteData.Values["id"]),MVC足夠智能來解析參數並轉化為需要的類型,MVC稱之為模型綁定(后續具體來看)。上面注冊路由時使用了默認參數:defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },如果我們在請求URL沒有指定某些參數,defaults參數會被用作默認值,比如:
mydomain.com = mydomain.com/home/index mydomain.com/home = mydomain/home/index mydomain.com/customer = mydomain/customer/index
id為可選參數,可以不包括在URL請求中,所以上面注冊的路徑可以映射的URL有:
mydomain.com mydomain.com/home mydomain.com/home/list mydomain.com/customer/list/4
除此之外不能映射的請求都會得到404錯誤,比如mydomain.com/customer/list/4/5,這里參數過多不能被映射。
RouteCollection.MapRoute()等同於:
Route myRoute = new Route("{controller}/{action}", new MvcRouteHandler()); routes.Add("MyRoute", myRoute);
這里直接向Routes表添加一個Route對象。
其他一些URL映射的例子:
routes.MapRoute("", "Public/{controller}/{action}",new { controller = "Home", action = "Index" }); //URL可以包含靜態的部分,這里的public routes.MapRoute("", "X{controller}/{action}"); //所有以X開頭的控制器路徑,比如mydomain.com/xhome/index映射到home控制器 routes.MapRoute("ShopSchema", "Shop/{action}",new { controller = "Home" }); //URL可以不包含控制器部分,使用這里的默認Home控制器 routes.MapRoute("ShopSchema2", "Shop/OldAction",new { controller = "Home", action = "Index" }); //URL可以是全靜態的,這里mydomain.com/shop/oldaction傳遞到home控制器的index方法
一個比較特殊的例子:
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional });
這可以映射任意多的URL分段,id后的所有內容都被賦值到cathall參數,比如/Customer/List/All/Delete/Perm,catchall = Delete/Perm。
需要注意的是路徑表的注冊是有先后順序的,按照注冊路徑的先后順序在搜索到匹配的映射后搜索將停止。
命名空間優先級
MVC根據{controller}在應用程序集中搜索同名控制類,如果在不同命名空間下有同名的控制類,MVC會給出多個同名控制類的異常,我們可以在注冊路由的時候指定搜索的命令空間:
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional , new[] { "URLsAndRoutes.AdditionalControllers" });
這里表示我們將在"URLsAndRoutes.AdditionalControllers"命名空間搜索控制類,可以添加多個命名空間,比如:
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new[] { "URLsAndRoutes.AdditionalControllers", "UrlsAndRoutes.Controllers"});
"URLsAndRoutes.AdditionalControllers", "UrlsAndRoutes.Controllers"兩個命名空間是等同處理沒有優先級的區分,如果這兩個空間里有重名的控制類一樣導致錯誤,這種情況可以分開注冊多條映射:
routes.MapRoute("AddContollerRoute", "Home/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new[] { "URLsAndRoutes.AdditionalControllers" }); routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new[] { "URLsAndRoutes.Controllers" });
路由限制
除了在注冊路由映射時可以指定控制器搜索命名空間,還可以使用正則表達式限制路由的應用范圍,比如:
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[] { "URLsAndRoutes.Controllers" });
這里限制MyRoute路由僅用於映射所有H開頭的控制類、且action為Index或者About、且HTTP請求方法為GET的客戶端請求。
如果標准的路由限制不能滿足要求,可以從IRouteConstraint接口擴展自己的路由限制類:
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); } }
在注冊路由時這樣使用:
routes.MapRoute("ChromeRoute", "{*catchall}", new { controller = "Home", action = "Index" }, new { customConstraint = new UserAgentConstraint("Chrome")}, new[] { "UrlsAndRoutes.AdditionalControllers" });
這表示我們限制路由僅為瀏覽器Agent為Chrome的請求時使用。
路由到磁盤文件
除了控制器方法,我們也需要返回一些靜態內容比如HTML、圖片、腳本到客戶端,默認情況下路由系統優先檢查是否有和請求路徑一致的磁盤文件存在,如果有則不再從路由表中匹配路徑。我們可以通過配置顛倒這個順序:
public static void RegisterRoutes(RouteCollection routes) { routes.RouteExistingFiles = true; ... ....
還需要修改web配置文件:
<add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" preCondition=""/>
這里設置preCondition為空。如果我們再請求一些靜態內容比如~/Content/StaticContent.html時會優先從路徑表中匹配。
而如果我們又需要忽略某些路徑的路由匹配,可以:
... public static void RegisterRoutes(RouteCollection routes) { routes.RouteExistingFiles = true; routes.IgnoreRoute("Content/{filename}.html");
...
它會在RouteCollection中添加一個route handler為StopRoutingHandler的路由對象,在匹配到content路徑下的后綴為html的文件時停止繼續搜索路徑表,轉而匹配磁盤文件。
生成對外路徑
路徑表注冊不僅影響到來自於客戶端的URL映射,也影響到我們在視圖中使用HTML幫助函數生成對外路徑,比如我們注冊了這樣的映射
public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }); } ...
在視圖中調用Html.ActionLink生成一個對外路徑:
<div> @Html.ActionLink("This is an outgoing URL", "CustomVariable") </div>
根據我們當前的請求鏈接,http://localhost:5081/home,生成的outgoing鏈接為:
<a href="/Home/CustomVariable">This is an outgoing URL</a>
而如果我們調整路徑表為:
... 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 }); } ...
“@Html.ActionLink("This is an outgoing URL", "CustomVariable") ”得到的結果是:
<a href="/App/DoCustomVariable">This is an outgoing URL</a>
它將使用在路徑表中找到的第一條匹配的記錄來生成相應的鏈接路徑。
Html.ActionLink()有多個重載,可以多中方式生成URL鏈接:
@Html.ActionLink("This targets another controller", "Index", "Admin") //生成到Admin控制器Index方法的鏈接 @Html.ActionLink("This is an outgoing URL", "CustomVariable", new { id = "Hello" }) //生成額外參數的鏈接,比如上面的路徑配置下結果為“href="/App/DoCustomVariable?id=Hello"”;如果路徑映射為 "{controller}/{action}/{id}",結果為href="/Home/CustomVariable/Hello" @Html.ActionLink("This is an outgoing URL", "Index", "Home", null, new {id = "myAnchorID", @class = "myCSSClass"}) //設定生成A標簽的屬性,結果類似“<a class="myCSSClass"href="/" id="myAnchorID">This is an outgoing URL</a> ”
參數最多的調用方式是:
@Html.ActionLink("This is an outgoing URL", "Index", "Home", "https", "myserver.mydomain.com", " myFragmentName", new { id = "MyId"}, new { id = "myAnchorID", @class = "myCSSClass"})
得到的結果是:
<a class="myCSSClass" href="https://myserver.mydomain.com/Home/Index/MyId#myFragmentName" id="myAnchorID">This is an outgoing URL</a>
Html.ActionLink方法生成的結果中帶有HTML的<a>標簽,而如果只是需要URL,可以使用Html.Action(),比如:
@Url.Action("Index", "Home", new { id = "MyId" }) //結果為單純的/home/index/myid
如果需要在生成URL指定所用的路徑記錄,可以:
@Html.RouteLink("Click me", "MyOtherRoute","Index", "Customer") //指定使用路徑注冊表中的MyOtherRoute記錄
上面講的都是在Razor引擎視圖中生成對外URL,如果是在控制器類中我們可以:
string myActionUrl = Url.Action("Index", new { id = "MyID" }); string myRouteUrl = Url.RouteUrl(new { controller = "Home", action = "Index" });
更多的情況是在控制類方法中需要轉到其他的Action,我們可以:
... public RedirectToRouteResultMyActionMethod() { return RedirectToAction("Index"); } ... public RedirectToRouteResult MyActionMethod() { return RedirectToRoute(new { controller = "Home", action = "Index", id = "MyID" }); } ...
創建自定義ROUTE類
除了使用MVC自帶的Route類,我們可以從RouteBase擴展自己的Route類來實現自定義的路徑映射:
public class LegacyRoute : RouteBase { private string[] urls; public LegacyRoute(params string[] targetUrls) { urls = 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) { VirtualPathData result = null; if (values.ContainsKey("legacyURL") && urls.Contains((string)values["legacyURL"], StringComparer.OrdinalIgnoreCase)) { result = new VirtualPathData(this, new UrlHelper(requestContext) .Content((string)values["legacyURL"]).Substring(1)); } return result; } }
GetRouteData()函數用於處理URL請求映射,我們可以這樣注冊路徑映射:
routes.Add(new LegacyRoute( "~/articles/Windows_3.1_Overview.html", "~/old/.NET_1.0_Class_Library"));
上面的例子中如果我們請求"~/articles/Windows_3.1_Overview.html"將被映射到Legacy控制器的GetLegacyURL方法。
GetVirtualPath()方法則是用於生成對外鏈接,在視圖中使用:
@Html.ActionLink("Click me", "GetLegacyURL", new { legacyURL = "~/articles/Windows_3.1_Overview.html" })
生成對外鏈接時得到的結果是:
<a href="/articles/Windows_3.1_Overview.html">Click me</a>
創建自定義ROUTE Handler
除了可以創建自定義的Route類,還可以創建自定義的Route handler類:
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"); } }
注冊路徑時使用自定義的Route handler:
routes.Add(new Route("SayHello", new CustomRouteHandler()));
其效果就是針對鏈接 /SayHello的訪問得到的結果就是“Hello”。
使用Area
大型的Web應用可能分為不同的子系統(比如銷售、采購、管理等)以方便管理,可以在MVC中創建不同的Area來划分這些子系統,在VS中右鍵點擊Solution exploer->Add->Area可以添加我們想要的區域,在Solution exploer會生成Areas/<區域名稱>的文件夾,其下包含Models、Views、Controllers三個目錄,同時生成一個AreaRegistration的子類,比如我們創建一個名為Admin的區域,會自動生成名為AdminAreaRegistration的類:
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 } ); } } }
它的主要作用是注冊一個到Admin/{controller}/{action}/{id}路徑映射,在global.asax中會通過AreaRegistration.RegisterAllAreas()來調用到這里的RegisterArea()來注冊區域自己的路徑映射。
在Area下創建Controller、視圖同整個工程下創建是相同的,需要注意的是可能遇到控制器重名的問題,具體解決參見命名空間優先級一節。
如果在視圖中我們需要生成到特定Area的鏈接,可以在參數中指定Area:
@Html.ActionLink("Click me to go to another area", "Index", new { area = "Support" })
如果需要得到頂級控制器的鏈接area=""留空即可。
以上為對《Apress Pro ASP.NET MVC 4》第四版相關內容的總結,不詳之處參見原版 http://www.apress.com/9781430242369。