WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

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

by WebSurfer 17. April 2021 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 16. April 2021 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

MVC に JSON をバインドするには [FromBody] が必要 (CORE)

by WebSurfer 12. April 2021 15:14

ASP.NET Core MVC のアクションメソッドに JSON 文字列をボディに含めて POST 送信する場合、アクションメソッドの引数に [FromBody] 属性を付与しないと、なぜかモデルバインディングに失敗するという話を書きます。(Core 3.1 と 5.0 で確認。1.x, 2.x. 6.0 は未検証・未確認です)

モデルバインディングに失敗

上の画像がその例で、アクションメソッドの引数には [FromBody] を付与していません。RequestJson は生成されて引数の requestJson に渡されますが、各プロパティには送信されてきた JSON 文字列の値が代入されていません。(各プロパティの型の既定値のまま)

引数の RequestJson クラスの定義は以下の通りです。

namespace MvcCore5App2.Models
{
    public class RequestJson
    {
        public string Method { get; set; }
        public bool MuteHttpExceptions { get; set; }
        public string ContentType { get; set; }
        public string Payload { get; set; }
    }
}

JSON 文字列の送信は以下の画像のように Fiddler の Composer を使いました。(注: RequestJson クラスの各プロパティ名と JSON の {"name";"value"} の "name" の大文字小文字が違いますがそこは関係ないです。先の記事「JsonSerializer の Camel Casing (CORE)」に書いたように、Core v3.x 以降で使われる System.Text.Json 名前空間の JsonSerializer クラスは、デフォルトではデシリアライズする際の大文字小文字を区別するのですが、アプリ内で区別しない設定にしているようです)

Fiddler の Composer で送信

.NET Framework 版の MVC5 では [FromBody] を付与しなくてもモデルバインディングされたのですが・・・

何故か ASP.NET Core MVC では [FromBody] を付与しないと、送信されてきたクエリ文字列、フォームボディ、ルートパラメータ、クッキー、要求ヘッダ、ファイルなどのどれから取得するかが分からず、モデルバインディング (JSON 文字列から値を取得して各プロパティに代入) できないということのようです。(想像です)

以下の画像のように、アクションメソッドの引数に [FromBody] を付与すればモデルバインディングに成功します。送信した JSON 文字列は上の Fiddler の Composer 画像のものと同じですが、JSON の value が RequestJson クラスの各プロパティに代入されてから引数の requestJson に渡されています。

モデルバインディングに成功

上にも書きましたが、.NET Framework 版の MVC では [FromBody] 無しでも問題なくモデルバインディングされます。Core 版 MVC でも同じだろうと思っているとハマります。何を隠そう自分もハマって 2 時間ぐらい悩みました。

さらに、Core 版でも Web API の場合は [FromBody] 無しでも問題なくモデルバインディングされます。以下の画像を見てください。

Web API の場合

Web API では、コンプレックス型(この記事の例では RequestJson クラス)の場合、デフォルトではボディからパラメータを取得するということになるそうで、それゆえ [FromBody] 無しでも問題ないということかもしれません(想像です)。

Web API にせよ MVC5 にせよ、JSON 文字列をボディに含めて POST 送信するなら、それを受けるアクションメソッドの引数には [FromBody] を付与しておくというのが正解と思いました。

Tags: , , , ,

CORE

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  May 2021  >>
MoTuWeThFrSaSu
262728293012
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar