Mvc Custom Route和Breadcrumb策略?

时间:2013-12-18 16:12:09

标签: asp.net-mvc asp.net-mvc-routing breadcrumbs mvcsitemapprovider

我正在创建一个ASP.net MVC / Entity Framework购物车,以便更熟悉该技术。我想要的功能之一是基于关闭独特的slugs而不是将实体ID嵌入到URL中的URL。一些例子:

  • /
  • /信息
  • /信息/约-我们
  • /信息/接触我们
  • /男士服装
  • /男士服装/男士衬衫
  • /男士服装/男士衬衫/测试的T恤

slugs在所有内容类型中都是唯一的,但是例如,test-tshirt可以出现在多个类别中:

  • /男士服装/男士衬衫/测试的T恤
  • /男士服装/清除/测试的T恤

我创建了一个自定义路由,它接受路径中的最后一个slug并使用它来查找当前页面。

public class SlugRoute : RouteBase
{
    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        string path = HttpContext.Current.Request.Path.TrimStart('/').TrimEnd('/');

        if (string.IsNullOrEmpty(path))
            path = "home";

        string[] slugs = path.Split('/');

        string slug = slugs[slugs.Length - 1];

        CatalogPage page = Token.Instance.DB.Pages.SingleOrDefault(p => p.UrlSlug == slug);
        if (page != null)
        {
            // Cache current page in context
            HttpContext.Current.Items["CurrentPage"] = page;

            // Set up route data
            RouteData data = new RouteData(this, new MvcRouteHandler());
            data.Values["action"] = "Index";
            data.Values["id"] = page.Id;
            data.DataTokens.Add("namespaces", new string[] { "MyProject.Presentation.Controllers" });

            // Set controller value if specified in db, or set based on entity type
            if (!string.IsNullOrEmpty(page.Controller))
                data.Values["controller"] = page.Controller;
            else if (page.GetUnproxiedType() == typeof(CategoryPage))
                data.Values["controller"] = "Category";
            else if (page.GetUnproxiedType() == typeof(ProductPage))
                data.Values["controller"] = "Product";
            else
                data.Values["controller"] = "Content";
            return data;
        }       
        return null;
    }

    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        return null;
    }
}

这很好用,我在实现自定义逻辑和显示模板方面有很大的灵活性(内容类型也有“View”属性,所以我可以动态设置控制器中的视图。)

然而,在实现面包屑方面,我偶然发现了一点。快速而肮脏的方法是使用URL中的路径,对路径中的每个slug进行查询,并忽略页面是否实际上是该类别的子节点。另一个解决方案是使用类似MvcSiteMapProvider的东西并在后端添加内容时构建XML树...我不确定这个特定的实现是如何工作的,因为它似乎非常关注标准{controller} / {action} / {id}路由模式。

您使用过或看过哪些其他类型的实现?

1 个答案:

答案 0 :(得分:1)

MvcSiteMapProvider v4也可以通过设置Url属性而不是使用{controller} / {action} / {id}来处理URL。这正是我使用它(数据库驱动的URL /自定义RoutBase派生路由)的场景,它运行良好。但是,您也应该在路由中实现反向URL查找,否则您的URL解析将无效。

