理解MVC路由配置

  在上一篇文章中,我簡短的談了一下MVC的底層運行機制,如果對MVC還不是很瞭解的朋友,可以作爲入門的參照。接下來,我開始介紹關於URL路由的相關知識。URL路由不是MVC獨有的,相反它是獨立於MVC而單獨存在的(在System.Web.Routing下)。因此,URL路由也能爲傳統的ASP.NET應用程序服務。我用一個簡單的例子來解釋路由,在我們的上一節的"ASP.NET MVC應用程序"中添加一個新的頁面default.aspx,其後臺代碼如下:
複製代碼
protected void Page_Load(object sender, EventArgs e)
{
    var originalPath = Request.Path;
    HttpContext.Current.RewritePath(Request.ApplicationPath, false);
    IHttpHandler handler = new MvcHttpHandler();
    handler.ProcessRequest(HttpContext.Current);
    HttpContext.Current.RewritePath(originalPath, false);
}
複製代碼

當執行請求頁面default.aspx時,首先將請求的路徑重寫爲"/",而該路徑會匹配路由{controller}/{action}/{id},根據Global.asax路由配置,將映射爲Home/Index/,從而輸出Index.aspx的內容(而不是default.aspx的內容),不禁要問:爲什麼default.aspx在MVC應用程序中當做普通的ASP.NET頁面,而不是將該請求轉化爲路由執行?是不是在MVC中就不能使用普通的ASP.NET頁面呢?如果不能,當有一天我希望將我的普通站點轉化爲MVC站點時,是否還需要重新開發?帶着這些疑問,我們進一步來探討如何在MVC中支持原有的ASP.NET頁面。在Global.asax中的Application_Start()方法中添加以下代碼:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    RegisterRoutes(RouteTable.Routes);
    RouteTable.Routes.RouteExistingFiles = true;
}

再次運行default.aspx會出現以前錯誤頁面(找不到頁面)。


剛纔增加的代碼:RouteTable.Routes.RouteExistingFiles = true(默認爲false);表明MVC啓用對現有Web頁面的解析,此時儘管同樣解析到路由{controller}/{action}/{id}上,但卻找不到對應的控制器路由參數default.aspx,因而導致錯誤。接下來我們爲它添加相關的路由設置,在Global.asax中添加以下代碼:
//爲default.aspx添加路由
routes.MapRoute(
    "Start",
    "default.aspx",
    new { controller = "Home", action = "Index", id = UrlParameter.Optional } 
);

刪除或註釋掉default.aspx.cs中的代碼再次運行,會發現頁面能正常解析。需要說明的是,URL路由的排列順序非常關鍵,一旦找到第一個匹配的路由,將不會繼續尋找剩下的路由了。是因爲這裏找到了匹配的路由Start,從而正確的映射到Home下的視圖Index.aspx。爲了更加深入理解路由解析的匹配規則,我們引入"路由匹配監控器"。添加引用"RouteDebug.dll",並在Global.asax中註冊檢測代碼以分別檢測沒有啓用和啓用(依靠RouteExistingFiles屬性)ASP.NET頁面解析兩種情況。

複製代碼
protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    RegisterRoutes(RouteTable.Routes);
    //RouteTable.Routes.RouteExistingFiles = true;
    RouteDebugger.RewriteRoutesForTesting(RouteTable.Routes);
}
複製代碼

未啓用(默認)Web頁面解析的路由監控圖

啓用Web頁面解析的路由監控圖

從圖中我們發現,當啓用Web頁面解析時,比未啓用適多出一個路由項,這是因爲此時是從Global.asax中去尋找匹配的路由項。過程大概是這樣的:首先會到路由表集合中尋找,如果找到匹配的路由定義則解析路由定義中的各個參數,依次獲取參數映射的控制器/視圖/參數信息,最後定位到指定控制器下的指定視圖頁面進行輸出。相反,而過找不到對應的匹配路由則返回錯誤頁面。

     前面已提到過,URL路由的基本格式是: {controller}/{action}/{id},但也存在很多變體,總的來說,都會把不包含在{}中的部分當成常量來處理。如:blog/{action}/{author}的正確映射應該爲:blog/show/miracle,再比如{report}/{year}/{month}/{day}映射爲:sale/2012/5/25。這樣就非常好理解頁面具有的功能。開發者一般都會通過Global.asax文件的Application_Start()來設置URL路由的定義,通過靜態類RouteTable的Routes屬性來設置路由。

