ASP.NET Core 3.1 / 5.0 MVC アプリのカスタム Tag ヘルパーで url の文字列を生成したいということがあると思います。そのために IUrlHelper インターフェイスを利用する方法を書きます。(IUrlHelper インターフェイスは .NET Framework 版の MVC5 で使われる UrlHelper クラスに相当します)
上の画像の赤枠の部分は、Visual Studio のテンプレートで生成する ASP.NET Core MVC プロジェクトで自動生成される _Layout.cshtml では組み込みのアンカータグヘルパーを並べてリンクを作成していますが、これをカスタム tag ヘルパーを使って表示してみます。
ASP.NET Core MVC でもカスタム html ヘルパーは使えます。ただし、静的クラス / 静的メソッドになるので ASP.NET Core 組み込みのコンストラクタ経由での DI 機能が使えません。そのためか、カスタム html ヘルパーは Core では推奨されてないような感じで、Mocrosoft のドキュメントにも見当たりません。というわけで、この記事ではカスタム html ヘルパーではなくてカスタム tag ヘルパーで実装してみました。
(カスタム html ヘルパーまたは部分ビュー + 組み込み tag ヘルパー使ってもこの記事と同様な機能は実装できます。それらについては別の記事「カスタム Html ヘルパーで IUrlHelper を利用 (CORE)」に書きました。やってみた結果、この記事で書いた機能程度のことを実装するなら部分ビュー + 組み込み tag ヘルパーを使うのが一番シンプルでよさそうだと思いました)
カスタム tag ヘルパーの作成方法の概要は Microsoft のドキュメント「ASP.NET Core のタグ ヘルパー作成」の記事が参考になると思います。
その記事でほとんど用は足りると思いますが、もし、カスタム tag ヘルパー内でコントローラーとアクションメソッドの名前から url パスを作成する必要があると問題です。例えば、以下のようにアクションメソッドに [HttpPost("/fileupload")] というような属性が付与されているような場合、単純に名前から文字列連結で /upload/index というパスを組み立てて a タグに href="/upload/index" と設定したりすると HTTP 404 Not Found となってしまいます。
namespace MvcCoreApp.Controllers
{
public class UploadController : Controller
{
[HttpGet("/fileupload")]
public IActionResult Index()
{
return View();
}
// ・・・中略・・・
}
}
ちなみに、組み込みのアンカータグヘルパーを使って asp-controller="Upload" asp-action="Index" とすると、それから生成される a タグには href="/fileupload" と設定されます。
そのようにならないと困るので、tag ヘルパー内で Url.Action("index", "upload"); というように url ヘルパーを使ってパスを取得したいということになると思います。そのために IUrlHelper オブジェクトを tag ヘルパーのコードの中でどのように取得するかがこの記事の課題です。
.NET Framework 版の MVC5 アプリでは、カスタム html ヘルパーの中で以下のコードのようにして url ヘルパーのオブジェクトを取得できます。
using System.Web;
using System.Web.Mvc;
namespace Mvc5App.HtmlHelpers
{
public static class Mvc5AppHelpers
{
public static IHtmlString AchorTag(this HtmlHelper helper,
string contoller,
string action,
string text)
{
var urlHepler = new UrlHelper(helper.ViewContext.RequestContext);
var path = urlHepler.Action(action, contoller);
return MvcHtmlString.Create(
$"<a href=\"{path}\">{HttpUtility.HtmlEncode(text)}</a>");
}
}
}
Core 版の MVC アプリではもうちょっと頑張ってコードを書かないと url ヘルパーのオブジェクトを取得できません。前置きが長きなってしまいましたが、以下にその方法を書きます。
基本的には、IUrlHelperFactory インターフェイスの IUrlHelperFactory.GetUrlHelper(ActionContext) メソッドを使って IUrlHelper オブジェクトを取得することになります。・・・と思いましたが、LinkGenerator API を取得して利用する方が良さそうです。LinkGenerator を使ったカスタム tag ヘルパーは下の「2021/4/18 追記」に書きます。
IUrlHelperFactory は、Visual Studio のテンプレートで作る ASP.NET Core 3.1 / 5.0 MVC アプリのプロジェクトにはデフォルトで IServiceCollection(DI コンテナ)に登録されているようで、tag ヘルパーのコンストラクタ経由で DI できます。
IUrlHelper オブジェクトを生成するためにIUrlHelperFactory.GetUrlHelper(ActionContext) メソッドを使いますが、その引数の「現在の要求に関連付けられている ActionContext」をどのように取得するかが問題でした。調べてみると、IActionContextAccessor を IServiceCollection に登録してコンストラクタ経由で DI できるようにし、その ActionContext プロパティを使うということのようです。
まず、IActionContextAccessor をサービスに登録します。具体的には、startup.cs の ConfigureServices メソッドで以下のようにします。
// 追加
using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace MvcCoreApp
{
public class Startup
{
// ・・・中略・・・
public void ConfigureServices(IServiceCollection services)
{
// 追加
services.AddSingleton<IActionContextAccessor,
ActionContextAccessor>();
// ・・・中略・・・
}
カスタム tag ヘルパーは、Visual Studio のテンプレートで作った ASP.NET Core MVC アプリのプロジェクトのルートに TagHelpers という名前のフォルダを作って実装しました。コードは以下の通りで、コンストラクタ経由で IUrlHelperFactory と IActionContextAccessor が DI されるようにしています。
using System.Collections.Generic;
using Microsoft.AspNetCore.Razor.TagHelpers;
using MvcCoreApp.Models;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using System.Web;
namespace MvcCoreApp.TagHelpers
{
public class NaviTagHelper : TagHelper
{
private readonly IUrlHelperFactory _urlHelperFactory;
private readonly IActionContextAccessor _actionContextAccessor;
public NaviTagHelper(IUrlHelperFactory urlHelperFactory,
IActionContextAccessor actionContextAccessor)
{
_urlHelperFactory = urlHelperFactory;
_actionContextAccessor = actionContextAccessor;
}
public IEnumerable<AnchorTagData> Info { get; set; }
public override void Process(TagHelperContext context,
TagHelperOutput output)
{
var urlHelper = _urlHelperFactory.GetUrlHelper(
_actionContextAccessor.ActionContext);
output.TagName = "ul";
var @class = "navbar-nav flex-grow-1";
output.Attributes.SetAttribute("class", @class);
var content = "";
foreach (var data in Info)
{
var path = urlHelper.Action(
action: data.Action,
controller: data.Controller);
content += "<li class=\"nav-item\">" +
$"<a class=\"nav-link text-dark\" href=\"{path}\">" +
$"{HttpUtility.HtmlEncode(data.Text)}</a>" +
"</li>\r\n";
}
output.Content.SetHtmlContent(content);
}
}
}
上のカスタム tag ヘルパーで使っているモデル AnchorTagData クラスの定義は以下の通りです。
namespace MvcCoreApp.Models
{
public class AnchorTagData
{
public string Controller { get; set; }
public string Action { get; set; }
public string Text { get; set; }
}
_Layout.cshtml に、上に定義した tag ヘルパーが使えるように addTagHelper ディレクティブを記述し、tag に渡すモデルを初期化します。さらに、tag ヘルパーを表示する場所に <navi info="model"></navi> というタグを配置します。以下のような感じ。
@addTagHelper MvcCoreApp.TagHelpers.NaviTagHelper, MvcCoreApp
@{
IEnumerable<AnchorTagData> model =
new List<AnchorTagData> {
new AnchorTagData { Controller="Home", Action="Index", Text="Home" },
new AnchorTagData { Controller="Home", Action="Privacy", Text="Privacy" },
new AnchorTagData { Controller="People", Action="Index", Text="People" },
new AnchorTagData { Controller="Messages", Action="Index", Text="Messages" },
new AnchorTagData { Controller="Validation", Action="Create", Text="Validation" },
new AnchorTagData { Controller="Upload", Action="Index", Text="FileUpload" },
new AnchorTagData { Controller="Products", Action="Index", Text="Products" },
new AnchorTagData { Controller="Ajax", Action="Index", Text="Ajax" },
new AnchorTagData { Controller="IHttpClientFactory", Action="Index", Text="HttpClient" }};
}
// ・・・中略・・・
<navi info="model"></navi>
// ・・・中略・・・
以上により、_Layout.cshtml に配置した <navi info="model"></navi> の部分に、上の画像の赤枠で示したリンクが表示されます。FileUpload へのリンク先も正しく href="/fileupload" となります。
-------- 2021/4/18 追記 (LinkGenerator 利用) --------
上のカスタム tag ヘルパーのコードは、IUrlHelperFactory と IActionContextAccessor を DI により取得し、 IUrlHelperFactory.GetUrlHelper(ActionContext) メソッドを使って IUrlHelper オブジェクトを取得して利用しています。
しかし、後でよく調べたら、Microsoft のドキュメント「URL 生成の概念」に書いてあるように LinkGenerator API を取得して利用する方が良さそうと思いました。
というわけで、LinkGenerator 使ったカスタム tag ヘルパーのコードを以下に書きます。IActionContextAccessor のサービスへの登録は不要ですしコードも簡単になります。LinkGenerator はサービスコンテナに登録済みのようで、以下のように引数に含めるだけで DI できます。
using System.Collections.Generic;
using Microsoft.AspNetCore.Razor.TagHelpers;
using MvcCoreApp.Models;
using System.Web;
// LinkGenarator の利用
using Microsoft.AspNetCore.Routing;
namespace MvcCoreApp.TagHelpers
{
public class NaviTagHelper : TagHelper
{
private readonly LinkGenerator _linkGenerator;
public NaviTagHelper(LinkGenerator linkGenerator)
{
_linkGenerator = linkGenerator;
}
public IEnumerable<AnchorTagData> Info { get; set; }
public override void Process(TagHelperContext context,
TagHelperOutput output)
{
output.TagName = "ul";
var @class = "navbar-nav flex-grow-1";
output.Attributes.SetAttribute("class", @class);
var content = "";
foreach (var data in Info)
{
var path = _linkGenerator.GetPathByAction(
action: data.Action,
controller: data.Controller);
content += "<li class=\"nav-item\">" +
$"<a class=\"nav-link text-dark\" href=\"{path}\">" +
$"{HttpUtility.HtmlEncode(data.Text)}</a>" +
"</li>\r\n";
}
output.Content.SetHtmlContent(content);
}
}
}