NDW

アプリ開発やトラブルシューティング等のノウハウ、キャンプや登山の紹介や体験談など。

1. システムエンジニアリング ASP.NET Core 実装技術

ASP.NET Core: タグヘルパーでのHTML編集方法

投稿日:

ASP.NET Coreで独自のタグを生成するためにTagHelperを使用します。
TagHelperでどのようにHTMLを生成できるかを説明します。

概要

基本的なHTML編集の仕組み

  • タグヘルパーでは、Process()メソッドの引数output(TagHelperOutput)のメソッドやプロパティを操作してHTMLを編集できます。
    HTMLを任意に編集するためには、TagHelperOutputのPreElement, PostElement, PreContent, Content, PostContentプロパティ(TagHelperContent型)を理解することが重要です。
    ここではまず、これらのプロパティを操作するタグヘルパーのサンプルを説明します。
    [HtmlTargetElement("div", Attributes = "basic")]
    public class BasicTagHelper : TagHelper
    {
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.PreElement.SetHtmlContent("<div class=\"pre-element\"></div>");
    
            output.PreContent.SetHtmlContent("<div class=\"pre-content\"></div>");
            output.PostContent.SetHtmlContent("<div class=\"post-content\"></div>");
    
            output.PostElement.SetHtmlContent("<div class=\"post-element\"></div>");
        }
    }
    

    このタグヘルパーの入力となるHTMLと、タグヘルパーが出力するHTMLを次に示します。
    前述の各プロパティを操作することで、入力要素の前後への要素の追加や、入力要素に子要素を追加することができます。
    (出力例に関して、入力要素に対応する行はマークしています。)

    <div example class="test">
    コンテンツ
    </div>
    
    <div class="pre-element"></div>
    <div example="" class="test">
        <div class="pre-content"></div>
        コンテンツ
        <div class="post-content"></div>
    </div>
    <div class="post-element"></div>
    
  • タグヘルパーの入出力とTagHelperOutputの関係は次の通りです。
    • ここでは説明の便宜上、タグヘルパーの処理対象となるHTML要素を「入力要素」と表記します。
      タグヘルパーで出力するHTML要素に関して、入力要素に対応して既定で作成されるHTML要素を「メイン出力要素」、それ以外の要素を「その他出力要素」と表記します。
    • メイン出力要素の前後にdiv, span等の任意のHTML要素を挿入したい場合、Process()メソッドの引数output(TagHelperOutput)のPreElement, output.PostElementプロパティ(TagHelperContent)を使用します。
    • メイン出力要素の子要素としてHTML要素を挿入したい場合、同様に引数outputのPreContent, output.Content, output.PostContentプロパティ(TagHelperContent)を使用します。
      なお、タグヘルパーの対象がinput等の開始タグのみの要素(値を持たない要素)の場合、これらのプロパティを設定しても無視されます。
  • サンプルコードでは文字列でHTML要素を指定していますが、TagBuilderクラスを使用してHTMLを編集する方法があります。
    この方法は、煩雑な文字列操作は不要かつオブジェクト操作のみでHTMLを編集(Object-HTMLマッパー?)できるので、通常はTagBuilderを使用する方法がおすすめです。
    [HtmlTargetElement("div", Attributes = "tag-builder-example")]
    public class TagBuilderExampleTagHelper: TagHelper
    {
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            var preElementDiv = new TagBuilder("div");
            preElementDiv.AddCssClass("pre-element");
            output.PreElement.SetHtmlContent(preElementDiv);
    
            var preContentDiv = new TagBuilder("div");
            preContentDiv.AddCssClass("pre-content");
            output.PreContent.SetHtmlContent(preContentDiv);
    
            var postContentDiv = new TagBuilder("div");
            postContentDiv.AddCssClass("post-content");
            output.PostContent.SetHtmlContent(postContentDiv);
    
            var postElementDiv = new TagBuilder("div");
            postElementDiv.AddCssClass("post-element");
            output.PostElement.SetHtmlContent(postElementDiv);
        }
    }
    

TagHelperOutputを使ったHTML編集

  • TagHelperOutputを使用してメイン出力要素を編集する方法を説明します。
  • メイン出力要素以外を編集する場合、PreElement, PostElement, PreContent, Content, PostContentプロパティを使用する必要があります。詳細は後述の「TagHelperContentを使ったHTML編集」でご覧ください。
  • メイン出力要素の要素名や属性を編集するサンプルを次に示します。
    なお、出力例はブラウザのページソースを記載しており、見やすいよう表示順番を変更しています。(開発者モードF12だと実ソースとの違いが大きいため。)
    <div output-example
        class="form-control delete-class" 
        delete-attr="delete"
        set-attr4="old4"
        add-attr4="old4"
        copy-attr4="old4">
    </div>
    
    <span output-example="" 
        class="form-control add-class copy-class"
        set-attr1=""  set-attr2=""  set-attr3="val3"  set-attr4="newval4" set-attr5
        add-attr1=""  add-attr2=""  add-attr3="val3"  add-attr4="old4"    add-attr5
        copy-attr1="" copy-attr2="" copy-attr3="val3" copy-attr4="old4">
    </span>
    
    [HtmlTargetElement("div", Attributes = "tag-helper-output-example")]
    public class TagHelperOutputExampleTagHelper: TagHelper
    {
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            // 出力するタグ名をdivからspanに変更
            output.TagName = "span";
    
            // class属性の編集
            output.AddClass("add-class", HtmlEncoder.Default);
            output.RemoveClass("delete-class", HtmlEncoder.Default);
    
            // 属性全般の編集
            var attrs = output.Attributes;
    
            // 属性の削除
            if (attrs.ContainsName("delete-attr"))
                attrs.Remove(attrs["delete-attr"]);
    
            // 属性の設定(上書き)
            attrs.SetAttribute("set-attr1", null);      // 結果: 空値("")
            attrs.SetAttribute("set-attr2", "");        // 結果: 空値("")
            attrs.SetAttribute("set-attr3", "val3");
            attrs.SetAttribute("set-attr4", "newval4"); // 結果: 既存属性の上書き
            attrs.SetAttribute(
                new TagHelperAttribute("set-attr5"));   // 属性名のみ出力
    
            // 属性の追加
            attrs.Add("add-attr1", null);             // 結果: 空値("")
            attrs.Add("add-attr2", "");               // 結果: 空値("")
            attrs.Add("add-attr3", "val3");
            attrs.Add("add-attr4", "newval4");        // 結果: 同名で追加→無視
            attrs.Add(
                new TagHelperAttribute("add-attr5")); // 属性名のみ出力
    
            // 既存のタグから属性のコピー
            var copy = new TagBuilder("div");
            copy.AddCssClass("copy-class");
            copy.Attributes.Add("copy-attr1", null);      // 結果: 空値("")
            copy.Attributes.Add("copy-attr2", "");        // 結果: 空値("")
            copy.Attributes.Add("copy-attr3", "val3");
            copy.Attributes.Add("copy-attr4", "newval4"); // 結果: 無視(反映されない)
            output.MergeAttributes(copy); // TagHelperOutputExtensionsのusing宣言が必要
        }
    }
    
  • TagHelperOutputのTagMode(TagMode列挙体)を指定して、メイン出力要素の開始タグ・終了タグの出力有無を変更できます。
    TagMode値 説明 備考
    SelfClosing 自己完結するタグを出力 “<div />”, “<br />”等
    StartTagAndEndTag 開始タグ・終了タグを出力 “<div></div>”等
    StartTagOnly 開始タグのみ出力 “<input>”等

    TagHelperのTagModeの既定値はStartTagAndEndTagになっており、開始タグ・終了タグが出力されます。input要素を扱うInputTagHelperのTagModeの既定値はStartTagOnlyになっており終了タグを出力しません。(HTML仕様上、input要素は終了タグが不要であるため、このような既定値になっていると思われます。)

  • なお、TagHelperOutputの属性値(output.Attributes)は、入力検証等のレンダリング処理過程でページソース(cshtml)と差異が発生する場合があります。例えば、サーバ側入力検証が失敗した場合は”input-validation-error”等のCSSクラスが追加される場合があります。そのため、cshtmlで定義した属性のみある前提の実装だと不具合が発生する可能性があります。
  • output.Attributesに含まれるclass属性の値は通常は文字列なのですが、サーバ側入力検証失敗時はClassAttributeHtmlContentオブジェクトが設定されます。この属性(オブジェクト)を別のタグ等にマージやコピーを試みると、class属性の値が”ClassAttributeHtmlContent”のようなクラス名になってしまいます。これは、ClassAttributeHtmlContentがTagHelperOutputExtensionsクラスのprivateクラスであることに起因しているようです。デバッガで当該オブジェクト内部でcssクラス名が保持されていることは確認できるのですが、その値を取得できません。
    回避策として、リフレクションで無理やり内部にアクセスする方法もありますが、context(TagHelperContext)のAllAttributesプロパティに含まれるclass属性値(cshtmlで定義した値)を使用する方法もあります。

TagHelperContentを使ったHTML編集

  • メイン出力要素の前後のHTML要素や子のHTML要素を編集する場合、TagHelperOutputのPreElement, PostElement, PreContent, Content, PostContentプロパティを使用します。
  • これらのプロパティは全てTagHelperContent型なので、ここではTagHhelperContentを使用したHTML編集方法を説明します。
  • TagHelperContentの基本的な操作例を次に示します。
    TagHelerContentにHTML要素を設定(上書き)する場合はSet系メソッド、追加する場合はAppend系メソッドを使用します。”Html”が付かないメソッドは、引数の文字列をHTMLエンコードして設定・追加します。
    // HTML要素の設定(上書き)
    output.Content.SetContent("<span>あ</span>");     // "&lt;span&gt;あ&lt;/span&gt;"
    output.Content.SetHtmlContent("<span>あ</span>"); // "<span>あ</span>"
    var tag = new TagBuilder("span");
    output.Content.SetHtmlContent(tag);               // "<span></span>"
    
    // HTML要素の追加
    output.Content.Append("<span>あ</span>");     // "&lt;span&gt;あ&lt;/span&gt;"
    output.Content.AppendHtml("<span>あ</span>"); // "<span>あ</span>"
    var tag = new TagBuilder("span");
    output.Content.AppendHtml(tag);               // "<span></span>"
    
    • SetContent(), Append()等のようにメソッド名にHtmlがないメソッドは、引数の文字列は「HTMLエンコードされていない文字列」とみなし、HTMLエンコードしてから設定・追加します。ユーザやDBからのデータを使用する場合など、”< >”等のようにHTMLで特別な意味を持つ文字が含まれる可能性がある場合に使用します。(画面崩れの防止やXSS対策のため。)
    • SetHtmlContent(), AppendHtml()等のようにHtmlがあるメソッドは、引数の文字列が「HTMLエンコード済みである」とみなし、HTMLエンコードは行わずにそのまま設定・追加します。プログラマが明示的にタグを編集する(ユーザやDBからの入力等に依存しない)場合は、前述のような危険性はないので、こちらを使用します。
  • TagBuilderの基本的な操作例を次に示します。
    TagBuilderの子としてHTML要素を設定(上書き)する場合はInnerHtmlプロパティに対してSet系メソッド、追加する場合はAppend系メソッドを使用します。”Html”が付かないメソッドは、引数の文字列をHTMLエンコードして設定・追加します。
    // 子とするspan要素を作成
    var childSpan= new TagBuilder("span");
    childSpan.AddCssClass("child-class");
    childSpan.Attributes.Add("attr1", "value1"); // 属性追加
    childSpan.MergeAttribute("attr2", "value2"); // 存在しない場合は属性追加
    childSpan.MergeAttribute("attr3", "value3", true); // 属性上書き
    
    // 親となるdiv要素を作成して子要素を追加
    var parentDiv = new TagBuilder("div");
    parentDiv.AddCssClass("parent-class");
    parentDiv.TagRenderMode = TagRenderMode.StartTag; // 終了タグなし
    parentDiv.InnerHtml.SetHtmlContent(childSpan); // 子要素を設定
    
    // 作成した要素群を設定
    output.Content.SetHtmlContent(parentDiv);
    
    
    • InnerHtmlプロパティのSetContent(), SetHtmlContent()は拡張メソッドのため、”Microsoft.AspNetCore.Html”のusing宣言が必要です。
    • 属性操作に関して、TagHelperOutputと微妙に異なるので混乱しないよう注意が必要です。(TagHelperOutputは文字列またはTagHelperAttributeによる属性操作が可能ですが、TagHelperContentは文字列のみになります。)

メイン出力要素に子要素を追加するサンプル

メイン出力要素に子のHTML要素を追加するタグヘルパーの入出力とサンプルコードを説明します。
Contentに対して子となるHTML要素を追加することで実現します。

<div tag-helper-content-example class="form-control">
</div>
<div tag-helper-content-example class="form-control">
    <div class="div-class1" div-attr1="" div-attr2="" div-attr3="val3"></div>
    <div class="div-class2">
        <span class="span-class1"></span>
        <br />
    </div>
</div>
[HtmlTargetElement("div", Attributes = "tag-helper-content-example")]
public class TagHelperContentExampleTagHelper : TagHelper
{
    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        // 例題としてoutput.Content(TagHelperContent)を編集
        var content = output.Content;

        // コンテンツをdivで上書き
        var topDiv1 = new TagBuilder("div");
        topDiv1.AddCssClass("div-class1");
        topDiv1.Attributes.Add("div-attr1", null);
        topDiv1.Attributes.Add("div-attr2", "");
        topDiv1.Attributes.Add("div-attr3", "val3");
        content.SetHtmlContent(topDiv1);

        // HTMLソースコード上で改行を入れたい場合
        // (HtmlContentBuilderExtensionsのusing宣言が必要)
        content.AppendLine();

        // コンテンツにdivを追加(子としてspan, brを持つ)
        var topDiv2 = new TagBuilder("div");
        topDiv2.AddCssClass("div-class2");
        // 子要素spanを生成
        var childSpan = new TagBuilder("span");
        childSpan.AddCssClass("span-class1");
        topDiv2.InnerHtml.AppendHtml(childSpan);
        // 子要素brを生成
        var childBr = new TagBuilder("br");
        childBr.TagRenderMode = TagRenderMode.SelfClosing; // <br />
        topDiv2.InnerHtml.AppendHtml(childBr);
        // コンテンツにdivを追加
        content.AppendHtml(topDiv2);
    }
}

メイン出力要素を子要素にするサンプル

メイン出力要素を子要素(ネスト)にするタグヘルパーの入出力とサンプルコードを説明します。
PreElementには終了タグがない要素を追加し、PostElementに終了タグのみを出力することでネストを実現しています。

<div tag-helper-nested-example class="form-control"></div>
<div class="start-class">
    <div class="start-sub-div"></div>
    <div tag-helper-nested-example class="form-control"></div>
    <div class="end-sub-div"></div>
</div>
[HtmlTargetElement("div", Attributes = "tag-helper-nested-example")]
public class TagHelperNestedExampleTagHelper : TagHelper
{
    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        // div開始タグを追加
        var startDiv = new TagBuilder("div");
        startDiv.AddCssClass("start-class");
        startDiv.TagRenderMode = TagRenderMode.StartTag;
        output.PreElement.AppendHtml(startDiv);

        // 子要素divを追加(メイン出力要素の前に追加)
        var startSubDiv = new TagBuilder("div");
        startSubDiv.AddCssClass("start-sub-div");
        output.PreElement.AppendHtml(startSubDiv);

        // 子要素divを追加(メイン出力要素の後に追加)
        var endSubDiv = new TagBuilder("div");
        endSubDiv.AddCssClass("end-sub-div");
        output.PostElement.AppendHtml(endSubDiv);

        // div終了タグを追加
        var endDiv = new TagBuilder("div");
        endDiv.TagRenderMode = TagRenderMode.EndTag;
        output.PostElement.AppendHtml(endDiv);
    }
}







-1. システムエンジニアリング, ASP.NET Core, 実装技術

関連記事

CentOS7のマルチホーム化

サイトの存在を隠しつつも、sftpサーバを公開し、後輩と1G以上のファイルのやりとりしたい。 パブリック側のIPアドレスを教えてしまうと、どこのサーバだろうかとブラウザで開いたりするとサイトの存在がわ …

システム開発でのmybatis-generatorの利用

システム開発における製造工程の前段では、開発メンバの負荷軽減や共通化のために、各テーブルに対するSELECT/INSERT/UPDATE/DELETEを容易に行うための共通クラスを準備することが望まれ …

ftp, ftps, sftpの違い

開発対象システムの連携先システムとして、ftpsやらftpsサーバが指定される場合がある。 私の場合、開発標準の役割を担う場合が多く、これらの仕様を把握し、動作確認や単体テスト用のダミーのサーバを用意 …

ASP.NET Core: 変更ページを実行環境に反映

ASP.Net Core(3.0)の開発で、ページ(cshtml)を編集しながらページデザインを確認したい。 既定ではページを変更しても実行環境に反映れずサーバの再起動が必要となり開発効率が悪い。 サ …

.NET Core: Azure AD B2Cユーザアカウント操作のC#サンプル

概要 管理者がAzure AD B2Cのユーザアカウントの管理が行えるASP.NET Coreのアプリの開発を想定している。Azure AD B2Cユーザアカウントの作成や更新等の操作はMicroso …