複製代碼
public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.Add(new Route("blog/{action}/{author}", new BlogRouteHandler()));
}

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    RegisterRoutes(RouteTable.Routes);
}
複製代碼

我們可以看出在路由中定義了兩個路由參數:action和author。當用戶發送URL請求http://server/blog/show/miracle時,根據路由的定義將進行映射,使得action映射爲show,author映射爲miracle,如果當用戶發送請求http://server/blog/add時,由於找不到對應的路由也沒有默認路由,此時URL路由將不會進行處理,而是作爲普通的頁面交由ASP.NET應用程序處理。在添加URL路由的同時,還可以設置路由參數的默認值。

複製代碼
public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.Add(new Route("blog/{action}/{author}", new BlogRouteHandler())
    {
        Defaults = new RouteValueDictionary
        {
            {"action", "show"},
            {"author", "miracle"}
        }
    });
}
複製代碼

現在用戶就可以發送更多的請求了,如: http://server/blog將映射到http://server/blog/show/miracle上,http://server/blog/add將映射到http://server/blog/add上,而http://server/blog/add/miraclehe將映射到http://server/blog/add/miraclehe上。簡單總結一句:如果對應的路由參數沒有填充值的話,則使用默認值,否則使用對應的路由參數值。有時,爲了適應一類URL的定義,可以用通配符(*)來表示。如:blog/query/{*values},對應的映射可以包含如下:/blog/query/miracle/2012/05,/blog/query/miracle或/blog/query等。可能還需要爲路由參數添加約束,以更好的滿足參數的準確性。如:

複製代碼
public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.Add(new Route("blog/{locale}/{year}", new BlogRouteHandler())
    {
        Constraints = new RouteValueDictionary
        {
            {"locale", "{a-z}{2}-{A-Z}{2}"},
            {"year", @"\d{4}"}
        }
    });
}
複製代碼

對應的映射爲:/blog/en-US/2012,而/blog/en-us/2012,/blog/en-US/12均不符合要求。介紹了關於默認值和約束的相關知識後,我們來看看Route類定義,主要包含以下屬性:默認值Defaults、約束Constraints、命名空間DataTokens、URL路由以及路由處理程序RouteHandler,以下是其構造函數列表。

public Route(string url, IRouteHandler handler);
public Route(string url, RouteValueDictionary defaults, IRouteHandler handler);
public Route(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler handler);
public Route(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler handler);

可以看出,最簡單的構造函數只需要URL路由和路由處理程序,最複雜的則包含所有屬性。

var route = new Route("blog/{action}/{author}", 
                       new RouteValueDictionary { {"action", "show" }, {"author", "miracle"} },
                       new MvcRouteHandler());

可將(多個)路由添加到路由集合RouteCollection中,通過RouteTable.Routes屬性來維護路由集合。

public static void RegisterRoutes(RouteCollection routes)
{
    routes.Add(new Route("blog/{action}/{author}", 
                       new RouteValueDictionary { {"action", "show" }, {"author", "miracle"} },
                       new MvcRouteHandler()));
}

大家可能會發現,添加路由有MapRoute和Add兩個方法,那到底有什麼區別的。MapRoute是RouteCollection的擴展方法,同時還有IngnoreRoute,而Add則是實例方法,相對來說要使用Add來調用比較複雜(包含剛纔提到的5大屬性),而MapRoute則相對簡潔。

複製代碼
public static void RegisterRoutes(RouteCollection routes)
{
    routes.MapRoute(
        "Article", // 路由名稱
        "blog/{action}/{author}", // 帶有參數的 URL
        new { controller = "Home", action = "show", author = "miracle" } 
    );
}
複製代碼

細心的朋友會發現,在MapRoute中指定了路由名稱(可選),到底有什麼用呢?先看以下生成鏈接(在下一篇文章中將提到)的代碼:

<%= Html.RouteLink("Miracle's Blog", "Article", new { action = "compose" }) %>

此時在URL路由匹配時就直接按照名稱解析,同時可以不用指定默認的路由參數,而不用遍歷匹配以提高匹配效率。另一種優化路由的方式是將最常見的路由存放在路由表的最前面(只要找到第一個則不再尋找)。

     有時,我們需要自定義路由約束(需要實現IRouteConstraint的Match方法)以滿足更復雜的路由需求。假定路由爲: blog/miracle/{year}/{month}/{day}。此時year參數的參考值:2000~2012,month爲:1~12, day爲:28、29、30或31(根據月份而定)。分別添加YearRouteConstraint類、MonthRouteConstraint類和DayRouteConstraint類。

日期路由約束
複製代碼
public class YearRouteConstraint : IRouteConstraint
{
    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (routeDirection == RouteDirection.IncomingRequest &&
            parameterName.ToLower() == "year")
        {
            var year = 0;
            if (int.TryParse(values["year"].ToString(), out year))
            {
                return year >= 2000 && year <= 2012;
            }
            return false;
        }
        return false;
    }
}

public class MonthRouteConstraint : IRouteConstraint
{
    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (routeDirection == RouteDirection.IncomingRequest &&
            parameterName.ToLower() == "month")
        {
            var month = 0;
            if (int.TryParse(values["month"].ToString(), out month))
            {
                return month >= 1 && month <= 12;
            }
            return false;
        }
        return false;
    }
}

public class DayRouteConstraint : IRouteConstraint
{
    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (routeDirection == RouteDirection.IncomingRequest &&
            parameterName.ToLower() == "day")
        {
            var month = 0;
            if (!int.TryParse(values["month"].ToString(), out month)) return false;
            if (month <= 0 || month > 12) return false;

            var day = 0;
            if (!int.TryParse(values["day"].ToString(), out day)) return false;

            switch (month)
            {
                case 1:
                case 3:
                case 5:
                case 7:
                case 8:
                case 10:
                case 12:
                    return day <= 31;
                case 4:
                case 6:
                case 9:
                case 11:
                    return day <= 31;
                case 2:
                    return day <= 28;//不計閏年
            }
        }
        return false;
    }
複製代碼

接下來,我們可以來利用自定義約束配置路由。

複製代碼
//配置自定義約束
routes.MapRoute("Article", "blog/{year}/{month}/{day}",
                new
                {
                    controller = "blog",
                    action = "show",
                    author = "miracle",
                    year = "",
                    month = "",
                    day = ""
                },
                new 
                {
                    year = new YearRouteConstraint(),
                    month = new MonthRouteConstraint(),
                    day = new DayRouteConstraint()
                });
複製代碼

現在如果運行http://server/blog/show/miracle/2012/4/31將報錯,因爲不符合日期約束。

      接下來,我簡要總結一下路由解析流程。當用戶輸入URL地址發送請求時,UrlRoutingModule類就會到路由表(RouteTable)中解析與請求匹配的路由,然後將該路由分發到路由處理程序(IRouteHandler),並連同RequestContext一起再次分發到Mvc處理程序(IHttpHandler),定位相關的控制器並執行相關的動作實現輸出。以下的整個過程的示意圖。

      最後,我順便提一下如何在傳統的Web站點中使用路由(一般情況下用戶將傳統站點轉化爲MVC站點的項目遷移過渡)。主要包含以下兩個步驟:

      1.常見實現IRouteHandler接口的WebFormRouteHandler類,返回實現IHttpHandler接口的實例化對象(實際上任何一個Page都是一個IHttpHandler實例對象)。

複製代碼
public class WebFormRouteHandler : IRouteHandler
{
    public string VirtualPath { get; private set; }
    //初始化虛擬路徑
    public WebFormRouteHandler(string virtualPath)
    {
        this.VirtualPath = virtualPath;
    }
    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        //創建實例化的頁面對象
        var page = BuildManager.CreateInstanceFromVirtualPath(VirtualPath, typeof(Page)) as IHttpHandler;
        return page;
    }
}
複製代碼

       2.配置全局應用程序類(Global.asax),實現路由到傳統Web Form的映射。

//映射傳統的web站點
routes.Add("show", new Route("blog/show/{author}", new WebFormRouteHandler("~/pages/show.aspx")));
routes.Add("compose", new Route("blog/compose/{year}/{month}/{day}", new WebFormRouteHandler("~/pages/compose.aspx")));

然後在default.aspx中添加兩個鏈接:

<div>
    <a href="blog/show/miracle">blog/show/miracle</a>
    <a href="blog/compose/2012/5/28">blog/compose/2012/5/28</a>
</div>

點擊對應的鏈接就會進入相關的頁面。通過以上的學習,我們發覺可以設置個性化的路由(源代碼在此下載),但請讀者考慮如何驗證被映射頁面的安全性,我將以後的章節中詳細講解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章