asp net MVC 5 SiteMap - 将面包屑构建到另一个控制器

asp net MVC 5 SiteMap - build breadcrumb to another controller

我正在为我的 asp net mvc 电子商务开发面包屑导航。我的类别有一个控制器。它看起来像这样:

 public class CategoryController : AppController
    {

    public ActionResult Index(string cat1, string cat2, string cat3, int? page)
        {

... some code

       // build breadcrumbs from parent cats
        int indexer = 0;
        foreach(var item in parCategories) //parCategories - list of parent categories
        {
            string currCatIndex = new StringBuilder().AppendFormat("category{0}", indexer + 1).ToString(); //+2 cause arr index begins from 0
            var currNode = SiteMaps.Current.FindSiteMapNodeFromKey(currCatIndex);           
            currNode.Title= parCategories.ElementAt(indexer).Name;
            indexer++;
        }

        string finalCatIndex = new StringBuilder().AppendFormat("category{0}", CategoryDepth + 1).ToString();
        var node = SiteMaps.Current.FindSiteMapNodeFromKey(finalCatIndex);
        node.Title = CurrCategory.Category.Name;

       //show View
        }
}

A 正在显示产品列表。如果用户打开产品,请求使用另一个控制器执行:

  public class ProductController : AppController
    {
        // GET: Product
        public ActionResult Index(string slug)
        {   
           // find product by slug and show it
        }

这是我的路由配置:

 routes.MapRoute(
                name: "Category",
                url: "Category/{cat1}/{cat2}/{cat3}",
                defaults: new { controller = "Category", action = "Index", cat1 = UrlParameter.Optional, cat2= UrlParameter.Optional, cat3 = UrlParameter.Optional }    
            );

            routes.MapRoute(
               name: "Product",
               url: "Product/{slug}",
               defaults: new { controller = "Product", action = "Index", slug = UrlParameter.Optional}
           );

和类别站点地图(完美):

 <mvcSiteMapNode title="Категории" controller="Category" action="Index" route="Category" preservedRouteParameters="cat1" key="category1">
      <mvcSiteMapNode title="Категории2" controller="Category" action="Index" route="Category" preservedRouteParameters="cat1;cat2" key="category2">
        <mvcSiteMapNode title="Категории3" controller="Category" action="Index" route="Category" preservedRouteParameters="cat1;cat2;cat3" key="category3" />
      </mvcSiteMapNode>
    </mvcSiteMapNode>

但我不知道如何为这样的产品构建 bredcrumbs:

Home>cat1>Product_name
Home>cat1>cat2>Product_name
Home>cat1>cat2>cat3>Product_name

我试过的:

此站点地图:

 <mvcSiteMapNode title="Категории" controller="Category" action="Index" route="Category" preservedRouteParameters="cat1" key="category1">
      <mvcSiteMapNode title="Prod" controller="Product" action="Index" route="Product" preservedRouteParameters="slug" key="prod1" />
      <mvcSiteMapNode title="Категории2" controller="Category" action="Index" route="Category" preservedRouteParameters="cat1;cat2" key="category2">
        <mvcSiteMapNode title="Prod" controller="Product" action="Index" route="Product" preservedRouteParameters="slug" key="prod2" />
        <mvcSiteMapNode title="Категории3" controller="Category" action="Index" route="Category" preservedRouteParameters="cat1;cat2;cat3" key="category3" >
          <mvcSiteMapNode title="Prod" controller="Product" action="Index" route="Product" preservedRouteParameters="slug" key="prod3" />
        </mvcSiteMapNode>
        </mvcSiteMapNode>
    </mvcSiteMapNode> 

我还尝试了自定义 DynamicNodeProvider

<mvcSiteMapNode title="Товар" controller="Product" action="Index" route="Product" preservedRouteParameters="slug" key="prodDyn" dynamicNodeProvider="FlatCable_site.Libs.Mvc.ProductNodeProvider, FlatCable_site" />

和供应商:

  public class ProductNodeProvider : DynamicNodeProviderBase
        {
            public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
            {

    // tried to get action parameter (slug) and get product by slug, then build category hierarchy but it doesn't passing
    // also this code calls on each page, not only on *site.com/Product/test_prod*

}

MvcSiteMapProvider 已经为您完成了大部分工作。它保留节点之间层次关系的缓存,并在每个请求时自动查找当前节点。

您唯一需要做的就是提供节点层次结构(每个应用程序启动一次)并使用面包屑的 HTML 助手,即 @Html.MvcSiteMap().SiteMapPath()。您还可以选择使用路由以任何您喜欢的方式自定义 URL。

由于您可能会处理数据库驱动的数据,因此您应该使用 DynamicNodeProvider,这样新数据在添加到数据库后将自动在 SiteMap 中可用。

数据库

首先,您的数据库应该跟踪类别之间的父子关系。您可以使用自连接 table.

| CategoryID  | ParentCategoryID  | Name           | UrlSegment     |
|-------------|-------------------|----------------|----------------|
| 1           | null              | Категории      | category-1     |
| 2           | 1                 | Категории2     | category-2     |
| 3           | 2                 | Категории3     | category-3     |

根据您将类别放在网站中的位置null 应该代表父节点(通常是主页或顶级类别列表页)。

那么你的产品应该被分类了。如果类别和产品之间存在多对多关系,这会变得更加复杂,因为每个节点都应该有自己的 unique URL(即使它只是另一个 link 到相同的产品页面)。我不会在这里详细介绍,但是使用 canonical tag helper in conjunction with custom routing (possibly ) 是推荐的方法。将类别添加到产品 URL 的开头是很自然的(我在下面显示),因此您将为产品的每个类别视图提供唯一的 URL。然后你应该在数据库中添加一个额外的标志来跟踪 "primary" 类别,然后可以使用它来设置规范键。

对于此示例的其余部分,我将假设产品与类别的关系是 1 对 1,但这不是当今大多数电子商务的处理方式。

| ProductID   | CategoryID | Name           | UrlSegment     |
|-------------|------------|----------------|----------------|
| 1           | 3          | Prod1          | product-1      |
| 2           | 1          | Prod2          | product-2      |
| 3           | 2          | Prod3          | product-3      |

控制器

接下来,构建控制器以提供动态类别和产品信息。 MvcSiteMapProvider 使用控制器和动作名称。

请注意,您在应用程序中获得产品的确切方式取决于您的设计。此示例使用 CQS.

public class CategoryController : Controller
{
    private readonly IQueryProcessor queryProcessor;

    public CategoryController(IQueryProcessor queryProcessor)
    {
        if (queryProcessor == null)
            throw new ArgumentNullException("queryProcessor");

        this.queryProcessor = queryProcessor;
    }

    public ActionResult Index(int id)
    {
        var categoryDetails = this.queryProcessor.Execute(new GetCategoryDetailsQuery
        {
            CategoryId = id
        });

        return View(categoryDetails);
    }
}


public class ProductController : Controller
{
    private readonly IQueryProcessor queryProcessor;

    public ProductController(IQueryProcessor queryProcessor)
    {
        if (queryProcessor == null)
            throw new ArgumentNullException("queryProcessor");

        this.queryProcessor = queryProcessor;
    }

    public ActionResult Index(int id)
    {
        var productDetails = this.queryProcessor.Execute(new GetProductDetailsDataQuery
        {
            ProductId = id
        });

        return View(productDetails);
    }
}

动态节点提供商

出于维护目的,使用单独的类别和产品节点提供程序可能会使事情变得更容易,但这并不是绝对必要的。事实上,您 可以 为所有节点提供一个动态节点提供程序。

public class CategoryDynamicNodeProvider : DynamicNodeProviderBase
{
    public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
    {
        var result = new List<DynamicNode>();

        using (var db = new MyEntities())
        {
            // Create a node for each category
            foreach (var category in db.Categories)
            {
                DynamicNode dynamicNode = new DynamicNode();

                // Key mapping
                dynamicNode.Key = "Category_" + category.CategoryID;

                // NOTE: parent category is defined as int?, so we need to check
                // whether it has a value. Note that you could use 0 instead if you want.
                dynamicNode.ParentKey = category.ParentCategoryID.HasValue ? "Category_" + category.ParentCategoryID.Value : "Home";

                // Add route values
                dynamicNode.Controller = "Category";
                dynamicNode.Action = "Index";
                dynamicNode.RouteValues.Add("id", category.CategoryID);

                // Set title
                dynamicNode.Title = category.Name;

                result.Add(dynamicNode);
            }
        }

        return result;
    }
}

public class ProductDynamicNodeProvider : DynamicNodeProviderBase
{
    public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
    {
        var result = new List<DynamicNode>();

        using (var db = new MyEntities())
        {
            // Create a node for each product
            foreach (var product in db.Products)
            {
                DynamicNode dynamicNode = new DynamicNode();

                // Key mapping
                dynamicNode.Key = "Product_" + product.ProductID;
                dynamicNode.ParentKey = "Category_" + product.CategoryID;

                // Add route values
                dynamicNode.Controller = "Product";
                dynamicNode.Action = "Index";
                dynamicNode.RouteValues.Add("id", product.ProductID);

                // Set title
                dynamicNode.Title = product.Name;

                result.Add(dynamicNode);
            }
        }

        return result;
    }
}

或者,如果您使用 DI,您可以考虑实施 ISiteMapNodeProvider 而不是动态节点提供程序。它是一个较低级别的抽象,允许您提供 所有 个节点。

Mvc.sitemap

您 XML 中需要的只是静态页面和动态节点提供程序定义节点。请注意,您已经在动态节点提供程序中定义了父子关系,因此无需在此处再次执行此操作(尽管您可以更清楚地说明产品嵌套在类别中)。

<?xml version="1.0" encoding="utf-8" ?>
<mvcSiteMap xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-4.0"
            xsi:schemaLocation="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-4.0 MvcSiteMapSchema.xsd">

    <mvcSiteMapNode title="Home" controller="Home" action="Index">
        <mvcSiteMapNode title="Category Nodes" dynamicNodeProvider="MyNamespace.CategoryDynamicNodeProvider, MyAssembly" />
        <mvcSiteMapNode title="Product Nodes" dynamicNodeProvider="MyNamespace.ProductDynamicNodeProvider, MyAssembly" />
    </mvcSiteMapNode>
</mvcSiteMap>

站点地图路径

那么只需将 SiteMapPath 放入您的视图即可。最简单的方法就是将它添加到您的 _Layout.cshtml.

<div id="body">
    @RenderSection("featured", required: false)
    <section class="content-wrapper main-content clear-fix">
        @Html.MvcSiteMap().SiteMapPath()
        @RenderBody()
    </section>
</div>

请注意,您可以编辑 /Views/Shared/DisplayTemplates/ 文件夹中的模板(或创建命名模板)来自定义 HTML 助手输出的 HTML。

路由

正如我之前提到的,我建议在制作数据驱动页面时使用数据驱动路由。这样做的主要原因是我是一个纯粹主义者。路由逻辑不属于控制器,因此将 slug 传递给控制器​​是一个混乱的解决方案。

此外,如果您有一个 URL 映射的主键,这意味着就应用程序的其余部分而言,路由只是装饰性的。键是驱动应用程序(和数据库)的,URLs 是驱动 MVC 的。这使得在您的应用程序逻辑之外管理 URL。

CachedRoute<TPrimaryKey>

这是一个允许您将一组数据记录映射到单个控制器操作的实现。每条记录都有一个单独的虚拟路径 (URL),映射到特定的主键。

此 class 可重复使用,因此您可以将其用于多组数据(通常每个 class 您希望映射的数据库 table)。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

public class CachedRoute<TPrimaryKey> : RouteBase
{
    private readonly string cacheKey;
    private readonly string controller;
    private readonly string action;
    private readonly ICachedRouteDataProvider<TPrimaryKey> dataProvider;
    private readonly IRouteHandler handler;
    private object synclock = new object();

    public CachedRoute(string controller, string action, ICachedRouteDataProvider<TPrimaryKey> dataProvider)
        : this(controller, action, typeof(CachedRoute<TPrimaryKey>).Name + "_GetMap_" + controller + "_" + action, dataProvider, new MvcRouteHandler())
    {
    }

    public CachedRoute(string controller, string action, string cacheKey, ICachedRouteDataProvider<TPrimaryKey> dataProvider, IRouteHandler handler)
    {
        if (string.IsNullOrWhiteSpace(controller))
            throw new ArgumentNullException("controller");
        if (string.IsNullOrWhiteSpace(action))
            throw new ArgumentNullException("action");
        if (string.IsNullOrWhiteSpace(cacheKey))
            throw new ArgumentNullException("cacheKey");
        if (dataProvider == null)
            throw new ArgumentNullException("dataProvider");
        if (handler == null)
            throw new ArgumentNullException("handler");

        this.controller = controller;
        this.action = action;
        this.cacheKey = cacheKey;
        this.dataProvider = dataProvider;
        this.handler = handler;

        // Set Defaults
        CacheTimeoutInSeconds = 900;
    }

    public int CacheTimeoutInSeconds { get; set; }


    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        string requestPath = httpContext.Request.Path;
        if (!string.IsNullOrEmpty(requestPath))
        {
            // Trim the leading and trailing slash
            requestPath = requestPath.Trim('/'); 
        }

        TPrimaryKey id;

        //If this returns false, that means the URI did not match
        if (!this.GetMap(httpContext).TryGetValue(requestPath, out id))
        {
            return null;
        }

        var result = new RouteData(this, new MvcRouteHandler());

        result.Values["controller"] = this.controller;
        result.Values["action"] = this.action;
        result.Values["id"] = id;

        return result;
    }

    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        TPrimaryKey id;
        object idObj;
        object controller;
        object action;

        if (!values.TryGetValue("id", out idObj))
        {
            return null;
        }

        id = SafeConvert<TPrimaryKey>(idObj);
        values.TryGetValue("controller", out controller);
        values.TryGetValue("action", out action);

        // The logic here should be the inverse of the logic in 
        // GetRouteData(). So, we match the same controller, action, and id.
        // If we had additional route values there, we would take them all 
        // into consideration during this step.
        if (action.Equals(this.action) && controller.Equals(this.controller))
        {
            // The 'OrDefault' case returns the default value of the type you're 
            // iterating over. For value types, it will be a new instance of that type. 
            // Since KeyValuePair<TKey, TValue> is a value type (i.e. a struct), 
            // the 'OrDefault' case will not result in a null-reference exception. 
            // Since TKey here is string, the .Key of that new instance will be null.
            var virtualPath = GetMap(requestContext.HttpContext).FirstOrDefault(x => x.Value.Equals(id)).Key;
            if (!string.IsNullOrEmpty(virtualPath))
            {
                return new VirtualPathData(this, virtualPath);
            }
        }

        return null;
    }

    private IDictionary<string, TPrimaryKey> GetMap(HttpContextBase httpContext)
    {
        IDictionary<string, TPrimaryKey> map;
        var cache = httpContext.Cache;
        map = cache[this.cacheKey] as IDictionary<string, TPrimaryKey>;
        if (map == null)
        {
            lock (synclock)
            {
                map = cache[this.cacheKey] as IDictionary<string, TPrimaryKey>;
                if (map == null)
                {
                    map = this.dataProvider.GetVirtualPathToIdMap(httpContext);
                    cache[this.cacheKey] = map;
                }
            }
        }
        return map;
    }

    private static T SafeConvert<T>(object obj)
    {
        if (typeof(T).Equals(typeof(Guid)))
        {
            if (obj.GetType() == typeof(string))
            {
                return (T)(object)new Guid(obj.ToString());
            }
            return (T)(object)Guid.Empty;
        }
        return (T)Convert.ChangeType(obj, typeof(T));
    }
}

ICachedRouteDataProvider<TPrimaryKey>

这是您提供主键映射数据虚拟路径的扩展点。

public interface ICachedRouteDataProvider<TPrimaryKey>
{
    IDictionary<string, TPrimaryKey> GetVirtualPathToIdMap(HttpContextBase httpContext);
}

CategoryCachedRouteDataProvider

下面是上述接口的实现,用于向 CachedRoute 提供类别。

public class CategoryCachedRouteDataProvider : ICachedRouteDataProvider<int>
{
    private readonly ICategorySlugBuilder categorySlugBuilder;

    public CategoryCachedRouteDataProvider(ICategorySlugBuilder categorySlugBuilder)
    {
        if (categorySlugBuilder == null)
            throw new ArgumentNullException("categorySlugBuilder");
        this.categorySlugBuilder = categorySlugBuilder;
    }

    public IDictionary<string, int> GetVirtualPathToIdMap(HttpContextBase httpContext)
    {
        var slugs = this.categorySlugBuilder.GetCategorySlugs(httpContext.Items);
        return slugs.ToDictionary(k => k.Slug, e => e.CategoryID);
    }
}

ProductCachedRouteDataProvider

这是一个提供产品 URLs 的实现(包含类别,但如果不需要,可以省略)。

public class ProductCachedRouteDataProvider : ICachedRouteDataProvider<int>
{
    private readonly ICategorySlugBuilder categorySlugBuilder;