public class ProductRoute
    : RouteBase, IRouteWithArea
{
    private readonly string area;
    private readonly IApplicationContext appContext;
    private readonly IRouteUrlProductListFactory routeUrlProductListFactory;
    private readonly IRouteUtilities routeUtilities;

    public ProductRoute(
        string area,
        IApplicationContext appContext,
        IRouteUrlProductListFactory routeUrlProductListFactory,
        IRouteUtilities routeUtilities
        )
    {
        if (appContext == null) { throw new ArgumentNullException("appContext"); }
        if (routeUrlProductListFactory == null) { throw new ArgumentNullException("routeUrlProductListFactory"); }
        if (routeUtilities == null) { throw new ArgumentNullException("routeUtilities"); }

        this.area = area;
        this.appContext = appContext;
        this.routeUrlProductListFactory = routeUrlProductListFactory;
        this.routeUtilities = routeUtilities;
    }

    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        RouteData result = null;
        var tenant = this.appContext.CurrentTenant;

        if (tenant.TenantType.ToString().Equals(this.area, StringComparison.OrdinalIgnoreCase))
        {
            var localeId = this.appContext.CurrentLocaleId;

            // Get all of the pages
            var path = httpContext.Request.Path;
            var pathLength = path.Length;

            var page = this.routeUrlProductListFactory
                .GetRouteUrlProductList(tenant.Id)
                .Where(x => x.UrlPath.Length.Equals(pathLength))
                .Where(x => x.UrlPath.Equals(path))
                .FirstOrDefault();

            if (page != null)
            {
                result = this.routeUtilities.CreateRouteData(this);

                this.routeUtilities.AddQueryStringParametersToRouteData(result, httpContext);

                result.Values["controller"] = "Product";
                result.Values["action"] = "Details";
                result.Values["localeId"] = localeId;
                result.DataTokens["area"] = this.area;

                // TODO: May need a compound key here (ProductXTenantLocaleID and 
                // CategoryId) to allow product to be hosted on pages that are not 
                // below categories.
                result.Values["id"] = page.CategoryXProductId;
            }
        }

        return result;
    }

    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        VirtualPathData result = null;

        if (requestContext.RouteData.IsAreaMatch(this.area))
        {
            var tenant = this.appContext.CurrentTenant;

            // Get all of the pages
            var pages = this.routeUrlProductListFactory.GetRouteUrlProductList(tenant.Id);
            IRouteUrlProductInfo page = null;

            if (this.TryFindMatch(pages, values, out page))
            {
                if (!string.IsNullOrEmpty(page.VirtualPath))
                {
                    result = this.routeUtilities.CreateVirtualPathData(this, page.VirtualPath);
                    result.DataTokens["area"] = tenant.TenantType.ToString();
                }
            }
        }

        return result;
    }

    private bool TryFindMatch(IEnumerable<IRouteUrlProductInfo> pages, RouteValueDictionary values, out IRouteUrlProductInfo page)
    {
        page = null;
        Guid categoryXProductId = Guid.Empty;
        var localeId = (int?)values["localeId"];

        if (localeId == null)
        {
            return false;
        }

        if (!Guid.TryParse(Convert.ToString(values["id"]), out categoryXProductId))
        {
            return false;
        }

        var controller = Convert.ToString(values["controller"]);
        var action = Convert.ToString(values["action"]);

        if (action == "Details" && controller == "Product")
        {
            page = pages
                .Where(x => x.CategoryXProductId.Equals(categoryXProductId))
                .Where(x => x.LocaleId.Equals(localeId))
                .FirstOrDefault();
            if (page != null)
            {
                return true;
            }
        }

        return false;
    }

    #region IRouteWithArea Members

    public string Area
    {
        get { return this.area; }
    }

    #endregion
}

public class RouteUtilities
    : IRouteUtilities
{
    #region IRouteUtilities Members

    public void AddQueryStringParametersToRouteData(RouteData routeData, HttpContextBase httpContext)
    {
        var queryString = httpContext.Request.QueryString;
        if (queryString.Keys.Count > 0)
        {
            foreach (var key in queryString.AllKeys)
            {
                routeData.Values[key] = queryString[key];
            }
        }
    }

    public RouteData CreateRouteData(RouteBase route)
    {
        return new RouteData(route, new MvcRouteHandler());
    }

    public VirtualPathData CreateVirtualPathData(RouteBase route, string virtualPath)
    {
        return new VirtualPathData(route, virtualPath);
    }

    #endregion
}

我使用缓存将所有URL加载到数据结构中(我的最终应用程序可能会使用文件缓存),因此每次URL查找都不会触发数据库。

MvcSiteMapProvider也设置为通过为页面创建多个节点(每个唯一URL一个节点)来使用multiple paths to a single page。通过使用CanonicalUrl或CanonicalKey属性实现规范标记,您可以修复为同一内容使用多个URL的SEO方面。有关完整示例,请参阅this article

您还可以通过实施IDynamicNodeProvider或ISiteMapNodeProvider从数据库中驱动MvcSiteMapProvider节点。

请注意,MvcSiteMapProvider中的URL匹配区分大小写。最好通过执行301重定向确保传入的URL总是小写。