カスタムタグヘルパーの色々な出力方法(ASP.NET CORE 6 MVC)

概要

カスタムタグヘルパーで色々なタグを出力します。
ここでは以下のものを説明します。
・ベースとなるタグヘルパーのサンプルを作る
・標準のInputタグを出力したい(ベースとなるタグヘルパーで説明)
・複数のタグを出力したい(ベースとなるタグヘルパーで説明)
・もとのタグを消したい
・もとのタグの前にタグを追加したい
・もとのタグの後にタグを追加したい
・もとのタグの中にタグを追加したい
・もとのタグとタグ内のコンテント(タグで囲まれた値、InnerHtml)の間にタグの階層を追加したい
・タグヘルパーにModelを渡したい(ベースとなるタグヘルパーで説明)
・タグヘルパーに配列を渡したい
・タグヘルパーにTypeを渡したい

ベースとなるタグヘルパー作り

まずはベースとなるタグヘルパーを作って、それを修正しながら色々な出力を行っていきます。
ついでにベースとなるタグヘルパーのなかで、IHtmlGeneratorを使った標準のInputタグ(TextBox)の出力と、複数のタグを出力する方法も説明します。
タグヘルパーのサンプルを動かすために必要になるのは以下のものになります。
※ControllerとView以外はパスはどこでも大丈夫です。
・タグヘルパー(Helpers/SampleTextTagHelper.cs)
・タグヘルパーを表示するためのサンプルView(Views/Test/Index.cshtml)
・サンプルViewで使うモデル(Models/Test/ViewModel/TestViewModel.cs)
・サンプルViewを表示するためのコントローラー(Controllers/TestController.cs)

タグヘルパー(Helpers/SampleTextTagHelper.cs)

「label」、「input(text)」、「input(hidden)」の3つのタグを出力するサンプルです。
labelとinput(text)はIHtmlGeneratorでASP.NET CORE標準のタグを出力しています。
IHtmlGeneratorは優秀で、ModelStateの考慮、Client Side Validationの考慮をしてくれます。
input(hidden)は、TagBuilderでタグを生成して出力しています。

詳細はコードのコメントを参照してください。
ポイントは以下になります。

  • 「HtmlTargetElement」でタグヘルパークラスとHTML上のタグ名を紐づけます(HtmlTargetElementを指定しない場合はクラス名(からTagHelperを除いたもの)をケバブケース(xxx-xxx)に変換した名前がタグ名に使われる)。
  • ViewContextはコンストラクターでDIするのではなく、プロパティに[ViewContext]属性を付与することでDIします。
  • タグの属性の値はプロパティで受け取れます。デフォルトだとプロパティ名をケバブケースに変換したものが対応する属性名になります。明示的にプロパティに対する属性名を決めたい場合は「HtmlAttributeName」属性をプロパティに付与します。
  • 処理に必要なクラスはコンストラクターでDIできます。
#nullable enable

using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace TestWebCore3.Helpers
{
    /// <summary>
    /// サンプルテキストタグヘルパー
    /// 通常のテキストタグを出力し
    /// 同じ値でHiddenタグを出力する
    /// テキストタグは「IHtmlGenerator」で生成しているため
    /// ModelStateの考慮やClient Side Validationの考慮がされている
    /// </summary>
    /// <remarks>
    /// HtmlTargetElementはカスタムタグヘルパーに紐づけるタグ名
    /// HtmlTargetElementを付与しない場合はデフォルト値として
    /// クラス名(からTagHelperをのぞいた)をケバブケースに変換したものが
    /// タグ名として使われる
    /// SampleTextTagHelperの場合はSampleTextの部分をケバブに変えたもの
    /// sample-textがデフォルトのタグ名になる
    /// 今はデフォルトと同じタグ名を明示的にHtmlTargetElementに指定している
    /// </remarks>
    [HtmlTargetElement("sample-text")]
    public class SampleTextTagHelper : TagHelper
    {
        /// <summary>
        /// コンテキスト
        /// TagHelperの場合[ViewContext]属性をつけておくとDIしてくれる
        /// [HtmlAttributeNotBound]は、タグの属性にview-contextというのがあった場合に
        /// 本プロパティにバインドさせないようにするために付与する
        /// </summary>
        [HtmlAttributeNotBound]
        [ViewContext]
        public ViewContext? ViewContext { get; set; }

        /// <summary>
        /// 慣例的にasp-for属性でタグに出力するモデルを受け取る
        /// </summary>
        [HtmlAttributeName("asp-for")]
        public ModelExpression? For { get; set; }

        /// <summary>
        /// Html属性(dynamic objectで受け取る)
        /// HtmlAttributeNameを指定しないとデフォルトの属性の名前は
        /// ケバブ(html-attributes)になる
        /// </summary>
        public object? HtmlAttributes { get; set; }

        /// <summary>
        /// 一般的なタグヘルパーを出力するためのクラス
        /// </summary>
        private IHtmlGenerator _Generator;

        /// <summary>
        /// Model情報を取得するためのクラス
        /// 実は今回のサンプルでは使わない。
        /// </summary>
        private IModelMetadataProvider _ModelMetadataProvider;

        /// <summary>
        /// Client Side Validation用のAttribute出力クラス
        /// 初期段階では使わない
        /// </summary>
        private ValidationHtmlAttributeProvider _ValidationHtmlProvider;

        /// <summary>
        /// コンストラクター
        /// </summary>
        /// <param name="generator">標準のinputタグなどを出力するためのクラス</param>
        /// <param name="modelMetadataProvider">Model情報を取得するためのクラス</param>
        /// <param name="validationHtmlProvider">Client Side Validation用のクラス</param>
        public SampleTextTagHelper(IHtmlGenerator generator
            , IModelMetadataProvider modelMetadataProvider
            , ValidationHtmlAttributeProvider validationHtmlProvider)
        {
            _Generator = generator;
            _ModelMetadataProvider = modelMetadataProvider;
            _ValidationHtmlProvider = validationHtmlProvider;
        }

        /// <summary>
        /// タグ出力
        /// </summary>
        /// <param name="context">コンテキスト</param>
        /// <param name="output">出力</param>
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            ArgumentNullException.ThrowIfNull(For, nameof(For));
            ArgumentNullException.ThrowIfNull(HtmlAttributes, nameof(HtmlAttributes));
            ArgumentNullException.ThrowIfNull(ViewContext, nameof(ViewContext));


            // 普通のラベルタグを出力する
            TagBuilder labelTag = _Generator.GenerateLabel(ViewContext
                , For.ModelExplorer, For.Name, For.Metadata.DisplayName, null);

            // 普通のinputタグを出力する
            TagBuilder inputTag = _Generator.GenerateTextBox(ViewContext
                , For.ModelExplorer, For.Name, For.Model, "", HtmlAttributes);

            // もしくはタグを自分で生成して出力する
            TagBuilder hiddenTag = new TagBuilder("input");
            hiddenTag.MergeAttribute("type", "hidden");
            hiddenTag.MergeAttribute("name", For.Name + "_hidden");
            hiddenTag.MergeAttribute("value", For.Model?.ToString());

            // タグを出力に追加する
            output.Content.AppendHtml(labelTag);
            output.Content.AppendHtml(inputTag);
            output.Content.AppendHtml(hiddenTag);
        }
    }
}

サンプルViewで使用するモデル(Models/Test/ViewModel/TestViewModel.cs)