    public ProductCachedRouteDataProvider(ICategorySlugBuilder categorySlugBuilder)
    {
        if (categorySlugBuilder == null)
            throw new ArgumentNullException("categorySlugBuilder");
        this.categorySlugBuilder = categorySlugBuilder;
    }

    public IDictionary<string, int> GetVirtualPathToIdMap(HttpContextBase httpContext)
    {
        var slugs = this.categorySlugBuilder.GetCategorySlugs(httpContext.Items);
        var result = new Dictionary<string, int>();

        using (var db = new ApplicationDbContext())
        {
            foreach (var product in db.Products)
            {
                int id = product.ProductID;
                string categorySlug = slugs
                    .Where(x => x.CategoryID.Equals(product.CategoryID))
                    .Select(x => x.Slug)
                    .FirstOrDefault();
                string slug = string.IsNullOrEmpty(categorySlug) ?
                    product.UrlSegment :
                    categorySlug + "/" + product.UrlSegment;

                result.Add(slug, id);
            }
        }
        return result;
    }
}

CategorySlugBuilder

这是将类别 URL 段转换为 URL 段的服务。它从类别数据库数据中查找父类别并将它们附加到 slug 的开头。

这里添加了一些额外的责任(我可能不会在生产项目中这样做)添加请求缓存,因为 CategoryCachedRouteDataProviderProductCachedRouteDataProvider 都使用了这个逻辑。为了简洁起见,我把它合并在这里。

