【问题标题】:Mvc Custom Route and Breadcrumb Strategies?Mvc 自定义路由和面包屑策略?
【发布时间】:2013-12-18 16:12:09
【问题描述】:

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

  • /
  • /信息
  • /信息/关于我们
  • /信息/联系我们
  • /男装
  • /mens-clothing/mens-shirts
  • /mens-clothing/mens-shirts/test-tshirt

slug 在所有内容类型中都是独一无二的,但例如,test-tshirt 可能出现在多个类别中:

  • /mens-clothing/mens-shirts/test-tshirt
  • /mens-clothing/clearance/test-tshirt

我创建了一个自定义路由,它采用路径中的最后一个 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;
    }
}

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

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

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

【问题讨论】:

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


    【解决方案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 也设置为使用multiple paths to a single page,方法是为页面创建多个节点(每个唯一 URL 一个节点)。您可以通过使用 CanonicalUrl 或 CanonicalKey 属性实现规范标签来修复对同一内容使用多个 URL 的 SEO 方面。有关完整示例,请参阅 this article

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

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

    【讨论】:

    • 内容丰富,谢谢!需要查看 URL 属性,在示例中没有看到。快速的问题,与我的原始问题无关...在检查路径集合中的路径相等性之前检查路径长度的原因是什么?
    • 谢谢,做了快速测试,“url”参数完美运行。找到了站点地图节点的 xsd,它具有大量额外的功能,我不必为我的应用程序自定义构建(基于角色的访问、httpMethod、可见性、规范、元机器人等)。好东西。
    • 我检查长度作为性能增强。整数比较比比较两个字节数组(字符串)要快得多,因此如果长度不相等,那么进行慢速字符串比较就没有什么意义了。请记住,需要对每个传入的请求进行比较,因此应该尽可能快。注意:我刚刚通过 Reflector 检查了 .NET 源代码,微软已经在 Equals 方法中首先比较了字符串长度,所以我想我可以把那行去掉。
    猜你喜欢
    • 2010-11-04
    • 1970-01-01
    • 1970-01-01
    • 2012-10-06
    • 2014-04-11
    • 1970-01-01
    • 1970-01-01
    • 2011-01-24
    • 1970-01-01
    相关资源
    最近更新 更多