c# - asp net MVC 5 SiteMap - build breadcrumb to another controller -
i developing breadcrumbs asp net mvc e-commerce. have controller categories. looks this:
public class categorycontroller : appcontroller { public actionresult index(string cat1, string cat2, string cat3, int? page) { ... code // build breadcrumbs 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 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'm showing list of products. if user opens product, request performing controller:
public class productcontroller : appcontroller { // get: product public actionresult index(string slug) { // find product slug , show }
here rout config:
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} );
and sitemap categories (works perfect):
<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>
but dont know how build bredcrumbs product this:
home>cat1>product_name home>cat1>cat2>product_name home>cat1>cat2>cat3>product_name
what tried:
this sitemap:
<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>
and tried custom dynamicnodeprovider
<mvcsitemapnode title="Товар" controller="product" action="index" route="product" preservedrouteparameters="slug" key="proddyn" dynamicnodeprovider="flatcable_site.libs.mvc.productnodeprovider, flatcable_site" />
and provider:
public class productnodeprovider : dynamicnodeproviderbase { public override ienumerable<dynamicnode> getdynamicnodecollection(isitemapnode node) { // tried action parameter (slug) , product slug, build category hierarchy doesn't passing // code calls on each page, not on *site.com/product/test_prod* }
mvcsitemapprovider
of work you. keeps cache of hierarchical relationship between nodes , automatically looks current node on each request.
the things need provide node hierarchy (once per application start) , use html helper breadcrumbs, namely @html.mvcsitemap().sitemappath()
. can optionally customize urls way using routing.
since dealing database-driven data, should use dynamicnodeprovider
new data automatically available in sitemap
after added database.
database
first of all, database should keep track of parent-child relationship between categories. can self-joining table.
| categoryid | parentcategoryid | name | urlsegment | |-------------|-------------------|----------------|----------------| | 1 | null | Категории | category-1 | | 2 | 1 | Категории2 | category-2 | | 3 | 2 | Категории3 | category-3 |
depending on where put categories in web site, null
should represent parent node (usually home page or top-level category list page).
then products should categorized. gets more complicated if there many-to-many relationship between category , product because each node should have own unique url (even if link same product page). won't go details here, using canonical tag helper in conjunction custom routing (possibly data-driven urls) recommended approach. natural add category beginning of product url (which show below) have unique urls each category view of product. should add additional flag in database keep track of "primary" category, can used set canonical key.
for rest of example, assume product category relationship 1-to-1, not how e-commerce done these days.
| productid | categoryid | name | urlsegment | |-------------|------------|----------------|----------------| | 1 | 3 | prod1 | product-1 | | 2 | 1 | prod2 | product-2 | | 3 | 2 | prod3 | product-3 |
controllers
next, controllers built supply dynamic category , product information. mvcsitemapprovider
uses controller , action name.
note exact way product in application depends on design. example uses 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); } }
dynamic node providers
for maintenance purposes, using separate category , product node providers may make things easier not strictly necessary. in fact, could provide of nodes single dynamic node provider.
public class categorydynamicnodeprovider : dynamicnodeproviderbase { public override ienumerable<dynamicnode> getdynamicnodecollection(isitemapnode node) { var result = new list<dynamicnode>(); using (var db = new myentities()) { // create node each category foreach (var category in db.categories) { dynamicnode dynamicnode = new dynamicnode(); // key mapping dynamicnode.key = "category_" + category.categoryid; // note: parent category defined int?, need check // whether has value. note use 0 instead if 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 node 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; } }
alternatively, if use di might consider implementing isitemapnodeprovider
instead of dynamic node provider. lower level abstraction allows provide all of nodes.
mvc.sitemap
all need in xml static pages , dynamic node provider definition nodes. note have defined parent-child relationship within dynamic node providers, there no need again here (although make more clear products nested within categories).
<?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
then matter of putting sitemappath
views. simplest approach add _layout.cshtml
.
<div id="body"> @rendersection("featured", required: false) <section class="content-wrapper main-content clear-fix"> @html.mvcsitemap().sitemappath() @renderbody() </section> </div>
note can edit templates (or create named templates) in /views/shared/displaytemplates/
folder customize html output html helpers.
routing
as mentioned before, recommend using data-driven routing when making data-driven pages. primary reason purist. routing logic not belong in controller, passing slug controller messy solution.
also, if have primary key url mapping, means routing cosmetic far rest of application concerned. keys drive application (and database) , urls drive mvc. makes managing urls external application logic.
cachedroute<tprimarykey>
this implementation allows map set of data records single controller action. each record has separate virtual path (url) maps specific primary key.
this class reusable can use multiple sets of data (typically 1 class per database table wish map).
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 leading , trailing slash requestpath = requestpath.trim('/'); } tprimarykey id; //if returns false, means 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); // logic here should inverse of logic in // getroutedata(). so, match same controller, action, , id. // if had additional route values there, take them // consideration during step. if (action.equals(this.action) && controller.equals(this.controller)) { // 'ordefault' case returns default value of type you're // iterating over. value types, new instance of type. // since keyvaluepair<tkey, tvalue> value type (i.e. struct), // 'ordefault' case not result in null-reference exception. // since tkey here string, .key of new instance 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] idictionary<string, tprimarykey>; if (map == null) { lock (synclock) { map = cache[this.cachekey] 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>
this extension point supply virtual path primary key mapping data.
public interface icachedroutedataprovider<tprimarykey> { idictionary<string, tprimarykey> getvirtualpathtoidmap(httpcontextbase httpcontext); }
categorycachedroutedataprovider
here implementation of above interface provide categories 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
and implementation provides product urls (complete categories, although omit if don't need it).
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
this service converts category url segments url slugs. looks parent categories category database data , appends them beginning of slug.
there little responsibility added here (which wouldn't in production project) adds request caching because logic used both categorycachedroutedataprovider
, productcachedroutedataprovider
. combined here brevity.
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; { 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; } }
route registration
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 } ); } }
now, if use following code in controller action or view:
var product1 = url.action("index", "product", new { id = 1 });
the result of product1
be
/category-1/category-2/category-3/product-1
and if enter url browser, call productcontroller.index
action , pass id
1. when view returns, breadcrumb is
home > Категории > Категории2 > Категории3 > prod1
you still improve things, such adding cache busting route urls, , adding paging categories (although these days sites go infinite scroll rather paging), should give starting point.
Comments
Post a Comment