Skip to main content
 首页 » 编程设计

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

2024年04月30日38birdshome

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

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

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添加缓存清除,以及在类别中添加分页(尽管如今,大多数站点都使用无限滚动而不是分页),但这应该为您提供了一个很好的起点。