我为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
操作并将其传递给id
1。当视图返回时,面包屑为Home > Категории > Категории2 > Категории3 > Prod1
您仍然可以进行改进,例如为路由URL添加缓存清除,以及在类别中添加分页(尽管如今,大多数站点都使用无限滚动而不是分页),但这应该为您提供了一个很好的起点。
关于c# - ASP Net MVC 5 SiteMap-将面包屑构建到另一个 Controller ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37429753/