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

Popular posts from this blog

PySide and Qt Properties: Connecting signals from Python to QML -

c# - DevExpress.Wpf.Grid.InfiniteGridSizeException was unhandled -

scala - 'wrong top statement declaration' when using slick in IntelliJ -