c# - ASP Net MVC 5 SiteMap-将面包屑构建到另一个 Controller

标签 c# asp.net asp.net-mvc asp.net-mvc-5 asp.net-mvc-routing

我为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
        }
}


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

  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中使用。

数据库

首先,您的数据库应跟踪类别之间的父子关系。您可以使用自联接表来实现。

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


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

然后,应将您的产品分类。如果类别和产品之间存在多对多的关系,这将变得更加复杂,因为每个节点都应该具有自己的唯一URL(即使它只是指向同一产品页面的另一个链接)。我不会在这里详细介绍,但是推荐将canonical tag helper与自定义路由(可能是data-driven URLs)结合使用。很自然地将类别添加到产品URL的开头(我在下面显示),因此您将为产品的每个类别视图拥有唯一的URL。然后,您应该在数据库中添加一个附加标志来跟踪“主”类别,然后可以使用该标志来设置规范密钥。

在本示例的其余部分中,我将假定产品与类别的关系为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而不是动态节点提供程序。它是一个较低级别的抽象,它允许您提供所有节点。

网站地图

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

然后,只需将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。

路由

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

另外,如果您具有URL映射的主键,则意味着就应用程序的其余部分而言,路由仅是修饰。密钥是驱动应用程序(和数据库)的源,URL是驱动MVC的源。这使得在应用程序逻辑外部管理URL。

CachedRoute<TPrimaryKey>

此实现使您可以将一组数据记录映射到单个控制器操作。每个记录都有一个映射到特定主键的单独的虚拟路径(URL)。

该类是可重用的,因此您可以将其用于多组数据(通常,您要映射的每个数据库表一个类)。

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

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

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段的服务。它从类别数据库数据中查找父类别,并将其附加到段的开头。

这里添加了一些额外的责任(在生产项目中我可能不会这样做),它添加了请求缓存,因为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操作并将其传递给id1。当视图返回时,面包屑为

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


您仍然可以进行改进,例如为路由URL添加缓存清除,以及在类别中添加分页(尽管如今,大多数站点都使用无限滚动而不是分页),但这应该为您提供了一个很好的起点。

关于c# - ASP Net MVC 5 SiteMap-将面包屑构建到另一个 Controller ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37429753/

相关文章:

c# - 使用 CaSTLe windsor 创建对象而不是工厂类

c# - 从 AJAX 结果呈现 HTML 内容

c# - asp.net 从数据集创建单选按钮

javascript - Javascript 文件中的相对图像 URL - ASP.net MVC 和 IIS 7

asp.net-mvc - ASP.NET MVC 3 中的 Flash 等效项

c# - 在 QueryOver 中

c# - 域驱动设计中上下文之间的通信

c# - 为什么文本框中的自动完成 jquery 在内容占位符中使用时不起作用?

c# - EF 迁移对象已存在错误

c# - Entity Framework Core fluent api一对多和一对一产生重复外键