How can dynamic breadcrumbs be achieved with ASP.net MVC?

90,121

Solution 1

There is a tool to do this on codeplex: http://mvcsitemap.codeplex.com/ [project moved to github]

Edit:

There is a way to derive a SiteMapProvider from a database: http://www.asp.net/Learn/data-access/tutorial-62-cs.aspx

You might be able to modify the mvcsitemap tool to use that to get what you want.

Solution 2

Sitemap's are definitely one way to go... alternatively, you can write one yourself! (of course as long as standard MVC rules are followed)... I just wrote one, I figured I would share here.

@Html.ActionLink("Home", "Index", "Home")
@if(ViewContext.RouteData.Values["controller"].ToString() != "Home") {
    @:> @Html.ActionLink(ViewContext.RouteData.Values["controller"].ToString(), "Index", ViewContext.RouteData.Values["controller"].ToString()) 
}
@if(ViewContext.RouteData.Values["action"].ToString() != "Index"){
    @:> @Html.ActionLink(ViewContext.RouteData.Values["action"].ToString(), ViewContext.RouteData.Values["action"].ToString(), ViewContext.RouteData.Values["controller"].ToString()) 
}

Hopefully someone will find this helpful, this is exactly what I was looking for when I searched SO for MVC breadcrumbs.

Solution 3

ASP.NET 5 (aka ASP.NET Core), MVC Core Solution

In ASP.NET Core, things are further optimized as we don't need to stringify the markup in the extension method.

In ~/Extesions/HtmlExtensions.cs:

using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace YourProjectNamespace.Extensions
{
    public static class HtmlExtensions
    {
        private static readonly HtmlContentBuilder _emptyBuilder = new HtmlContentBuilder();

        public static IHtmlContent BuildBreadcrumbNavigation(this IHtmlHelper helper)
        {
            if (helper.ViewContext.RouteData.Values["controller"].ToString() == "Home" ||
                helper.ViewContext.RouteData.Values["controller"].ToString() == "Account")
            {
                return _emptyBuilder;
            }

            string controllerName = helper.ViewContext.RouteData.Values["controller"].ToString();
            string actionName = helper.ViewContext.RouteData.Values["action"].ToString();

            var breadcrumb = new HtmlContentBuilder()
                                .AppendHtml("<ol class='breadcrumb'><li>")
                                .AppendHtml(helper.ActionLink("Home", "Index", "Home"))
                                .AppendHtml("</li><li>")
                                .AppendHtml(helper.ActionLink(controllerName.Titleize(),
                                                          "Index", controllerName))
                                .AppendHtml("</li>");


            if (helper.ViewContext.RouteData.Values["action"].ToString() != "Index")
            {
                breadcrumb.AppendHtml("<li>")
                          .AppendHtml(helper.ActionLink(actionName.Titleize(), actionName, controllerName))
                          .AppendHtml("</li>");
            }

            return breadcrumb.AppendHtml("</ol>");
        }
    }
}

~/Extensions/StringExtensions.cs remains the same as below (scroll down to see the MVC5 version).

In razor view, we don't need Html.Raw, as Razor takes care of escaping when dealing with IHtmlContent:

....
....
<div class="container body-content">

    <!-- #region Breadcrumb -->
    @Html.BuildBreadcrumbNavigation()
    <!-- #endregion -->

    @RenderBody()
    <hr />
...
...

ASP.NET 4, MVC 5 Solution

=== ORIGINAL / OLD ANSWER BELOW ===

