ASP.NET MVC:自定義 Route 以生成小寫的 Url

作者: 鶴沖天  來源: 博客園  發布時間: 2010-11-11 23:13  閱讀: 2839 次  推薦: 0   原文鏈接   [收藏]  

  先給出本文中測試用的 controller:

public class PersonsController : Controller
{
    public ActionResult Query(string name)
    {
        return View();
    }
}

  ASP.NET 中 Url 大小寫

  不嚴格來講,ASP.NET MVC 對 Url 是不敏感的,以下 Url 都是相同的,都可以訪問到 PersonController 的 Query 方法:

  1. ~/Persons/Query
  2. ~/PERSONS/QUERY
  3. ~/persons/query

  但對 MVC 的數據綁定來說,大小寫似乎還是有區別的:

  1. ~/Persons/Query?Name=Bob
  2. ~/Persons/Query?Name=bob

  對以上兩個 Url,Query 中 name 參數會接收到兩個不同值:Bobbob。Action 中的參數只是原樣接收,并沒有作任何處理。至于name 字符串的大小寫是否敏感要看具體的應用了。

  再回頭看前面的三個 Url:

  1. ~/Persons/Query: 是 MVC 中默認生成的,因為在 .Net 中方法命名通常采用 PascalCase;
  2. ~/PERSONS/QUERY: 全部大寫,這種寫法很不友好,很難讀,應該杜絕采用這種方式;
  3. ~/persons/query:這種方式比較好,易讀,也是大多數人選擇的方式。

  本文探討如何在 MVC 中使用第三種方式,也就是小寫(但不完全小寫),目標如下:

  在不影響程序正常運行的前提下,將所有能小寫的都小寫,如:

  ~/persons/query?name=Bob&age=18

  ~/persons/query/name/Bob/age/18

  MVC 中 Url 的生成

  在 View 中生成超級鏈接有多種方式:

<%: Html.ActionLink("人員查詢", "Query", "Persons", new { name = "Bob" }, null) %>
<%: Html.RouteLink("人員查詢", new { controller = "Persons", action = "Query", name = "Bob" })%>
<a href="<%:Url.Action("Query", "Persons", new { name="Bob" }) %>">人員查詢</a>

  在 Action 中,可以使用 RedirectTo 來調轉至新的頁面:

return RedirectToAction("Query", "Persons", new { name = "Bob" });
return RedirectToRoute(new { controller = "Persons", action = "Query", name = "Bob" });

  ActionLink、RouteLink、RedirectToAction 和 RedirectToRouter 都會生成 Url,并最終顯示在瀏覽器的地址欄中。

  這四個方法都有很多重載,想從這里下手控制 Url 小寫實在是太麻煩了。當然也不可行,因為可能還有其它方式來生成 Url。

  MVC 是一個非常優秀的框架,但凡優秀的框架都會遵循 DRY(Don't repeat yourself) 原則,MVC 也不例外。MVC 中 RouteBase 負責 Url 的解析和生成:

public abstract class RouteBase
{
    public abstract RouteData GetRouteData(HttpContextBase httpContext);
    public abstract VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values);
}

  GetRouteData 用來解析 Url,GetVirtualPath 用來生成 Url。ActionLink、RouteLink、RedirectToAction 和 RedirectToRouter 內部都會調用 GetVirtualPath 方法來生成 Url。

  因此我們的入手點就是 GetVirtualPath 方法。

  自定義 Route 以生成小寫的 Url

  MVC 中 RouteBase 的具體實現類是 Route,我們經常在 Global.asax 中經常使用:

public class MvcApplication : System.Web.HttpApplication
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapRoute(
            "Default", // Route name
            "{controller}/{action}/{id}", // URL with parameters
            new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
        );
    }
    //...
}

  MapRoute 返回 Route,MapRoute 有很多重載,用來簡化我們構建 Route 的過程。

  Route 類沒有給我們提供可直接擴展的地方,因此我們只能自定義一個新的 Route 來實現我們的小寫 Url。但處理路由的細節也是相當麻煩的,因此我們最簡單的方式就是寫一個繼承自 Route 的類,然后重寫它的 GetVirtualPath 方法:

public class LowerCaseUrlRoute : Route
{
    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        //在此處進行小寫處理
        return base.GetVirtualPath(requestContext, values);
    }
}

  再來看下我們的目標:

  ~/persons/query?name=Bob&age=18

  ~/persons/query/name/Bob/age/18

  其實我們只需要進行兩步操作:

  1. 將路由中的 area、controller、action 的值都變成小寫;
  2. 將路由中其它鍵值對的鍵變成小寫,如:Name=Bob 中的 Name。

  那我們先來完成這個功能吧:

private static readonly string[] requiredKeys = new [] { "area", "controller", "action" };

private void LowerRouteValues(RouteValueDictionary values)
{
    foreach (var key in requiredKeys)
    {
        if (values.ContainsKey(key) == false) continue;

        var value = values[key];
        if (value == null) continue;

        var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
        if (valueString == null) continue;

        values[key] = valueString.ToLower();
    }

    var otherKyes = values.Keys
        .Except(requiredKeys, StringComparer.InvariantCultureIgnoreCase)
        .ToArray();

    foreach (var key in otherKyes)
    {
        var value = values[key];
        values.Remove(key);
        values.Add(key.ToLower(), value);
    }
}

  GetVirtualPath 生成 Url 時,會將 requestContext.RouteData.Values、values(第二個參數) 以及 Defaults(當前 Router 的默認值)三個 RouteValueDictionary 進行合并,如在 View 寫了如下的一個 ActionLinks:

<%: Html.ActionLink("查看") %>

  生成的 Html 代碼可能是:

<a href="/Home/Details">查看</a>

  因為沒有指定 Controller,MVC 會自動使用當前的,即從 requestContext.RouteData.Values 中獲取 Controller,得到 ”Home“;”Details“來自 values;如果連 ActionLink 中 Action 也不指定,那將會從 Defaults 中取值。

  因此我們必須將這三個 RouteValueDictionary 都進行處理才能達到我們的目標:

public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
    LowerRouteValues(requestContext.RouteData.Values);
    LowerRouteValues(values);
    LowerRouteValues(Defaults);
    return base.GetVirtualPath(requestContext, values);
}

  再加上幾個構造函數,完整的 LowerCaseUrlRoute 如下:

public class LowerCaseUrlRoute : Route
{
    private static readonly string[] requiredKeys = new [] { "area", "controller", "action" };

    public LowerCaseUrlRoute(string url, IRouteHandler routeHandler)
        : base(url, routeHandler) { }
    
    public LowerCaseUrlRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler)
        : base(url, defaults, routeHandler){ }

    public LowerCaseUrlRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler)
        : base(url, defaults, constraints, routeHandler) { }
    public LowerCaseUrlRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler)
        : base(url, defaults, constraints, dataTokens, routeHandler) { }    

    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        LowerRouteValues(requestContext.RouteData.Values);
        LowerRouteValues(values);
        LowerRouteValues(Defaults);
        return base.GetVirtualPath(requestContext, values);
    }

    private void LowerRouteValues(RouteValueDictionary values)
    {
        foreach (var key in requiredKeys)
        {
            if (values.ContainsKey(key) == false) continue;

            var value = values[key];
            if (value == null) continue;

            var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
            if (valueString == null) continue;

            values[key] = valueString.ToLower();
        }

        var otherKyes = values.Keys
            .Except(requiredKeys, StringComparer.InvariantCultureIgnoreCase)
            .ToArray();

        foreach (var key in otherKyes)
        {
            var value = values[key];
            values.Remove(key);
            values.Add(key.ToLower(), value);
        }
    }
}

  有了 LowerCaseUrlRoute,我們就可以修改 Global.asax 文件中的路由了。

  創建 LowerCaseUrlRouteMapHelper

  這一步不是必須的,但有了這個 MapHelper 我們在修改 Global.asax 文件中的路由時可以非常方便:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapLowerCaseUrlRoute( //routes.MapRoute(
        "Default", // Route name
        "{controller}/{action}/{id}", // URL with parameters
        new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
    );
}

  尤其是已經配置了很多路由的情況下,其代碼如下:

public static class LowerCaseUrlRouteMapHelper
{
    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this RouteCollection routes, string name, string url){
        return routes.MapLowerCaseUrlRoute(name, url, null, null);
    }
    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this RouteCollection routes, string name, string url, object defaults){
        return routes.MapLowerCaseUrlRoute(name, url, defaults, null);
    }
    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this RouteCollection routes, string name, string url, string[] namespaces){
        return routes.MapLowerCaseUrlRoute(name, url, null, null, namespaces);
    }
    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this RouteCollection routes, string name, string url, object defaults, object constraints){
        return routes.MapLowerCaseUrlRoute(name, url, defaults, constraints, null);
    }
    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this RouteCollection routes, string name, string url, object defaults, string[] namespaces){
        return routes.MapLowerCaseUrlRoute(name, url, defaults, null, namespaces);
    }
    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces){
        if (routes == null) throw new ArgumentNullException("routes");
        if (url == null) throw new ArgumentNullException("url");
        LowerCaseUrlRoute route2 = new LowerCaseUrlRoute(url, new MvcRouteHandler());
        route2.Defaults = new RouteValueDictionary(defaults);
        route2.Constraints = new RouteValueDictionary(constraints);
        route2.DataTokens = new RouteValueDictionary();
        LowerCaseUrlRoute item = route2;
        if ((namespaces != null) && (namespaces.Length > 0))
            item.DataTokens["Namespaces"] = namespaces;
        routes.Add(name, item);
        return item;
    }

    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this AreaRegistrationContext context, string name, string url){
        return context.MapLowerCaseUrlRoute(name, url, null);
    }
    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this AreaRegistrationContext context, string name, string url, object defaults){
        return context.MapLowerCaseUrlRoute(name, url, defaults, null);
    }
    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this AreaRegistrationContext context, string name, string url, string[] namespaces){
        return context.MapLowerCaseUrlRoute(name, url, null, namespaces);
    }
    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this AreaRegistrationContext context, string name, string url, object defaults, object constraints)        {
        return context.MapLowerCaseUrlRoute(name, url, defaults, constraints, null);
    }
    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this AreaRegistrationContext context, string name, string url, object defaults, string[] namespaces){
        return context.MapLowerCaseUrlRoute(name, url, defaults, null, namespaces);
    }
    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this AreaRegistrationContext context, string name, string url, object defaults, object constraints, string[] namespaces)
    {
        if ((namespaces == null) && (context.Namespaces != null))
            namespaces = context.Namespaces.ToArray<string>();
        LowerCaseUrlRoute route = context.Routes.MapLowerCaseUrlRoute(name, url, defaults, constraints, namespaces);
        route.DataTokens["area"] = context.AreaName;
        bool flag = (namespaces == null) || (namespaces.Length == 0);
        route.DataTokens["UseNamespaceFallback"] = flag;
        return route;
    }
}

  總結

  大功告成,如果你感興趣,不妨嘗試下!寫到這里吧,如果需要,請下載本文中的示例代碼:MvcLowerCaseUrlRouteDemo.rar(209KB)如果你有其它辦法,歡迎交流!

0
0
 
標簽:ASP.NET MVC
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()