Comment fil d'Ariane dynamique peut être réalisé avec ASP.net MVC?
Si vous êtes curieux de savoir ce que sont les miettes de pain:
Qu'est-ce que le fil d'Ariane? Eh bien, si vous avez déjà parcouru une boutique en ligne ou lu des messages dans un forum, vous avez probablement rencontré du fil d'Ariane. Ils permettent de voir facilement où vous vous trouvez sur un site. Des sites comme Craigslist utilisent du fil d'Ariane pour décrire l'emplacement de l'utilisateur. Au-dessus des listes sur chaque page se trouve quelque chose qui ressemble à ceci:
s.f. Craigslist bayarea> Ville de San Francisco> Vélos
Je réalise ce qui est possible avec SiteMapProvider. Je connais également les fournisseurs sur le net qui vous permettront de mapper les sitenodes aux contrôleurs et aux actions.
Mais qu'en est-il lorsque vous souhaitez que le texte d'un fil d'Ariane corresponde à une valeur dynamique, comme ceci:
Accueil> Produits> Voitures> Toyota
Accueil> Produits> Voitures> Chevy
Accueil> Produits> Équipement d'exécution> Chaise électrique
Accueil> Produits> Équipement d'exécution> Potence
... où les catégories de produits et les produits sont des enregistrements d'une base de données. Certains liens doivent être définis de manière statique (Home à coup sûr).
J'essaie de comprendre comment faire cela, mais je suis sûr que quelqu'un l'a déjà fait avec ASP.net MVC.
Il existe un outil pour le faire sur codeplex: http://mvcsitemap.codeplex.com/ [projet déplacé vers github]
Modifier:
Il existe un moyen de dériver un SiteMapProvider à partir d'une base de données: http://www.asp.net/Learn/data-access/tutorial-62-cs.aspx
Vous pourrez peut-être modifier l'outil mvcsitemap pour l'utiliser pour obtenir ce que vous voulez.
Les plans de site sont certainement une façon de procéder ... sinon, vous pouvez en écrire un vous-même! (bien sûr tant que les règles MVC standard sont suivies) ... Je viens d'en écrire une, je pensais que je partagerais ici.
@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())
}
J'espère que quelqu'un trouvera cela utile, c'est exactement ce que je cherchais lorsque j'ai recherché SO pour le fil d'Ariane MVC.
Dans ASP.NET Core, les choses sont encore optimisées car nous n'avons pas besoin de filtrer le balisage dans la méthode d'extension.
Dans ~/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
reste le même que ci-dessous (faites défiler vers le bas pour voir la version MVC5).
En vue rasoir, nous n'avons pas besoin de Html.Raw
, car Razor prend soin de s'échapper lorsqu'il s'agit de IHtmlContent
:
....
....
<div class="container body-content">
<!-- #region Breadcrumb -->
@Html.BuildBreadcrumbNavigation()
<!-- #endregion -->
@RenderBody()
<hr />
...
...
=== ORIGINAL/VIEILLE RÉPONSE CI-DESSOUS ===
(Développant la réponse de Sean Haddy ci-dessus)
Si vous voulez le rendre basé sur l'extension (en gardant les vues propres), vous pouvez faire quelque chose comme:
Dans ~/Extesions/HtmlExtensions.cs
:
(compatible avec 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();
}
}
}
Dans ~/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]));
}
}
}
Ensuite, utilisez-le comme (dans _Layout.cshtml par exemple):
....
....
<div class="container body-content">
<!-- #region Breadcrumb -->
@Html.Raw(Html.BuildBreadcrumbNavigation())
<!-- #endregion -->
@RenderBody()
<hr />
...
...
Pour ceux qui sont intéressés, j'ai fait une version améliorée d'un HtmlExtension
qui prend également en compte les zones et utilise en outre la réflexion pour vérifier s'il y a un contrôleur par défaut dans une zone ou une action d'index dans un contrôleur:
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();
}
}
Si peut certainement être amélioré (ne couvre probablement pas tous les cas possibles), mais il ne m'a pas échoué jusqu'à présent.
Pour ceux qui utilisent ASP.NET Core 2.0 et recherchent une approche plus découplée que HtmlHelper de vulcan, je recommande d'avoir un aperçu de l'utilisation d'une vue partielle avec injection de dépendance .
Vous trouverez ci-dessous une mise en œuvre simple qui peut facilement être moulée pour répondre à vos besoins.
Le service de fil d'Ariane (./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; }
}
}
Enregistrez le service dans startup.cs
Après AddMvc()
:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddTransient<BreadcrumbService>();
Créez un partiel pour rendre le fil d'Ariane (~/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>
}
À ce stade, pour afficher le fil d'Ariane, appelez simplement Html.Partial("Breadcrumbs")
ou Html.PartialAsync("Breadcrumbs")
.
J'ai construit ce paquet nuget pour résoudre ce problème par moi-même:
https://www.nuget.org/packages/MvcBreadCrumbs/
Vous pouvez contribuer ici si vous avez des idées:
MvcSiteMapProvider de Maarten Balliauw a plutôt bien fonctionné pour moi.
J'ai créé une petite application mvc pour tester son fournisseur: Test MvcSiteMapProvider (404)