(Expanding on Sean Haddy's answer above)

If you want to make it extension-driven (keeping Views clean), you can do something like:

In ~/Extesions/HtmlExtensions.cs:

(compatible with MVC5 / bootstrap)

using System.Text;
using System.Web.Mvc;
using System.Web.Mvc.Html;

namespace YourProjectNamespace.Extensions
{
    public static class HtmlExtensions
    {
        public static string BuildBreadcrumbNavigation(this HtmlHelper helper)
        {
            // optional condition: I didn't wanted it to show on home and account controller
            if (helper.ViewContext.RouteData.Values["controller"].ToString() == "Home" ||
                helper.ViewContext.RouteData.Values["controller"].ToString() == "Account")
            {
                return string.Empty;
            }

            StringBuilder breadcrumb = new StringBuilder("<ol class='breadcrumb'><li>").Append(helper.ActionLink("Home", "Index", "Home").ToHtmlString()).Append("</li>");


            breadcrumb.Append("<li>");
            breadcrumb.Append(helper.ActionLink(helper.ViewContext.RouteData.Values["controller"].ToString().Titleize(),
                                               "Index",
                                               helper.ViewContext.RouteData.Values["controller"].ToString()));
            breadcrumb.Append("</li>");

            if (helper.ViewContext.RouteData.Values["action"].ToString() != "Index")
            {
                breadcrumb.Append("<li>");
                breadcrumb.Append(helper.ActionLink(helper.ViewContext.RouteData.Values["action"].ToString().Titleize(),
                                                    helper.ViewContext.RouteData.Values["action"].ToString(),
                                                    helper.ViewContext.RouteData.Values["controller"].ToString()));
                breadcrumb.Append("</li>");
            }

            return breadcrumb.Append("</ol>").ToString();
        }
    }
}

In ~/Extensions/StringExtensions.cs:

using System.Globalization;
using System.Text.RegularExpressions;

namespace YourProjectNamespace.Extensions
{
    public static class StringExtensions
    {
        public static string Titleize(this string text)
        {
            return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(text).ToSentenceCase();
        }

        public static string ToSentenceCase(this string str)
        {
            return Regex.Replace(str, "[a-z][A-Z]", m => m.Value[0] + " " + char.ToLower(m.Value[1]));
        }
    }
}

Then use it like (in _Layout.cshtml for example):

....
....
<div class="container body-content">

    <!-- #region Breadcrumb -->
    @Html.Raw(Html.BuildBreadcrumbNavigation())
    <!-- #endregion -->

    @RenderBody()
    <hr />
...
...

Solution 4

For those using ASP.NET Core 2.0 and looking for a more decoupled approach than vulcan's HtmlHelper, I recommend having a look at using a partial view with dependency injection.

Below is a simple implementation which can easily be molded to suit your needs.

The breadcrumb service (./Services/BreadcrumbService.cs):

using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using System;
using System.Collections.Generic;

namespace YourNamespace.YourProject
{  
  public class BreadcrumbService : IViewContextAware
  {
    IList<Breadcrumb> breadcrumbs;

    public void Contextualize(ViewContext viewContext)
    {
      breadcrumbs = new List<Breadcrumb>();

      string area = $"{viewContext.RouteData.Values["area"]}";
      string controller = $"{viewContext.RouteData.Values["controller"]}";
      string action = $"{viewContext.RouteData.Values["action"]}";
      object id = viewContext.RouteData.Values["id"];
      string title = $"{viewContext.ViewData["Title"]}";   

      breadcrumbs.Add(new Breadcrumb(area, controller, action, title, id));

      if(!string.Equals(action, "index", StringComparison.OrdinalIgnoreCase))
      {
        breadcrumbs.Insert(0, new Breadcrumb(area, controller, "index", title));
      }
    }

    public IList<Breadcrumb> GetBreadcrumbs()
    {
      return breadcrumbs;
    }
  }

  public class Breadcrumb
  {
    public Breadcrumb(string area, string controller, string action, string title, object id) : this(area, controller, action, title)
    {
      Id = id;
    }

    public Breadcrumb(string area, string controller, string action, string title)
    {
      Area = area;
      Controller = controller;
      Action = action;

      if (string.IsNullOrWhiteSpace(title))
      {
         Title = Regex.Replace(CultureInfo.CurrentCulture.TextInfo.ToTitleCase(string.Equals(action, "Index", StringComparison.OrdinalIgnoreCase) ? controller : action), "[a-z][A-Z]", m => m.Value[0] + " " + char.ToLower(m.Value[1]));
      }
      else
      {
         Title = title;
      } 
    }

    public string Area { get; set; }
    public string Controller { get; set; }
    public string Action { get; set; }
    public object Id { get; set; }
    public string Title { get; set; }
  }
}

Register the service in startup.cs after AddMvc():

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddTransient<BreadcrumbService>(); 

Create a partial to render the breadcrumbs (~/Views/Shared/Breadcrumbs.cshtml):

@using YourNamespace.YourProject.Services
@inject BreadcrumbService BreadcrumbService

@foreach(var breadcrumb in BreadcrumbService.GetBreadcrumbs())
{
    <a asp-area="@breadcrumb.Area" asp-controller="@breadcrumb.Controller" asp-action="@breadcrumb.Action" asp-route-id="@breadcrumb.Id">@breadcrumb.Title</a>
}

At this point, to render the breadcrumbs simply call Html.Partial("Breadcrumbs") or Html.PartialAsync("Breadcrumbs").

Solution 5

I built this nuget package to solve this problem for myself:

https://www.nuget.org/packages/MvcBreadCrumbs/

You can contribute here if you have ideas for it:

https://github.com/thelarz/MvcBreadCrumbs

Share:
90,121
MsBao
Author by

MsBao

Updated on January 31, 2020

Comments

  • MsBao
    MsBao about 4 years

    How can dynamic breadcrumbs be achieved with ASP.net MVC?

    If you are curious about what breadcrumbs are:

    What are breadcrumbs? Well, if you have ever browsed an online store or read posts in a forum, you have likely encountered breadcrumbs. They provide an easy way to see where you are on a site. Sites like Craigslist use breadcrumbs to describe the user's location. Above the listings on each page is something that looks like this:

    s.f. bayarea craigslist > city of san francisco > bicycles

    EDIT

    I realize what is possible with the SiteMapProvider. I am also aware of the providers out there on the net that will let you map sitenodes to controllers and actions.

    But, what about when you want a breadcrumb's text to match some dynamic value, like this:

    Home > Products > Cars > Toyota

    Home > Products > Cars > Chevy

    Home > Products > Execution Equipment > Electric Chair

    Home > Products > Execution Equipment > Gallows

    ... where the product categories and the products are records from a database. Some links should be defined statically (Home for sure).

    I am trying to figure out how to do this, but I'm sure someone has already done this with ASP.net MVC.

  • MsBao
    MsBao almost 15 years
    That provider on codeplex is good, but I can't figure out how to pass route parameters to the MvcSiteMapNode from the action method. There is documentation on doing this from the Web.Sitemap, but not from the action method. Can anyone advise?
  • Barbaros Alp
    Barbaros Alp about 14 years
    How did you create dynamic fields with custom routes ?
  • MsBao
    MsBao about 14 years
    I don't remember. I take my answer back, though. It didn't work that well for me. I ended up coding the menu items by hand where needed.
  • MsBao
    MsBao over 12 years
    This was so long ago that I can't remember anything about it.
  • REMESQ
    REMESQ over 10 years
    This was helpful with Orchard CMS in my custom theme (just because I couldn't spend time figuring out how to display a breadcrumb with Orchard)
  • Arrie
    Arrie over 9 years
    this is working as intended ... i cant see how this have so little votes lol
  • guitarlass
    guitarlass over 9 years
    what if i go from detail view of one action of a controller to the index action of another will it show as ControllerOne > Details 1 > ControllerTwo ?
  • Sean Haddy
    Sean Haddy over 9 years
    Thanks guys, glad it helped out :)
  • chiwal
    chiwal over 9 years
    Not bad. Just put this in your _Layout.cshml and you're set... almost. This doesn't work so well if you have an action link that posts to the same controller but different action.
  • mgrenier
    mgrenier over 9 years
    this is great, exactly what I was looking for thanks!!
  • twip
    twip almost 9 years
    CultureInfo.CurrentCulture.TextInfo.ToTitleCase(text.ToSente‌​nceCase())was what I need inside the Titleize call for the behavior expected. Great answer nonetheless.
  • durbo
    durbo about 8 years
    Years later and worked like a charm in MVC5! Thank you.
  • JHo
    JHo over 7 years
    I spent a few hours with MvcSiteMapProvider and found it a bit much for my needs. It's quite powerful, but I'm looking for something simpler that still supports some behavior customization. MvcBreadCrumbs appears to do the trick.
  • Izzy
    Izzy over 7 years
    This answer is great! just a quick quesiton, I can't seem to figure out where the divider / is coming from? Because I want to change it from / to >
  • Izzy
    Izzy over 7 years
    I've figured it out
  • clairestreb
    clairestreb over 7 years
    @Izzy, care to share?
  • Izzy
    Izzy over 7 years
    @clairestreb The StringBuilder is adding a class to the div named breadcrumb which is used by Bootstrap css framework. So all I had to do was to modify my css with the following... .breadcrumb > li + li:before { font-family: 'FontAwesome'; content: "\f105"; } which changed the divider from / to >
  • SmartDev
    SmartDev over 6 years
    Just add @Html.Raw(Html.BuildBreadcrumbNavigation()) in your Razor view or layout (recommended). See @vulcan raven's answer above for more details...
  • Ahm3d Said
    Ahm3d Said almost 6 years
    each time the user navigate to an action the service will create new Breadcrumbs.
  • pim
    pim almost 6 years
    Yes. But object construction here is relatively cheap. If your site is high enough traffic to be concerned with this, you'll have measures in place like output caching to prevent this from ever being a problem. If you were desperate to prevent the complete rebuild, you could use a concurrent dictionary to store results based on URL.
  • IeuanW
    IeuanW over 5 years
    Does anyone now how to do this for Razor Pages?
  • vulcan raven
    vulcan raven almost 5 years
    We can use ol.breadcrumb > li selector without an additional class.
  • habib
    habib almost 5 years
    I've implemented this package. It's amazing and simple to use.
  • Dashony
    Dashony almost 4 years
    This is great, works great. What a simple solution. I've placed this into my "_layout.cshtml" file in a div "<div class="action-bar">" so it appears on every page. My question is how do I add all different buttons to appear in this div beside the breadcrumbs based on what page I'm on, since the div is in my _layout.cshtml file and I'm trying to avoid adding the div with the breadcrumbs manually on every page to add the buttons beside it... Any suggestions how to pass the buttons to the div inside my _layout page?
  • Sean Haddy
    Sean Haddy almost 4 years
    @Dashony I would suggest that you go with a solution that would allow you to pass in css classes to override the standard display here. You could have a standard node button visual for the breadcrumbs, then on the pages you wanted to override, provide a parameter in your views to override that class to display whatever you're wanting in those areas. Something even as simple as adding a variable to a ViewBag, determining if it exists, and if it does, display the associated value (class) as a configuration piece to your breadcrumbs.
  • Trần Hữu Hiền
    Trần Hữu Hiền over 3 years
    It's good for me, easy to implement and custom. Thank you!
  • Kris Bunda
    Kris Bunda over 3 years
    This is a very simple and elegant way to generate and display breadcrumbs, but I'm having some weird artifacts (like double breadcrumbs -- e.g. "dashboard > dashboard" and no chain of crumbs --e.g. missing "home > other page > dashboard") I haven't gone much further on troubleshooting yet, and probably residual code from a previously installed breadcrumb nuget package I haven't removed yet... I'd like to show how to turn your _Partial into a Bootstrap styled breadcrumb string, and how to change the bootstrap separator character(s)... should I do that in a comment?
  • pim
    pim over 3 years
    Hey @KrisBunda! Appreciate the comment. Turning this into a bootstrap style breadcrumb string should involve nothing more than altering Breadcrumbs.cshmtl. With respect to the artifacts, this solution is one dimensional in that it has no concept of "nesting". Barring that, check that you're following a standard mvc approach within your controllers.
  • A Coder
    A Coder over 3 years
    Can i add area into this? kind of Area/Controller/Action ?
  • Waleed Naveed
    Waleed Naveed almost 3 years
    There are major flaws in this approach. Firstly, it can only handle 2 pages. Secondly, when user moves from 1st page to 2nd page, the title of 1st breadCrumb becomes same as that of 2nd.