WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

カスタム Html ヘルパーで IUrlHelper を利用 (CORE)

by WebSurfer 2021年4月17日 12:27

先の記事「カスタム Tag ヘルパーで IUrlHelper を利用 (CORE)」のカスタム html ヘルパー版です。加えて、比較のために、同等な機能を部分ビューで実装する方法も書いてみました。

カスタム Html ヘルパー

カスタム html ヘルパーは ASP.NET Core MVC では推奨されてないのか、Microsoft のドキュメントには作成方法の記事が見当たりませんでした。でも、使うことは可能なようですので、先の記事と同じく内部で IUrlHelper を利用する html ヘルパーを書いてみました。

.NET Framework 版の MVC5 アプリでは、カスタム html ヘルパーの中で以下のコードのようにして UrlHelper オブジェクトを取得できます。

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 3.1 / 5.0 版の MVC アプリでは、先の記事「カスタム Tag ヘルパーで IUrlHelper を利用 (CORE)」に書きましたように、ASP.NET Core 組み込みの DI 機能を利用してコンストラクタ経由で IUrlHelperFactory と IActionContextAccessor を DI し、IUrlHelperFactory の GetUrlHelper(ActionContext) メソッドを使って IUrlHelper オブジェクトを取得しました。・・・よく調べてみると、LinkGenerator API を取得して利用する方が良さそうです。LinkGenerator を使ったカスタム html ヘルパーは下の「2021/4/18 追記」に書きます。

しかし、カスタム html ヘルパーで上のコード例のように拡張メソッドを使う場合は、静的クラス内に静的メソッドを配置することになりますので、コンストラクタ経由での DI ができません。

そこをどうするかですが、HttpContextRequestServices プロパティ から IServiceProvider(サービスコンテナーへのアクセスを提供)を取得できますので、それを使って IUrlHelperFactory と IActionContextAccessor のインスタンスを取得できるようです。

HttpContent は IHtmlHelper.ViewContext プロパティから取得できる ViewContext の HttpContext プロパティで取得できます。

それらを利用したカスタム html ヘルパーを書いてみました。以下をコードを見てください。IServiceProvider から IUrlHelperFactory と IActionContextAccessor のインスタンスを取得し、それらを使って IUrlHelper オブジェクトを取得してその Action メソッドにより url パスの文字列を取得しています。(引数の AnchorTagData クラスの定義は先の記事を見てください)

using System;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Web;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using MvcCoreApp.Models;

namespace MvcCoreApp.HtmlHelpers
{
    public static class MvcCoreAppHtmlHelpers
    {
        // 静的クラスなのでコンストラクタ経由での DI はできない。
        // IServiceProvider を取得してそれから IServiceCollection に登録されている
        // IUrlHelperFactory と IActionContextAccessor のインスタンスを取得
        public static IHtmlContent AchorTag(this IHtmlHelper helper,
                                            IEnumerable<AnchorTagData> Info)
        {
            IServiceProvider provider = helper.ViewContext.HttpContext.RequestServices;

            var urlFactory = provider.GetRequiredService<IUrlHelperFactory>();

            var actionAccessor = provider.GetRequiredService<IActionContextAccessor>();

            var urlHelper = urlFactory.GetUrlHelper(actionAccessor.ActionContext);

            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";
            }

            var output = $"<ul class=\"navbar-nav flex-grow-1\">{content}</ul>";

            return new HtmlString(output);
        }
    }
}

先のカスタム tag ヘルパーの記事と同様に、startup.cs での IActionContextAccessor のサービスへの登録は必要ですので忘れないようにしてください。具体的なコードは先の記事を見てください。

_Layout.cshtml に、上に定義した html ヘルパーが使えるように using 句を記述し、html ヘルパーに渡すモデルを初期化します。さらに、html ヘルパーを表示する場所に @Html.AchorTag(model) というコードを書きます。以下のような感じ。

@using MvcCoreApp.HtmlHelpers;

