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

EditorFor での属性の付与方法

by WebSurfer 2018年2月23日 18:32

Microsoft のドキュメント What's New in ASP.NET MVC 5.1 によると、MVC 5.1 から、HTML ヘルパー EditorFor を使っても、生成される html 要素に任意の属性を付与できるようになったそうです。(実際、自分が試した限りですが、 MVC 4 では以下に述べるいずれの方法でもダメでした)

何を今頃と言われるかもしれませんが(汗)、最近そのあたりのことを調べて自分にとっていろいろ新発見があったので、備忘録としてまとめて書いておきます。

(1) TextBoxFor の場合

LabelFor, TextBoxFor などでは、第 2 引数に匿名オブジェクトを渡して html 属性を付与することができます。例えば以下のようにすると、

@Html.TextBoxFor(m => m.LastName, 
  new { @class = "coolTextBox", data_date = "12-02-2012" })

それから生成される html 要素は以下のようになります。(見やすくなるよう改行を入れてます)

<input 
  class="coolTextBox" 
  data-date="12-02-2012" 
  id="LastName" 
  name="LastName" 
  type="text" 
  value="Gee" />

本題とは関係ないですが、ついでに、匿名オブジェクトを書くときの注意点を上記の TestBoxFor の設定を例にして書いておきます。

匿名オブジェクトは、上記の場合 C# のコードですから、プロパティ名は C# としての識別子の条件を満たす必要があります。class は C# では予約語ですからそのまま使うことはできません。なので @ を付与して識別子名として使えるようにしています。(コンパイラには @ は無視されて class が識別子名と見なされます)

html 要素の属性としてよくある名前で、C# の識別子として使えないケースではハイフン '-' を含む属性名があると思います。例えば上記のように data-date="12-02-2012" という属性を設定したい場合です。その場合はハイフン '-' に代えてアンダースコア '_' を使います。ASP.NET によって html に変換されると、'_' は '-' に変換されます。

(2) EditorFor の場合

EditorFor では第 2 引数を上記の TextBoxFor と同様に設定しても無視されます。例えば以下のようにすると、

@Html.EditorFor(m => m.LastName, 
  new { @class = "coolTextBox", data_date = "12-02-2012" })

生成される html 要素は以下のようになります。

<input 
  class="text-box single-line" 
  id="LastName" 
  name="LastName" 
  type="text" 
  value="Gee" />

上記の class="text-box single-line" はデフォルトでハードコーディングされている属性だそうです。そのあたりのことは Overwriting the class on a `Html.EditorFor` とか、その記事が参照している ASP.NET MVC 2 Templates, Part 3: Default Templates に書いてありますので興味があれば見てください。

MVC 5.1 より前ではデフォルトを変更したりそれに追加することができなかったので EditorFor に代えて TextBoxFor を使うという手段を取っていたらしいです。MSDN Forum のスレッド Override CSS for textbox in MVC4 にそのような記事があります。

ところが、MVC 5.1 からは一番上に紹介した記事にあるように、EditorFor にも任意の属性が付与できるようになりました。

以下のように、匿名オブジェクトのプロパティ名を htmlAttributes とし、それに付与したい属性の匿名オブジェクトを設定するというように、入れ子で匿名オブジェクトを設定します。

@Html.EditorFor(m => m.LastName,
  new { htmlAttributes =
    new { @class = "coolTextBox", data_date = "12-02-2012" } })

その結果生成される html 要素は以下のようになります。

<input 
  class="coolTextBox text-box single-line" 
  data-date="12-02-2012" 
  id="LastName" 
  name="LastName" 
  type="text" 
  value="Gee" />

デフォルトでハードコーディングされている class="text-box single-line" と、上記コードで設定した class="coolTextBox" data-date="12-02-2012" がマージされているのが分かるでしょうか。

上に紹介した記事 What's New in ASP.NET MVC 5.1 によると Bootstrap をサポートするためとのことです。それでどのようにサポートできるのかは調べ切れてません。

TextBoxFor と EditorFor でなぜ違うのかは Html.EditorFor and htmlAttributes に詳しく書いてありますので、興味があれば見てください。

