ASP.net MVCで 動的ブレッドクラム を実現するにはどうすればよいですか?
パンくずリストとは何かを知りたい場合:
パンくずとは何ですか?まあ、オンラインストアを閲覧したり、フォーラムで投稿を読んだことがあるなら、パンくずリストに出会った可能性があります。サイトのどこにいるかを簡単に確認できます。 Craigslistのようなサイトは、パンくずリストを使用してユーザーの場所を記述します。各ページのリストの上には、次のようなものがあります。
s.f.ベイエリアクレイグリスト>サンフランシスコ市>自転車
SiteMapProviderで可能なことを理解しています。また、サイトノードをコントローラーおよびアクションにマップできるプロバイダーがネット上にあることも知っています。
しかし、パンくずリストのテキストを次のような動的な値に一致させる場合はどうでしょうか。
ホーム>製品>車>トヨタ
ホーム>製品>車>シボレー
ホーム>製品>実行装置>電気椅子
ホーム>製品>実行装置>絞首台
...ここで、製品カテゴリと製品はデータベースのレコードです。一部のリンクは静的に定義する必要があります(ホーム)。
私はこれを行う方法を理解しようとしていますが、誰かがすでにASP.net MVCでこれを行っていると確信しています。
Codeplexでこれを行うツールがあります。 http://mvcsitemap.codeplex.com/ [project githubに移動)
編集:
データベースからSiteMapProviderを派生させる方法があります: http://www.asp.net/Learn/data-access/tutorial-62-cs.aspx
Mvcsitemapツールを変更して、それを使用して必要なものを取得できる場合があります。
サイトマップは間違いなく行く方法の1つです...または、自分で作成することもできます! (もちろん、標準のMVCルールが守られている限り)... 1つだけを書いたので、ここで共有すると思いました。
@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())
}
うまくいけば、誰かがこれが役立つことを願っています。これは、MVCパンくずリストでSOを検索したときにまさに探していたものです。
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バージョンを見るには下にスクロールしてください)。
カミソリビューでは、Html.Raw
、RazorはIHtmlContent
を処理するときにエスケープを処理します。
....
....
<div class="container body-content">
<!-- #region Breadcrumb -->
@Html.BuildBreadcrumbNavigation()
<!-- #endregion -->
@RenderBody()
<hr />
...
...
===オリジナル/以下の古い回答===
(上記のSean Haddyの回答を拡大)
拡張駆動型(ビューをクリーンに保つ)にしたい場合は、次のようなことができます。
~/Extesions/HtmlExtensions.cs
:
(MVC5 /ブートストラップと互換性があります)
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 />
...
...
興味のある人は誰でも、エリアを考慮したHtmlExtension
の改良版を作成し、さらにReflectionを使用して、エリア内にデフォルトコントローラがあるか、コントローラ内にインデックスアクションがあるかどうかを確認しました。
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();
}
}
間違いなく改善できる場合(おそらくすべての可能なケースをカバーしているわけではありません)が、それは今まで私を失敗させませんでした。
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; }
}
}
_
AddMvc()
の後の_startup.cs
_にサービスを登録します:
_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")
を呼び出します。
Maarten BalliauwのMvcSiteMapProvider は私にとってはかなりうまくいきました。
私は彼のプロバイダーをテストするために小さなmvcアプリを作成しました: MvcSiteMapProviderテスト (404)