route - ¿Cómo se pueden lograr migas de pan dinámicas con ASP.net MVC?
tag helpers asp net core (7)
ASP.NET 5 (también conocido como ASP.NET Core), MVC Core Solution
En ASP.NET Core, las cosas se optimizan aún más, ya que no es necesario que el código de extensión esté marcado en el método de extensión.
En ~/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
sigue siendo el mismo que el de abajo (desplácese hacia abajo para ver la versión MVC5).
En la vista de la maquinilla de afeitar, no necesitamos Html.Raw
, ya que Razor se encarga de escapar al tratar con IHtmlContent
:
....
....
<div class="container body-content">
<!-- #region Breadcrumb -->
@Html.BuildBreadcrumbNavigation()
<!-- #endregion -->
@RenderBody()
<hr />
...
...
ASP.NET 4, Solución MVC 5
=== RESPUESTA ORIGINAL / ANTIGUA ABAJO ===
(Ampliando la respuesta de Sean Haddy arriba)
Si desea convertirlo en una extensión (manteniendo las vistas limpias), puede hacer algo como:
En ~/Extesions/HtmlExtensions.cs
:
(compatible con 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();
}
}
}
En ~/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]));
}
}
}
Luego, úselo como (en _Layout.cshtml por ejemplo):
....
....
<div class="container body-content">
<!-- #region Breadcrumb -->
@Html.Raw(Html.BuildBreadcrumbNavigation())
<!-- #endregion -->
@RenderBody()
<hr />
...
...
¿Cómo se pueden lograr migas de pan dinámicas con ASP.net MVC?
Si tienes curiosidad por saber qué migas de pan son:
¿Qué son las migas de pan? Bueno, si alguna vez ha navegado por una tienda en línea o ha leído publicaciones en un foro, es probable que se haya encontrado con las migajas de pan. Proporcionan una manera fácil de ver dónde se encuentra en un sitio. Sitios como Craigslist utilizan migas de pan para describir la ubicación del usuario. Encima de los listados en cada página hay algo que se ve así:
sf bayarea craigslist> ciudad de san francisco> bicicletas
EDITAR
Me doy cuenta de lo que es posible con el SiteMapProvider. También estoy al tanto de los proveedores que hay en la red que le permitirán asignar los códigos de sitio a los controladores y las acciones.
Pero, ¿qué pasa cuando quieres que el texto de una ruta de navegación coincida con algún valor dinámico, como esto:
Inicio> Productos> Automóviles> Toyota
Inicio> Productos> Automóviles> Chevy
Inicio> Productos> Equipos de ejecución> Silla eléctrica
Inicio> Productos> Equipos de Ejecución> Horca
... donde las categorías de productos y los productos son registros de una base de datos. Algunos enlaces deben definirse estáticamente (Inicio seguro).
Estoy tratando de averiguar cómo hacer esto, pero estoy seguro de que alguien ya lo ha hecho con ASP.net MVC.
Construí este paquete nuget para resolver este problema por mí mismo:
https://www.nuget.org/packages/MvcBreadCrumbs/
Puedes contribuir aquí si tienes ideas para ello:
Hay una herramienta para hacer esto en codeplex: http://mvcsitemap.codeplex.com/ [proyecto movido a github]
Editar:
Hay una forma de obtener un SiteMapProvider a partir de una base de datos: http://www.asp.net/Learn/data-access/tutorial-62-cs.aspx
Es posible que pueda modificar la herramienta mvcsitemap para usarla y obtener lo que desea.
Los sitemap son definitivamente un camino a seguir ... ¡alternativamente, puedes escribir uno tú mismo! (por supuesto, siempre y cuando se sigan las reglas estándar de MVC) ... Acabo de escribir una, pensé que compartiría aquí.
@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())
}
Esperamos que alguien encuentre esto útil, esto es exactamente lo que estaba buscando cuando busqué SO para migajas de pan MVC.
Para aquellos que usan ASP.NET Core 2.0 y buscan un enfoque más desacoplado que el HtmlHelper de vulcan, recomiendo echar un vistazo al uso de una vista parcial con la inyección de dependencia .
A continuación se muestra una implementación simple que se puede moldear fácilmente para satisfacer sus necesidades.
El servicio ./Services/BreadcrumbService.cs
( ./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; }
}
}
Registre el servicio en startup.cs
después de AddMvc()
:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddTransient<BreadcrumbService>();
Cree un parcial para representar las rutas de navegación ( ~/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>
}
En este punto, para representar las migajas de pan, simplemente llame a Html.Partial("Breadcrumbs")
o Html.PartialAsync("Breadcrumbs")
.
Para quien esté interesado, hice una versión mejorada de una HtmlExtension
que también está considerando Áreas y además usa Reflexión para verificar si hay un controlador predeterminado dentro de un área o una acción de índice dentro de un controlador:
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();
}
}
Definitivamente, se puede mejorar (probablemente no cubre todos los casos posibles), pero hasta ahora no me falló.
El MvcSiteMapProvider de Maarten Balliauw funcionó bastante bien para mí.
Creé una pequeña aplicación mvc para probar su proveedor: MvcSiteMapProvider Test (404)