public interface ICategorySlugBuilder
{
    IEnumerable<CategorySlug> GetCategorySlugs(IDictionary cache);
}

public class CategorySlugBuilder : ICategorySlugBuilder
{
    public IEnumerable<CategorySlug> GetCategorySlugs(IDictionary requestCache)
    {
        string key = "__CategorySlugs";
        var categorySlugs = requestCache[key];
        if (categorySlugs == null)
        {
            categorySlugs = BuildCategorySlugs();
            requestCache[key] = categorySlugs;
        }
        return (IEnumerable<CategorySlug>)categorySlugs;
    }

    private IEnumerable<CategorySlug> BuildCategorySlugs()
    {
        var categorySegments = GetCategorySegments();
        var result = new List<CategorySlug>();

        foreach (var categorySegment in categorySegments)
        {
            var map = new CategorySlug();
            map.CategoryID = categorySegment.CategoryID;
            map.Slug = this.BuildSlug(categorySegment, categorySegments);

            result.Add(map);
        }

        return result;
    }

    private string BuildSlug(CategoryUrlSegment categorySegment, IEnumerable<CategoryUrlSegment> categorySegments)
    {
        string slug = categorySegment.UrlSegment;
        if (categorySegment.ParentCategoryID.HasValue)
        {
            var segments = new List<string>();
            CategoryUrlSegment currentSegment = categorySegment;

            do
            {
                segments.Insert(0, currentSegment.UrlSegment);

                currentSegment =
                    currentSegment.ParentCategoryID.HasValue ?
                    categorySegments.Where(x => x.CategoryID == currentSegment.ParentCategoryID.Value).FirstOrDefault() :
                    null;

            } while (currentSegment != null);

            slug = string.Join("/", segments);
        }
        return slug;
    }

