天天看點

ASP.NET MVC 4 (一)路徑映射

正如ASP.NET MVC名字所揭示的一樣,是以模型-視圖-控制設計模式建構在ASP.NET基礎之上的WEB應用程式,我們需要建立相應的程式類來協調處理,完成從用戶端請求到結果相應的整個過程:

ASP.NET MVC 4 (一)路徑映射
VS2012中一個典型的MVC工程結構是這樣的:
ASP.NET MVC 4 (一)路徑映射
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。