Tags: , ,

MVC

EditorFor と DisplayFor の違い

by WebSurfer 2013年3月27日 21:38

Html Helper の EditorFor と DisplayFor が、POST 要求に対する応答で、異なった値を表示することがあります。その理由と解決策を備忘録として書いておきます。

EditFor と DisplayFor の違い

上の画像を表示した Model, View, Controller のコードは以下の通りです(説明に関係ない部分は省略してあります)。

Model

public class FileModel
{
  public string FileName { get; set; }
}

View

@model MvcApplication1.Models.FileModel

@using (Html.BeginForm())
{
  @Html.EditorFor(m => m.FileName)
  <input type="submit" value="POST" />
  @Html.DisplayFor(m => m.FileName)
}

Controller

public class FileController : Controller
{
  [HttpPost]
  public ActionResult Index(FileModel m)
  {
    m.FileName += ".ext";
    return View(m);
  }
}

ユーザーがテキストボックス(EditorFor 相当)にファイル名を入力し、form を submit(POST 要求)すると、Controller で model の FileName プロパティに拡張子 ".ext" を追加します。

この場合、応答画面では、DisplayFor には拡張子 ".ext" が追加されて表示されるものの、EditorFor には ".ext" は追加されません。 上の画像の例を見てください。

つまり、EditorFor には POST された値がそのまま使われています。

何故そうなるかと言うと、例えば、EditorFor の入力にユーザーが間違った値を入力した場合、サーバーに POST された時に検証 NG とし、エラーメッセージ(例:入力が間違っています)を表示するとともに、EditorFor にはユーザー入力をそのまま表示したいという理由だそうです。

具体的には、POST された値は ModelState ディクショナリ(model ではない)に格納されていて、Html Helper はまず ModelState ディクショナリを調べて、そこに値があればそれを表示するようになっています。

詳しくは以下のページを見てください。

ASP.NET MVC’s Html Helpers Render the Wrong Value!

value が POST されるか否かが問題になります。例えば、EditorFor に限らず、HiddenFor もその value は POST されるので、結果は EditorFor と同じになります。

一方、DisplayFor には、POST される value などというものはないので、model の値が使われます。

EditorFor にも DisplayFor と同様に model の値(拡張子 .ext が追加されたもの)を表示するには、ModelState ディクショナリを Clear することです。

そうすれば model から値を取ってくるので、期待した結果になります。具体的には以下のコードを追加します。

if (ModelState.IsValid)
{
  ModelState.Clear();
}

ただしこのようにして、検証結果 OK で POST 要求への応答をそのまま返すのは、��重 POST の問題が起こりうるので、好ましくないようです。

二重 POST 問題

MVC に限らず、Web アプリケーション開発の基本として、Post/Redirect/Get (PRG) パターンを使う・・・即ち、POST 要求への応答をそのまま返すのは検証結果 NG の場合のみとし、検証結果 OK の場合は、例え同じページを表示するにしても、リダイレクトしてブラウザに GET 要求させるのがよいそうです。(詳しくはリンク先を見てください)

Post/Redirect/Get による問題解決

Post/Redirect/Get パターンを使うように、上記のコードを書き直すと、以下のようになると思います。

public class FileController : Controller
{
  [HttpGet]
  public ActionResult Index()
  {
    FileModel model;

    object obj = TempData["ValidationResult"];
    if (obj is FileModel)
    {
      // 検証 OK ⇒ 再確認
      model = (FileModel)obj;
    }
    else
    {
      // 初期画面
      model = new FileModel();
    }
    return View(model);
  }

  [HttpPost]
  public ActionResult Index(FileModel m)
  {
    if (ModelState.IsValid)
    {
      // 検証 OK ⇒ 再確認
      m.FileName =
        ModelState["FileName"].Value.AttemptedValue
        + ".ext";
      TempData["ValidationResult"] = m;
      return RedirectToAction("Index");
    }
    else
    {
      // 検証失敗
      return View(m);
    }
  }
}

Tags:

MVC

About this blog

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

Calendar

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

View posts in large calendar