dan*_*y89 1 c# asp.net asp.net-mvc asp.net-mvc-routing asp.net-mvc-5
我为我的 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
}
}
Run Code Online (Sandbox Code Playgroud)
A 显示产品列表。如果用户打开产品,请求使用另一个控制器执行:
public class ProductController : AppController
{
// GET: Product
public ActionResult Index(string slug)
{
// find product by slug and show it
}
Run Code Online (Sandbox Code Playgroud)
这是我的路由配置:
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}
);
Run Code Online (Sandbox Code Playgroud)
和类别的站点地图(完美无缺):
<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>
Run Code Online (Sandbox Code Playgroud)
但我不知道如何为这样的产品构建 bredcrumbs:
Home>cat1>Product_name
Home>cat1>cat2>Product_name
Home>cat1>cat2>cat3>Product_name
Run Code Online (Sandbox Code Playgroud)
我试过的:
此站点地图:
<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>
Run Code Online (Sandbox Code Playgroud)
我也试过自定义 DynamicNodeProvider
<mvcSiteMapNode title="?????" controller="Product" action="Index" route="Product" preservedRouteParameters="slug" key="prodDyn" dynamicNodeProvider="FlatCable_site.Libs.Mvc.ProductNodeProvider, FlatCable_site" />
Run Code Online (Sandbox Code Playgroud)
和提供者:
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*
}
Run Code Online (Sandbox Code Playgroud)
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 |
Run Code Online (Sandbox Code Playgroud)
根据在那里你把类别在你的网站,null应该是代表父节点(通常它是主页或顶级类别列表页)。
那么你的产品应该被分类。如果类别和产品之间存在多对多关系,这会变得更加复杂,因为每个节点都应该有自己唯一的URL(即使它只是指向同一产品页面的另一个链接)。我不会在这里详细介绍,但推荐的方法是将规范标签助手与自定义路由(可能是数据驱动的 URL)结合使用。将类别添加到产品 URL(我在下面显示)的开头是很自然的,因此您将拥有产品的每个类别视图的唯一 URL。然后您应该在数据库中添加一个额外的标志来跟踪“主要”类别,然后可以使用它来设置规范键。
对于本示例的其余部分,我将假设产品与类别的关系是 1 对 1,但如今大多数电子商务并非如此。
| ProductID | CategoryID | Name | UrlSegment |
|-------------|------------|----------------|----------------|
| 1 | 3 | Prod1 | product-1 |
| 2 | 1 | Prod2 | product-2 |
| 3 | 2 | Prod3 | product-3 |
Run Code Online (Sandbox Code Playgroud)
接下来,构建控制器以提供动态类别和产品信息。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);
}
}
Run Code Online (Sandbox Code Playgroud)
出于维护目的,使用单独的类别和产品节点提供程序可能会使事情变得更容易,但这并不是绝对必要的。事实上,您可以为所有节点提供单个动态节点提供程序。
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;
}
}
Run Code Online (Sandbox Code Playgroud)
或者,如果您使用 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>
Run Code Online (Sandbox Code Playgroud)
那么这只是将SiteMapPath放入您的视图中的问题。最简单的方法就是将它添加到您的_Layout.cshtml.
<div id="body">
@RenderSection("featured", required: false)
<section class="content-wrapper main-content clear-fix">
@Html.MvcSiteMap().SiteMapPath()
@RenderBody()
</section>
</div>
Run Code Online (Sandbox Code Playgroud)
请注意,您可以编辑/Views/Shared/DisplayTemplates/文件夹中的模板(或创建命名模板)以自定义 HTML 帮助程序输出的 HTML。
正如我之前提到的,我建议在制作数据驱动页面时使用数据驱动路由。造成这种情况的主要原因是我是一个纯粹主义者。路由逻辑不属于控制器,因此将 slug 传递给控制器是一个混乱的解决方案。
此外,如果您有一个 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));
}
}
Run Code Online (Sandbox Code Playgroud)
ICachedRouteDataProvider<TPrimaryKey>
这是您提供主键映射数据的虚拟路径的扩展点。
public interface ICachedRouteDataProvider<TPrimaryKey>
{
IDictionary<string, TPrimaryKey> GetVirtualPathToIdMap(HttpContextBase httpContext);
}
Run Code Online (Sandbox Code Playgroud)
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);
}
}
Run Code Online (Sandbox Code Playgroud)
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;
}
}
Run Code Online (Sandbox Code Playgroud)
CategorySlugBuilder这是将类别 URL 段转换为 URL slug 的服务。它从类别数据库数据中查找父类别并将它们附加到 slug 的开头。
还有就是因为这个逻辑是使用两个,增加了请求缓存此处添加一些额外的责任(我可能不会在一个生产项目做)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; }
}
Run Code Online (Sandbox Code Playgroud)
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 }
);
}
}
Run Code Online (Sandbox Code Playgroud)
现在,如果您在控制器操作或视图中使用以下代码:
var product1 = Url.Action("Index", "Product", new { id = 1 });
Run Code Online (Sandbox Code Playgroud)
的结果product1是
/category-1/category-2/category-3/product-1
Run Code Online (Sandbox Code Playgroud)
如果你在浏览器中输入这个 URL,它会调用ProductController.Indexaction 并传递它id1。当视图返回时,面包屑是
Home > ????????? > ?????????2 > ?????????3 > Prod1
Run Code Online (Sandbox Code Playgroud)
您仍然可以改进一些东西,例如为路由 URL 添加缓存破坏,以及为类别添加分页(尽管现在大多数站点都使用无限滚动而不是分页),但这应该为您提供一个很好的起点。
| 归档时间: |
|
| 查看次数: |
3613 次 |
| 最近记录: |