    private IEnumerable<CategoryUrlSegment> GetCategorySegments()
    {
        using (var db = new ApplicationDbContext())
        {
            return db.Categories.Select(
                c => new CategoryUrlSegment
                {
                    CategoryID = c.CategoryID,
                    ParentCategoryID = c.ParentCategoryID,
                    UrlSegment = c.UrlSegment
                }).ToArray();
        }
    }
}

public class CategorySlug
{
    public int CategoryID { get; set; }
    public string Slug { get; set; }
}

public class CategoryUrlSegment
{
    public int CategoryID { get; set; }
    public int? ParentCategoryID { get; set; }
    public string UrlSegment { get; set; }
}

路线登记

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

        routes.Add("Categories", new CachedRoute<int>(
            controller: "Category", 
            action: "Index", 
            dataProvider: new CategoryCachedRouteDataProvider(new CategorySlugBuilder())));

        routes.Add("Products", new CachedRoute<int>(
            controller: "Product",
            action: "Index",
            dataProvider: new ProductCachedRouteDataProvider(new CategorySlugBuilder())));

        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
    }
}

现在,如果您在控制器操作或视图中使用以下代码:

var product1 = Url.Action("Index", "Product", new { id = 1 });

product1的结果将是

/category-1/category-2/category-3/product-1

如果你在浏览器中输入这个URL,它会调用ProductController.Index动作并传递给它id 1.当视图returns,面包屑是

Home > Категории > Категории2 > Категории3 > Prod1

您仍然可以改进一些东西,例如为路由 URL 添加缓存清除,以及为类别添加分页(尽管现在大多数网站都使用无限滚动而不是分页),但这应该给你一个好的起点。