【问题标题】:How can dynamic breadcrumbs be achieved with ASP.net MVC?如何使用 ASP.net MVC 实现动态面包屑?
【发布时间】:2009-07-01 00:28:47
【问题描述】:

dynamic breadcrumbs如何用ASP.net MVC实现?

如果您对面包屑是什么感到好奇:

什么是面包屑?好吧,如果您曾经浏览过在线商店或阅读过论坛中的帖子,那么您可能会遇到面包屑。它们提供了一种简单的方法来查看您在网站上的位置。像 Craigslist 这样的网站使用面包屑来描述用户的位置。每个页面的列表上方是这样的:

顺丰海湾地区 craigslist > 旧金山市 > 自行车

编辑

我意识到使用 SiteMapProvider 可以实现什么。我还知道网络上的提供程序可以让您将站点节点映射到控制器和操作。

但是,当您希望面包屑的文本匹配某个动态值时该怎么办,如下所示:

首页 > 产品 > 汽车 > 丰田

首页 > 产品 > 汽车 > 雪佛兰

首页 > 产品展示 > 执行设备 > 电动椅

首页 > 产品展示 > 执行设备 > 绞刑架

...其中产品类别和产品是来自数据库的记录。一些链接应该是静态定义的(当然是 Home)。

我正试图弄清楚如何做到这一点,但我确信有人已经用 ASP.net MVC 做到了这一点。

【问题讨论】:

    标签: asp.net-mvc breadcrumbs


    【解决方案1】:

    站点地图绝对是一种方法...或者,您可以自己编写一个! (当然只要遵循标准的 MVC 规则)......我刚写了一个,我想我会在这里分享。

    @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()) 
    }
    

    希望有人会觉得这很有帮助,这正是我在 SO 中搜索 MVC 面包屑时所寻找的。​​p>

    【讨论】:

    • 这对我的自定义主题中的 Orchard CMS 很有帮助(只是因为我无法花时间弄清楚如何使用 Orchard 显示面包屑)
    • 这是按预期工作的......我看不出这票票怎么这么少,哈哈
    • 多年后,在 MVC5 中的工作就像一个魅力!谢谢。
    • @Dashony 我建议您采用一种解决方案,该解决方案允许您传入 css 类以覆盖此处的标准显示。您可以为面包屑提供一个标准节点按钮视觉效果,然后在您想要覆盖的页面上,在您的视图中提供一个参数来覆盖该类以显示您在这些区域中想要的任何内容。甚至像将变量添加到 ViewBag 一样简单,确定它是否存在,如果存在,则将关联的值(类)作为配置片段显示到您的面包屑。
    • 这对我有好处,易于实施和定制。谢谢!
    【解决方案2】:

    ASP.NET 5(又名 ASP.NET Core),MVC 核心解决方案

    在 ASP.NET Core 中,由于我们不需要在扩展方法中对标记进行字符串化,因此进一步优化。

    ~/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 与下面保持一致(向下滚动查看 MVC5 版本)。

    在 razor 视图中,我们不需要 Html.Raw,因为 Razor 在处理 IHtmlContent 时会负责转义:

    ....
    ....
    <div class="container body-content">
    
        <!-- #region Breadcrumb -->
        @Html.BuildBreadcrumbNavigation()
        <!-- #endregion -->
    
        @RenderBody()
        <hr />
    ...
    ...
    

    ASP.NET 4、MVC 5 解决方案

    === 下面是原始/旧答案 ===

    (扩展 Sean Haddy 的上述回答)

    如果你想让它由扩展驱动(保持视图干净),你可以这样做:

    ~/Extesions/HtmlExtensions.cs:

    (兼容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();
            }
        }
    }
    

    ~/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]));
            }
        }
    }
    

    然后像这样使用它(例如在 _Layout.cshtml 中):

    ....
    ....
    <div class="container body-content">
    
        <!-- #region Breadcrumb -->
        @Html.Raw(Html.BuildBreadcrumbNavigation())
        <!-- #endregion -->
    
        @RenderBody()
        <hr />
    ...
    ...
    

    【讨论】:

    • 这太好了,正是我想要的,谢谢!!
    • CultureInfo.CurrentCulture.TextInfo.ToTitleCase(text.ToSentenceCase())是我在 Titleize 调用中需要的预期行为。尽管如此,很好的答案。
    • 这个答案很棒!只是一个简单的问题,我似乎无法弄清楚分隔符/ 来自哪里?因为我想把/改成&gt;
    • 我想通了
    • @clairestreb StringBuilder 正在向名为 breadcrumb 的 div 添加一个类,该类由 Bootstrap css 框架使用。所以我所要做的就是用以下内容修改我的css....breadcrumb &gt; li + li:before { font-family: 'FontAwesome'; content: "\f105"; }将分隔线从/更改为&gt;
    【解决方案3】:

    在 codeplex 上有一个工具可以做到这一点:http://mvcsitemap.codeplex.com/ [project moved to github]

    编辑:

    有一种方法可以从数据库中派生 SiteMapProvider:http://www.asp.net/Learn/data-access/tutorial-62-cs.aspx

    您也许可以修改 mvcsitemap 工具以使用它来获得您想要的。

    【讨论】:

    • codeplex 上的提供者很好,但我不知道如何将路由参数从操作方法传递给 MvcSiteMapNode。 Web.Sitemap 中有关于执行此操作的文档,但操作方法中没有。谁能给点建议?
    • 如果我从一个控制器的一个动作的详细视图转到另一个控制器的索引动作,它会显示为ControllerOne &gt; Details 1 &gt; ControllerTwo 吗?
    【解决方案4】:

    我构建了这个 nuget 包来为自己解决这个问题:

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

    如果你有想法,可以在这里投稿:

    https://github.com/thelarz/MvcBreadCrumbs

    【讨论】:

    • 我在 MvcSiteMapProvider 上花了几个小时,发现它有点满足我的需要。它非常强大,但我正在寻找仍然支持某些行为自定义的更简单的东西。 MvcBreadCrumbs 似乎可以解决问题。
    • 我已经实现了这个包。它令人惊叹且易于使用。
    【解决方案5】:

    对于那些使用 ASP.NET Core 2.0 并寻找比 vulcan 的 HtmlHelper 更解耦的方法的人,我建议看看使用带有 dependency injection 的局部视图。

    下面是一个简单的实现,可以很容易地根据您的需要进行调整。

    面包屑服务 (./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; }
      }
    }
    

    startup.csAddMvc()之后注册服务:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
    
        services.AddTransient<BreadcrumbService>(); 
    

    创建部分以呈现面包屑 (~/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>
    }
    

    此时,要渲染面包屑,只需调用Html.Partial("Breadcrumbs")Html.PartialAsync("Breadcrumbs")

    【讨论】:

    • 每次用户导航到某个操作时,该服务都会创建新的面包屑导航。
    • 是的。但是这里的对象构造相对便宜。如果您的站点的流量足够高,需要关注此问题,您将采取措施,例如输出缓存,以防止此问题成为问题。如果您迫切希望阻止完全重建,您可以使用并发字典来存储基于 URL 的结果。
    • 这是一种非常简单而优雅的生成和显示面包屑的方法,但我有一些奇怪的工件(比如双面包屑——例如“仪表板>仪表板”并且没有面包屑链——例如缺少“主页>其他页面>仪表板”)我还没有进一步解决故障,可能还有我尚未删除的先前安装的面包屑nuget包中的残留代码......我想展示如何打开您的 _Partial 到 Bootstrap 样式的面包屑字符串,以及如何更改引导分隔符...我应该在评论中这样做吗?
    • 嘿@KrisBunda!欣赏评论。将其转换为引导样式的面包屑字符串应该只涉及更改 Breadcrumbs.cshmtl。关于工件,这个解决方案是一维的,因为它没有“嵌套”的概念。除此之外,请检查您是否在控制器中遵循标准的 mvc 方法。
    • 这种方法存在重大缺陷。首先,它只能处理 2 页。其次,当用户从第 1 页移动到第 2 页时,第 1 个面包屑的标题与第 2 个相同。
    【解决方案6】:

    Maarten Balliauw's MvcSiteMapProvider 对我来说效果很好。

    我创建了一个小型 mvc 应用来测试他的提供者:MvcSiteMapProvider Test (404)

    【讨论】:

    • 您是如何使用自定义路由创建动态字段的?
    • 我不记得了。不过,我收回我的回答。它对我来说效果不佳。我最终在需要的地方手动编码了菜单项。
    • 这是很久以前的事了,我什么都不记得了。
    【解决方案7】:

    对于感兴趣的人,我做了一个HtmlExtension 的改进版本,它也考虑了区域,此外还使用反射来检查区域内是否有默认控制器或控制器内是否有索引操作:

    public static class HtmlExtensions
        {
            public static MvcHtmlString BuildBreadcrumbNavigation(this HtmlHelper helper)
            {
                string area = (helper.ViewContext.RouteData.DataTokens["area"] ?? "").ToString();
                string controller = helper.ViewContext.RouteData.Values["controller"].ToString();
                string action = helper.ViewContext.RouteData.Values["action"].ToString();
    
                // add link to homepage by default
                StringBuilder breadcrumb = new StringBuilder(@"
                    <ol class='breadcrumb'>
                        <li>" + helper.ActionLink("Homepage", "Index", "Home", new { Area = "" }, new { @class="first" }) + @"</li>");
    
                // add link to area if existing
                if (area != "")
                {
                    breadcrumb.Append("<li>");
                    if (ControllerExistsInArea("Default", area)) // by convention, default Area controller should be named Default
                    {
                        breadcrumb.Append(helper.ActionLink(area.AddSpaceOnCaseChange(), "Index", "Default", new { Area = area }, new { @class = "" }));
                    }
                    else
                    {
                        breadcrumb.Append(area.AddSpaceOnCaseChange());
                    }
                    breadcrumb.Append("</li>");
                }
    
                // add link to controller Index if different action
                if ((controller != "Home" && controller != "Default") && action != "Index")
                {
                    if (ActionExistsInController("Index", controller, area))
                    {
                        breadcrumb.Append("<li>");
                        breadcrumb.Append(helper.ActionLink(controller.AddSpaceOnCaseChange(), "Index", controller, new { Area = area }, new { @class = "" }));
                        breadcrumb.Append("</li>");
                    }
                }
    
                // add link to action
                if ((controller != "Home" && controller != "Default") || action != "Index")
                {
                    breadcrumb.Append("<li>");
                    //breadcrumb.Append(helper.ActionLink((action.ToLower() == "index") ? controller.AddSpaceOnCaseChange() : action.AddSpaceOnCaseChange(), action, controller, new { Area = area }, new { @class = "" }));
                    breadcrumb.Append((action.ToLower() == "index") ? controller.AddSpaceOnCaseChange() : action.AddSpaceOnCaseChange());
                    breadcrumb.Append("</li>");
                }
    
                return MvcHtmlString.Create(breadcrumb.Append("</ol>").ToString());
            }
    
            public static Type GetControllerType(string controller, string area)
            {
                string currentAssembly = Assembly.GetExecutingAssembly().GetName().Name;
                IEnumerable<Type> controllerTypes = Assembly.GetExecutingAssembly().GetTypes().Where(o => typeof(IController).IsAssignableFrom(o));
    
                string typeFullName = String.Format("{0}.Controllers.{1}Controller", currentAssembly, controller);
                if (area != "")
                {
                    typeFullName = String.Format("{0}.Areas.{1}.Controllers.{2}Controller", currentAssembly, area, controller);
                }
    
                return controllerTypes.Where(o => o.FullName == typeFullName).FirstOrDefault();
            }
    
            public static bool ActionExistsInController(string action, string controller, string area)
            {
                Type controllerType = GetControllerType(controller, area);
                return (controllerType != null && new ReflectedControllerDescriptor(controllerType).GetCanonicalActions().Any(x => x.ActionName == action));
            }
    
            public static bool ControllerExistsInArea(string controller, string area)
            {
                Type controllerType = GetControllerType(controller, area);
                return (controllerType != null);
            }
    
    
        public static string AddSpaceOnCaseChange(this string text)
        {
            if (string.IsNullOrWhiteSpace(text))
                return "";
            StringBuilder newText = new StringBuilder(text.Length * 2);
            newText.Append(text[0]);
            for (int i = 1; i < text.Length; i++)
            {
                if (char.IsUpper(text[i]) && text[i - 1] != ' ')
                    newText.Append(' ');
                newText.Append(text[i]);
            }
            return newText.ToString();
        }
    }
    

    如果可以肯定可以改进(可能没有涵盖所有可能的情况),但直到现在它并没有让我失望。

    【讨论】:

    • 只需在 Razor 视图或布局中添加 @Html.Raw(Html.BuildBreadcrumbNavigation())(推荐)。有关更多详细信息,请参阅上面@vulcan raven 的答案...
    猜你喜欢
    • 2010-09-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多