@{
    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" }};
}

// ・・・中略・・・

@Html.AchorTag(model)

// ・・・中略・・・

以上により、_Layout.cshtml に書いた @Html.AchorTag(model) というコードの部分に上の画像の赤枠で示したリンクが表示されます。

部分ビューで実装

次に、先の記事のカスタム tag ヘルパー、この記事のカスタム html ヘルパーと同等の機能を部分ビューを使って実装してみます。

ASP.NET Core の組み込みタグヘルパーのアンカータグヘルパーを以下のように部分ビュー _Navi.cshtml(名前は任意)に組み込みます。

@using MvcCoreApp.Models
@using System.Web;
@model IEnumerable<AnchorTagData>

<ul class="navbar-nav flex-grow-1">
    @foreach (var data in Model)
    {
        <li class="nav-item">
            <a class="nav-link text-dark"
               asp-controller=@data.Controller
               asp-action=@data.Action>
                @HttpUtility.HtmlEncode(data.Text)
            </a>
        </li>
    }
</ul>

上記部分ビューを、部分タグヘルパーを使って、以下のように _Layout.cshtml に配置します。

@{
    IEnumerable<AnchorTagData> model = // 省略 (上と同じ)
}

// ・・・中略・・・

<partial name="_Navi.cshtml" model="model" />

// ・・・中略・・・

これだけで、先の記事のカスタム tag ヘルパー、この記事のカスタム html ヘルパーと同様に、上の画像の赤枠で示したリンクが表示されます。この記事で書いた程度のことを実装するなら部分ビューを使うのが一番シンプルでよさそうだと思いました


-------- 2021/4/18 追記 (LinkGenerator 利用) --------

上のカスタム html ヘルパーのコードは、IUrlHelperFactory と IActionContextAccessor をサービスコンテナーから取得し、 IUrlHelperFactory.GetUrlHelper(ActionContext) メソッドを使って IUrlHelper オブジェクトを取得して利用しています。

しかし、後でよく調べたら、Microsoft のドキュメント「URL 生成の概念」に書いてあるように LinkGenerator API を取得して利用する方が良さそうと思いました。

というわけで、LinkGenerator を使ったカスタム html ヘルパーのコードを以下に書きます。IActionContextAccessor のサービスへの登録は不要ですしコードも簡単になります。LinkGenerator はサービスコンテナに登録済みのようで、以下のようにするだけで取得できます。

using System;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Web;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Generic;
using MvcCoreApp.Models;

// LinkGenarator の利用
using Microsoft.AspNetCore.Routing;

namespace MvcCoreApp.HtmlHelpers
{
    public static class MvcCoreAppHtmlHelpers
    {
        // 静的クラスなのでコンストラクタ経由での DI はできない。
        // IServiceProvider を取得してそれから IServiceCollection に登録されている
        // LinkGenarator のインスタンスを取得して利用する
        public static IHtmlContent AchorTag(this IHtmlHelper helper,
                                            IEnumerable<AnchorTagData> Info)
        {
            IServiceProvider provider = helper.ViewContext.HttpContext.RequestServices;

            var linkGenerator = provider.GetRequiredService<LinkGenerator>();

            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";
            }

            var output = $"<ul class=\"navbar-nav flex-grow-1\">{content}</ul>";

            return new HtmlString(output);
        }
    }
}

Tags: , , ,

CORE

カスタム Tag ヘルパーで IUrlHelper を利用 (CORE)

by WebSurfer 2021年4月16日 15:18

ASP.NET Core 3.1 / 5.0 MVC アプリのカスタム Tag ヘルパーで url の文字列を生成したいということがあると思います。そのために IUrlHelper インターフェイスを利用する方法を書きます。(IUrlHelper インターフェイスは .NET Framework 版の MVC5 で使われる UrlHelper クラスに相当します)

カスタム Tag ヘルパー

上の画像の赤枠の部分は、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);
        }
    }
}

Tags: , , ,

CORE

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar