Как можно достичь динамических панировочных сухарей с помощью ASP.net MVC?

#asp.net-mvc #breadcrumbs

#asp.net-mvc #панировочные сухари

Вопрос:

Как можно получить динамические панировочные сухари с ASP.net MVC?

Если вам интересно, что такое панировочные сухари:

Что такое панировочные сухари? Что ж, если вы когда-либо просматривали интернет-магазин или читали сообщения на форуме, вы, вероятно, сталкивались с панировочными сухарями. Они предоставляют простой способ увидеть, где вы находитесь на сайте. Такие сайты, как Craigslist, используют панировочные сухари для описания местоположения пользователя. Над списками на каждой странице находится что-то вроде этого:

s.f. bayarea craigslist > город Сан-Франциско > велосипеды

Редактировать

Я понимаю, что возможно с SiteMapProvider. Я также знаю о поставщиках в сети, которые позволят вам сопоставлять узлы сайта с контроллерами и действиями.

Но как насчет того, чтобы текст панировочной сухарики соответствовал некоторому динамическому значению, например, этому:

Главная страница > Продукты> Автомобили > Toyota

Главная страница > Продукты> Автомобили > Chevy

Главная страница > Продукты > Исполнительное оборудование > Электрический стул

Главная страница > Продукты > Исполнительное оборудование > Виселицы

… где категории продуктов и сами продукты являются записями из базы данных. Некоторые ссылки должны быть определены статически (обязательно Home).

Я пытаюсь выяснить, как это сделать, но я уверен, что кто-то уже сделал это с ASP.net MVC.

Ответ №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.

Комментарии:

1. Это было полезно с Orchard CMS в моей пользовательской теме (просто потому, что я не мог тратить время на выяснение, как отобразить панировочный сухарь с помощью Orchard)

2. это работает так, как задумано … я не могу понять, почему за это так мало голосов, лол

3. Спустя годы и в MVC5 все работало как по маслу! Спасибо.

4. @Dashony Я бы посоветовал вам выбрать решение, которое позволило бы вам передавать классы css для переопределения стандартного отображения здесь. У вас мог бы быть стандартный визуальный элемент кнопки узла для панировочных сухарей, затем на страницах, которые вы хотите переопределить, укажите параметр в своих представлениях, чтобы переопределить этот класс для отображения всего, что вы хотите в этих областях. Что-то даже такое простое, как добавление переменной в ViewBag, определение того, существует ли она, и если существует, отображение связанного значения (класса) в виде элемента конфигурации в ваших панировочных сухарях.

5. Для меня это хорошо, легко реализовать и настроить. Спасибо!

Ответ №2:

ASP.NET 5 (он же ASP.NET Ядро), базовое решение MVC

В ASP.NET В целом, все дополнительно оптимизировано, поскольку нам не нужно упорядочивать разметку в методе расширения.

В ~/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

=== ОРИГИНАЛЬНЫЙ / СТАРЫЙ ОТВЕТ НИЖЕ ===

(Расширяя ответ Шона Хэдди выше)

Если вы хотите сделать его управляемым расширением (сохраняя чистоту представлений), вы можете сделать что-то вроде:

В ~/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 />
...
...
  

Комментарии:

1. CultureInfo.CurrentCulture.TextInfo.ToTitleCase(text.ToSentenceCase()) было ли то, что мне нужно внутри вызова Titleize для ожидаемого поведения. Тем не менее, отличный ответ.

2. Этот ответ великолепен! просто небольшой вопрос, я, кажется, не могу понять, откуда берется разделитель / ? Потому что я хочу изменить его с / на >

3. Я понял это

4. @Izzy, не хочешь поделиться?

5. @clairestreb StringBuilder добавляет класс в div с именем, breadcrumb который используется Bootstrap css framework. Итак, все, что мне нужно было сделать, это изменить мой css следующим образом… .breadcrumb > li li:before { font-family: 'FontAwesome'; content: "f105"; } который изменил разделитель с / на >

Ответ №3:

В codeplex есть инструмент для этого: http://mvcsitemap.codeplex.com / [проект перенесен на github]

Редактировать:

Существует способ получить SiteMapProvider из базы данных: http://www.asp.net/Learn/data-access/tutorial-62-cs.aspx

Возможно, вы сможете модифицировать инструмент mvcsitemap, чтобы использовать его для получения желаемого.

Комментарии:

1. Этот поставщик в codeplex хорош, но я не могу понять, как передать параметры маршрута в MvcSiteMapNode из метода action. Документация по выполнению этого есть в Web.Sitemap, но не в методе action. Кто-нибудь может посоветовать?

2. что, если я перейду от детального просмотра одного действия контроллера к индексному действию другого, будет ли это отображаться как ControllerOne > Details 1 > ControllerTwo ?

Ответ №4:

Я создал этот пакет nuget, чтобы решить эту проблему для себя:

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

Вы можете внести свой вклад здесь, если у вас есть идеи для этого:

https://github.com/thelarz/MvcBreadCrumbs

Комментарии:

1. Я потратил несколько часов на MvcSiteMapProvider и обнаружил, что это многовато для моих нужд. Это довольно мощный инструмент, но я ищу что-то более простое, которое все еще поддерживает некоторую настройку поведения. Похоже, что MvcBreadCrumbs справляется со своей задачей.

2. Я реализовал этот пакет. Это потрясающе и просто в использовании.

Ответ №5:

Для тех, кто использует ASP.NET Ядро 2.0 и в поисках более развязанного подхода, чем HtmlHelper от vulcan, я рекомендую взглянуть на использование частичного представления с внедрением зависимостей.

Ниже приведена простая реализация, которую можно легко сформировать в соответствии с вашими потребностями.

Служба панировочных сухарей ( ./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.cs после AddMvc() :

 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") .

Комментарии:

1. каждый раз, когда пользователь переходит к действию, сервис будет создавать новые панировочные сухари.

2. ДА. Но построение объектов здесь относительно дешево. Если у вашего сайта достаточно высокий трафик, чтобы беспокоиться об этом, у вас будут приняты меры, такие как кэширование выходных данных, чтобы это никогда не стало проблемой. Если вы отчаянно хотели предотвратить полную перестройку, вы могли бы использовать параллельный словарь для хранения результатов на основе URL.

3. Это очень простой и элегантный способ создания и отображения панировочных сухарей, но у меня возникают некоторые странные артефакты (например, двойные панировочные сухари — например, «панель мониторинга> dashboard» и отсутствие цепочки крошек — например, отсутствует «главная> другая страница > панель мониторинга») Я пока не продвинулся дальше устранения неполадок и, вероятно, сохранил код из ранее установленного пакета breadcrumb nuget, который я еще не удалил… Я хотел бы показать, как превратить ваш _Partial в строку панировочных сухарей в стиле Bootstrap и как изменить символы-разделители начальной загрузки… должен ли я сделать это в комментарии?

4. Привет, @KrisBunda! Ценю комментарий. Превращение этого в строку панировочных сухарей в стиле bootstrap не должно включать ничего, кроме изменения Breadcrumbs.cshmtl . Что касается артефактов, это решение является одномерным в том смысле, что в нем нет понятия «вложенности». За исключением этого, убедитесь, что вы следуете стандартному подходу mvc в своих контроллерах.

5. В этом подходе есть серьезные недостатки. Во-первых, он может обрабатывать только 2 страницы. Во-вторых, когда пользователь переходит с 1-й страницы на 2-ю, заголовок 1-й панировочной сухари становится таким же, как у 2-й.

Ответ №6:

MvcSiteMapProvider от Мартена Баллиау работал для меня довольно хорошо.

Я создал небольшое приложение mvc для тестирования его провайдера: Тест MvcSiteMapProvider (404)

Комментарии:

1. Как вы создавали динамические поля с пользовательскими маршрутами?

2. Я не помню. Однако я беру свой ответ обратно. У меня это не очень хорошо сработало. В итоге я вручную кодировал пункты меню там, где это было необходимо.

3. Это было так давно, что я ничего об этом не помню.

Ответ №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" amp;amp; controller != "Default") amp;amp; 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" amp;amp; 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 amp;amp; 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]) amp;amp; text[i - 1] != ' ')
                newText.Append(' ');
            newText.Append(text[i]);
        }
        return newText.ToString();
    }
}
  

If can определенно может быть улучшен (вероятно, не охватывает все возможные случаи), но это не подводило меня до сих пор.

Комментарии:

1. Просто добавьте @Html.Raw(Html.BuildBreadcrumbNavigation()) в свой Razor view или layout (рекомендуется). Смотрите ответ @vulcan raven выше для получения более подробной информации…