テストID、テスト番号という2つのstring型の項目をもつ、どこにでもある普通のモデルです。

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace TestWebCore3.Models.Test.ViewModel
{
    /// <summary>
    /// テスト用のモデル
    /// </summary>
    public class TestViewModel
    {
        /// <summary>
        /// テストID
        /// </summary>
        [DisplayName("テストID")]
        [Required(AllowEmptyStrings = true, ErrorMessage = "{0}を入力してください")]
        public string? TestId { get; set; }

        /// <summary>
        /// テスト番号
        /// </summary>
        [DisplayName("テスト番号")]
        [Required(AllowEmptyStrings = true, ErrorMessage = "{0}を入力してください")]
        [RegularExpression("^[0-9]+", ErrorMessage = "{0}は数字で入力してください")]
        public string? TestNo { get; set; }
    }
}

タグヘルパーを表示するためのサンプルView(Views/Test/Index.cshtml)

テストIDとテスト番号をSampleTextTagHelper(タグ名はsample-text)で出力します。
ポイントは以下のとおりです。

  • addTagHelperでタグヘルパーが含まれるアセンブリ名を指定します。アセンブリ名はnamespaceではなく、ソースが格納されるDLL名(プロジェクト名)になります。
  • jquery.validate.jsとjquery.validate.unobtrusive.jsはClient Side Validationを有効にするためのJavaScriptになります。
  • タグのasp-for属性はSampleTextTagHelperのForプロパティにバインドされ、html-attributes属性はSampleTextTagHelperのHtmlAttributesプロパティにバインドされます。
  • asp-forにはモデルのプロパティを指定しています(Model.TestId)。タグヘルパーのタグの属性の型がstring以外(ModelExpressionなど)の場合は””の中身はモデルのプロパティを指定したことになります。逆に属性がstring型の場合は固定文字列を指定したことになります。string型の属性に変数を格納したい場合、test-str = “@(Model.TestId)”など、@()で括って変数を展開します(test-strは例としてstring属性のプロパティ)。
@model TestWebCore3.Models.Test.ViewModel.TestViewModel
@* TagHelperを使用する際はAddTagHelperでTagHelperが格納されているAssembly名を指定する
   Assembly名はnamespaceではなくプロジェクト名(DLLの名前)になる。
   本プロジェクトの名前はTestWebCore3なのでTestWebCore3を指定している
*@
@addTagHelper *, TestWebCore3

@section scripts {
    @* Jquery.jsは_Layout.cshtmlで読込み済の想定 *@
    @* Client Side Validationを有効にするために下記2つのJSを読み込んでいる *@
    <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
}

<form asp-action="Check" asp-controller="Test">
    @* エラーを出力する *@
    @Html.ValidationSummary()

    @*
       カスタムタグヘルパー model.TestIdの値をバインドする
       dynamic object型でhtmlタグの属性を渡す
    *@
    <sample-text asp-for="TestId" html-attributes="@(new {tabindex=1, style="width:80px;"})">
        あいうえお
    </sample-text>

    @*
        カスタムタグヘルパー model.TestNoの値をバインドする
        dynamic object型でhtmlタグの属性を渡す
    *@
    <sample-text asp-for="TestNo" html-attributes="@(new {tabindex=2, style="width:100px;"})">
        かきくけこ
    </sample-text>

    <input type="submit" value="クリック" class="btn btn-default" />
</form>

サンプルViewを表示するためのコントローラー(Controllers/TestController.cs)

何の変哲もないテスト用のコントローラーです。
画面初期表示用がIndexで、チェックボタンクリック時の処理がCheckになります。

using Microsoft.AspNetCore.Mvc;
using TestWebCore3.Models.Test.ViewModel;

namespace TestWebCore3.Controllers
{
    /// <summary>
    /// テスト用コントローラー
    /// </summary>
    public class TestController : Controller
    {
        /// <summary>
        /// 初期表示
        /// </summary>
        /// <returns></returns>
        public IActionResult Index()
        {
            TestViewModel model = new TestViewModel();
            return View("Index",model);
        }

        /// <summary>
        /// テスト用なのでなにもしない
        /// </summary>
        /// <param name="inputModel">本当はViewModelではなくInput用のModelを用意して受ける</param>
        /// <returns></returns>
        public IActionResult Check(TestViewModel model)
        {
            return View("Index", model);
        }

    }
}

実行結果(画面)

実行結果(HTML)

一部抜粋です。<sample-text>の子要素としてlabel、input(text)、input(hidden)が出力されています。
Client Side Validation用の属性が出力されているのがわかります。

    <div b-h5jfl3y9ba="" class="container">
        <main b-h5jfl3y9ba="" role="main" class="pb-3">
            <form action="https://localhost:7257/Test/Check" method="post" novalidate="novalidate">
                <div class="validation-summary-valid" data-valmsg-summary="true">
                    <ul><li style="display:none"></li></ul>
                </div>
                <sample-text>
                    <label for="TestId">テストID</label>
                    <input data-val="true" data-val-required="テストIDを入力してください" id="TestId" name="TestId" style="width:80px;" tabindex="1" type="text" value="">
                    <input name="TestId_hidden" type="hidden" value="">
                </sample-text>

                <sample-text>
                    <label for="TestNo">テスト番号</label>
                    <input data-val="true" data-val-regex="テスト番号は数字で入力してください" data-val-regex-pattern="^[0-9]+" data-val-required="テスト番号を入力してください" id="TestNo" name="TestNo" style="width:100px;" tabindex="2" type="text" value="">
                    <input name="TestNo_hidden" type="hidden" value="">
                </sample-text>

                <input type="submit" value="クリック" class="btn btn-default">
                <input name="__RequestVerificationToken" type="hidden" value="CfDJ8NoLZtPk-fNLjKQV2jeAXtcCCzlx7Cht_P_wdHyCWzrpWVm5GmmGRqq0XbTPeiS29hmBwEHdgCpVzi9-ymlo5SOap7M2ehlNnvWAS0GfflrxcxTYI2D__s2fCHy_SQRp8MQYR8MZD6EBGGdw5hCfz7U">
            </form>
        </main>
    </div>

    <footer b-h5jfl3y9ba="" class="border-top footer text-muted">
        <div b-h5jfl3y9ba="" class="container">
            © 2023 - TestWebCore3 - <a href="https://localhost:7257/Home/Privacy">Privacy</a>
        </div>
    </footer>
    <script src="./- TestWebCore3_files/jquery.min.js.ダウンロード"></script>
    <script src="./- TestWebCore3_files/bootstrap.bundle.min.js.ダウンロード"></script>
    <script src="./- TestWebCore3_files/site.js.ダウンロード"></script>

    
    <script src="./- TestWebCore3_files/jquery.validate.js.ダウンロード"></script>
    <script src="./- TestWebCore3_files/jquery.validate.unobtrusive.js.ダウンロード"></script>

もとのタグを消したい

やっと本題です。ベースとなるタグヘルパーでは<sample-text>タグの子要素としてinputタグが出力されましたが、<sample-text>の代わりにinputタグを出力したい場合の方法になります。
output.SuppressOutput()を実行します。
以下、SampleTextTagHelper.csのProcessメソッドの抜粋です。

            // 自分自身のタグを消す
            output.SuppressOutput();

            // タグを出力に追加する
            output.Content.AppendHtml(labelTag);
            output.Content.AppendHtml(inputTag);
            output.Content.AppendHtml(hiddenTag);

実行後のHTML(抜粋)
<sample-text>タグが出力されていないのがわかると思います。

<form action="/Test/Check" method="post" novalidate="novalidate">
    <div class="validation-summary-valid" data-valmsg-summary="true">
        <ul><li style="display:none"></li></ul>
    </div>

    <label for="TestId">テストID</label>
    <input data-val="true" data-val-required="テストIDを入力してください" id="TestId" name="TestId" style="width:80px;" tabindex="1" type="text" value="">
    <input name="TestId_hidden" type="hidden" value="">

    <label for="TestNo">テスト番号</label>
    <input data-val="true" data-val-regex="テスト番号は数字で入力してください" data-val-regex-pattern="^[0-9]+" data-val-required="テスト番号を入力してください" id="TestNo" name="TestNo" style="width:100px;" tabindex="2" type="text" value="">
    <input name="TestNo_hidden" type="hidden" value="">

    <input type="submit" value="クリック" class="btn btn-default">
    <input name="__RequestVerificationToken" type="hidden" value="CfDJ8NoLZtPk-fNLjKQV2jeAXte2Nxreyu_XC_2dhz7LY6drNChHfS7t0_zHLDDs0m_NluGo8AXRKXnagOKr8T4GsG-kMLORLz-3pTfqE2OMA1nsRr5ENkaLp_odLs-H_jURL9qMSN1Hs_apD53Y1IM6uIs">
</form>

もとのタグの前・もとのタグの後にタグを追加したい

もとのタグの前にタグを追加したい場合はoutput.PreElement.AppendHtmlを使い、
もとのタグの後にタグを追加したい場合はoutput.PostElement.AppendHtmlを使います。
以下、SampleTextTagHelper.csのProcessメソッドの抜粋です。

            // もとのタグの前にタグを追加する
            output.PreElement.AppendHtml(labelTag);

            // タグを出力に追加する
            output.Content.AppendHtml(inputTag);

            // もとのタグの後にタグを追加する
            output.PostElement.AppendHtml(hiddenTag);

実行後のHTML(抜粋)

<form action="/Test/Check" method="post" novalidate="novalidate">
    <div class="validation-summary-valid" data-valmsg-summary="true">
        <ul><li style="display:none"></li></ul>
    </div>

    <label for="TestId">テストID</label>
    <sample-text>
        <input data-val="true" data-val-required="テストIDを入力してください" id="TestId" name="TestId" style="width:80px;" tabindex="1" type="text" value="">
    </sample-text>
    <input name="TestId_hidden" type="hidden" value="">

    <label for="TestNo">テスト番号</label>
    <sample-text>
        <input data-val="true" data-val-regex="テスト番号は数字で入力してください" data-val-regex-pattern="^[0-9]+" data-val-required="テスト番号を入力してください" id="TestNo" name="TestNo" style="width:100px;" tabindex="2" type="text" value="">
    </sample-text>
    <input name="TestNo_hidden" type="hidden" value="">

    <input type="submit" value="クリック" class="btn btn-default">
    <input name="__RequestVerificationToken" type="hidden" value="CfDJ8NoLZtPk-fNLjKQV2jeAXtf45IYTqTED1kJI6oM1dMT9ovvbNt7D8JmacHI3NwYQK1_piX71NLzbvxeqNTnRjZyppfuPi_dHeVbuqc-jgTlP6cCb6XtmbjnQZClBw7O1DRJMFlLVciQPVA0MpxHI0i0">
</form>

もとのタグの中で、もとのコンテントの前後にタグを追加したい

output.PreContent.AppendHtmlやoutput.PostContent.AppendHtmlを使います。

            // もとのタグの中で、コンテントの前にタグを追加する
            output.PreContent.AppendHtml(labelTag);
            output.PreContent.AppendHtml(inputTag);

            // もとのタグの中で、コンテントの後にタグを追加する
            output.PostContent.AppendHtml(hiddenTag);

もとのコンテント(あいうえお)を残した状態でタグが追加されているのがわかると思います(抜粋)。

    <sample-text>
        <label for="TestId">テストID</label>
        <input data-val="true" data-val-required="テストIDを入力してください" id="TestId" name="TestId" style="width:80px;" tabindex="1" type="text" value="">
        あいうえお
        <input name="TestId_hidden" type="hidden" value="">
    </sample-text>

    <sample-text>
        <label for="TestNo">テスト番号</label>
        <input data-val="true" data-val-regex="テスト番号は数字で入力してください" data-val-regex-pattern="^[0-9]+" data-val-required="テスト番号を入力してください" id="TestNo" name="TestNo" style="width:100px;" tabindex="2" type="text" value="">
        かきくけこ
        <input name="TestNo_hidden" type="hidden" value="">
    </sample-text>

もとのタグとコンテント(タグで囲まれた値、InnerHtml)の間にタグの階層を追加したい

output.GetChildContentAsync()でコンテント(例では「あいうえお」や「かきくけこ」)を取得後、TagBuilderでタグを作成し、そのタグにコンテントを設定して、作成したタグをoutput.Content.AppendHtmlで追加します。
なお、output.GetChildContentAsyncは非同期のため、ProcessもProcessAsyncに書き換えます。

        /// <summary>
        /// タグ出力
        /// </summary>
        /// <param name="context">コンテキスト</param>
        /// <param name="output">出力</param>
        public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            ArgumentNullException.ThrowIfNull(For, nameof(For));
            ArgumentNullException.ThrowIfNull(HtmlAttributes, nameof(HtmlAttributes));
            ArgumentNullException.ThrowIfNull(ViewContext, nameof(ViewContext));


            // 普通のラベルタグを出力する
            TagBuilder labelTag = _Generator.GenerateLabel(ViewContext, For.ModelExplorer, For.Name, For.Metadata.DisplayName, null);

            // 普通のinputタグを出力する
            TagBuilder inputTag = _Generator.GenerateTextBox(ViewContext, For.ModelExplorer, For.Name, For.Model, "", HtmlAttributes);

            // もしくはタグを自分で生成して出力する
            TagBuilder hiddenTag = new TagBuilder("input");
            hiddenTag.MergeAttribute("type", "hidden");
            hiddenTag.MergeAttribute("name", For.Name + "_hidden");
            hiddenTag.MergeAttribute("value", For.Model?.ToString());

            // コンテントを取得する
            TagHelperContent content = await output.GetChildContentAsync();

            // 適当なタグ(ここではdiv)を作る
            TagBuilder divTag = new TagBuilder("div");
            // divタグにコンテントを設定する
            divTag.InnerHtml.AppendHtml(content);

            // divタグをもとのタグに追加する
            output.Content.AppendHtml(divTag);
        }

実行結果(抜粋)。<sample-text>とコンテント(あいうえお)の間に<div>の階層が挟まれているのがわかると思います。

    <sample-text>
        <div>
            あいうえお
        </div>
    </sample-text>

    <sample-text>
        <div>
            かきくけこ
        </div>
    </sample-text>

タグヘルパーに配列を渡したい

配列や変数をタグヘルパーの属性に指定する場合は@()で括って指定します。
まずはサンプルのタグヘルパーを以下のように修正します。
string[]? StrArray を追加しています。
なお、今回はAppendHtmlにタグではなく文字列を指定しています。

#nullable enable

using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace TestWebCore3.Helpers
{
    /// <summary>
    /// サンプルテキストタグヘルパー
    /// 通常のテキストタグを出力し
    /// 同じ値でHiddenタグを出力する
    /// テキストタグは「IHtmlGenerator」で生成しているため
    /// ModelStateの考慮やClient Side Validationの考慮がされている
    /// </summary>
    /// <remarks>
    /// HtmlTargetElementはカスタムタグヘルパーに紐づけるタグ名
    /// HtmlTargetElementを付与しない場合はデフォルト値として
    /// クラス名(からTagHelperをのぞいた)をケバブケースに変換したものが
    /// タグ名として使われる
    /// SampleTextTagHelperの場合はSampleTextの部分をケバブに変えたもの
    /// sample-textがデフォルトのタグ名になる
    /// 今はデフォルトと同じタグ名を明示的にHtmlTargetElementに指定している
    /// </remarks>
    [HtmlTargetElement("sample-text")]
    public class SampleTextTagHelper : TagHelper
    {
        /// <summary>
        /// コンテキスト
        /// TagHelperの場合[ViewContext]属性をつけておくとDIしてくれる
        /// [HtmlAttributeNotBound]は、タグの属性にview-contextというのがあった場合に
        /// 本プロパティにバインドさせないようにするために付与する
        /// </summary>
        [HtmlAttributeNotBound]
        [ViewContext]
        public ViewContext? ViewContext { get; set; }

        /// <summary>
        /// 慣例的にasp-for属性でタグに出力するモデルを受け取る
        /// </summary>
        [HtmlAttributeName("asp-for")]
        public ModelExpression? For { get; set; }

        /// <summary>
        /// Html属性(dynamic objectで受け取る)
        /// HtmlAttributeNameを指定しないとデフォルトの属性の名前は
        /// ケバブ(html-attributes)になる
        /// </summary>
        public object? HtmlAttributes { get; set; }

        /// <summary>
        /// 配列受け取りのサンプル
        /// </summary>
        public string[]? StrArray { get; set; }

        /// <summary>
        /// 一般的なタグヘルパーを出力するためのクラス
        /// </summary>
        private IHtmlGenerator _Generator;

        /// <summary>
        /// Model情報を取得するためのクラス
        /// 実は今回のサンプルでは使わない。
        /// </summary>
        private IModelMetadataProvider _ModelMetadataProvider;

        /// <summary>
        /// Client Side Validation用のAttribute出力クラス
        /// 初期段階では使わない
        /// </summary>
        private ValidationHtmlAttributeProvider _ValidationHtmlProvider;

        /// <summary>
        /// コンストラクター
        /// </summary>
        /// <param name="generator">標準のinputタグなどを出力するためのクラス</param>
        /// <param name="modelMetadataProvider">Model情報を取得するためのクラス</param>
        /// <param name="validationHtmlProvider">Client Side Validation用のクラス</param>
        public SampleTextTagHelper(IHtmlGenerator generator
            , IModelMetadataProvider modelMetadataProvider
            , ValidationHtmlAttributeProvider validationHtmlProvider)
        {
            _Generator = generator;
            _ModelMetadataProvider = modelMetadataProvider;
            _ValidationHtmlProvider = validationHtmlProvider;
        }

        /// <summary>
        /// タグ出力
        /// </summary>
        /// <param name="context">コンテキスト</param>
        /// <param name="output">出力</param>
        public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            ArgumentNullException.ThrowIfNull(For, nameof(For));
            ArgumentNullException.ThrowIfNull(HtmlAttributes, nameof(HtmlAttributes));
            ArgumentNullException.ThrowIfNull(ViewContext, nameof(ViewContext));


            // 普通のラベルタグを出力する
            TagBuilder labelTag = _Generator.GenerateLabel(ViewContext, For.ModelExplorer, For.Name, For.Metadata.DisplayName, null);

            // 普通のinputタグを出力する
            TagBuilder inputTag = _Generator.GenerateTextBox(ViewContext, For.ModelExplorer, For.Name, For.Model, "", HtmlAttributes);

            // もしくはタグを自分で生成して出力する
            TagBuilder hiddenTag = new TagBuilder("input");
            hiddenTag.MergeAttribute("type", "hidden");
            hiddenTag.MergeAttribute("name", For.Name + "_hidden");
            hiddenTag.MergeAttribute("value", For.Model?.ToString());

            // コンテントを取得する
            TagHelperContent content = await output.GetChildContentAsync();
            string contentStr = content.GetContent();

            // StrArrayを追加する
            if (StrArray != null)
            {
                foreach (string data in StrArray)
                {
                    output.Content.AppendHtml($"{contentStr} {data}<br>");
                }
            }
        }
    }
}

Viewは以下のようにして配列を渡します(抜粋)。

    <sample-text asp-for="TestId" str-array="@(new string[]{"リンス","シャンプ"})" html-attributes="@(new {tabindex=1, style="width:80px;"})">
        あいうえお
    </sample-text>

HTMLの出力は以下のようになります(抜粋)。

    <sample-text>
        あいうえお リンス<br>
        あいうえお シャンプ<br>
    </sample-text>

    <sample-text>
        かきくけこ
    </sample-text>