ModalParitalTagHelperを作ってみた - ASP.NET Core MVC

BootstrapのModalを使っていると、ほとんど同じマークアップで一部だけを変更したいことがあると思います。

あるあるな要件かなと思いますが、

  • サイト内で使うモーダルはだいたい同じHTMLで統一したい(class属性も揃えたい)
  • モーダルのタイトルや本文はそれぞれで設定したい
  • さらにモーダル本文はテキストではなくHTMLを指定したい

といったことを解決するModalParitalTagHelperを作ってみましたというお話です。

ModalではなくCardでもToastでも何でもいいですしBootstrapに限った話でもないですが、とありえずBootstrapのModalで話を進めます。

モーダルの部分ビュー

まずモーダルのHTMLを部分ビュー(_Modal.cshtml)として用意しました。 モーダルのid属性、タイトル、本文はビューモデルのプロパティを使ってレンダリングできるようにしています。

@model ModalPartialViewModel
<div id="@Model.Id" class="modal" tabindex="-1">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                @* モーダルのタイトルはテキストで *@
                <h5 class="modal-title">@Model.Title</h5>
                <button type="button" class="close" data-dismiss="modal">
                    <span>&times;</span>
                </button>
            </div>
            <div class="modal-body">
                @* モーダルの本文はHTMLで *@
                @Model.Body
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                <button type="button" class="btn btn-primary">Save changes</button>
            </div>
        </div>
    </div>
</div>

実際にはボタンなど他にも変更したい部分があるとは思いますが、サンプルなので設定できるのはとりあえずこの3箇所で。

ビューモデル

次は部分ビューのビューモデル。 Titleプロパティは単なる文字列ですが、BodyプロパティはHTMLを設定するのでエンコードされないようにIHtmlContentです。

// _Modal.cshtmlのビューモデル
public class ModalPartialViewModel {
    // id属性
    public string Id { get; set; }
    // モーダルのタイトル
    public string Title { get; set; }
    // モーダルの本文(HTML)
    public IHtmlContent Body { get; set; }
}
PartialTagHelperを使った場合

PartialTagHelperをそのまま使っても部分ビューをレンダリングできます。

@{
    var model = new ModalPartialViewModel {
        Id = "sample-modal",
        Title = "Modal title",
        Body = new HtmlString("<p>Modal body text goes here.</p>"),
    };
    <partial name="_Modal" model="model" />
}

<!-- 生成されるHTML -->
<div id="sample-modal" class="modal" tabindex="-1">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Modal title</h5>
                <button type="button" class="close" data-dismiss="modal">
                    <span>&times;</span>
                </button>
            </div>
            <div class="modal-body">
                <p>Modal body text goes here.</p>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                <button type="button" class="btn btn-primary">Save changes</button>
            </div>
        </div>
    </div>
</div>

ただBodyプロパティにモーダル本文を設定している部分がちょっといまいちかなと思います。 インテリセンスは効きませんし、コンテンツが多いモーダルだと長い文字列になってメンテナンスしにくいでしょう。 HTMLを文字列で指定することはできれば避けたいところです。

ModalPartialTagHelper

次のコードでモーダルの部分ビューをレンダリングできると良さげかなと思います。 モーダル本文はエディタによる補完機能を使いつつHTMLとして埋め込むと。

<modal-partial id="sample-modal" title="Modal title">
    @* モーダルの本文はここに埋め込みたい *@
    <p>Modal body text goes here.</p>
</modal-partial>

ということで前置きが長くなりましたが、今回作ってみたのがこの<modal-partial></modal-partial>のModalPartialTagHelperです。

// モーダルの部分ビュータグヘルパー
public class ModalPartialTagHelper : TagHelper {
    // ビューモデル
    private readonly ModalPartialViewModel _model = new ModalPartialViewModel();
    // 部分ビュータグヘルパー
    private readonly PartialTagHelper _inner;

    public ModalPartialTagHelper(ICompositeViewEngine viewEngine, IViewBufferScope viewBufferScope) {
        _inner = new PartialTagHelper(viewEngine, viewBufferScope) {
            Name = "_Modal",
            Model = _model,
        };
    }

    // PartialTagHelperにViewContextが必要みたい(これがないとNullReferenceException)
    [HtmlAttributeNotBound]
    [ViewContext]
    public ViewContext ViewContext {
        get => _inner.ViewContext;
        set => _inner.ViewContext = value;
    }

    // モーダルのID
    public string Id {
        get => _model.Id;
        set => _model.Id = value;
    }

    // モーダルのタイトル
    public string Title {
        get => _model.Title;
        set => _model.Title = value;
    }

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) {
        // 子コンテンツをモーダルのボディ用HTMLとする
        _model.Body = await output.GetChildContentAsync();

        await _inner.ProcessAsync(context, output);
    }
}

使わせたくない属性(プロパティ)を公開しないために、PartialTagHelperを継承しないでフィールドで持つことにしました。 ICompositeViewEngine、IViewBufferScope、ViewContextAttributeはまだよく分からないのですがPartialTagHelperのソースを参考にしています。

今回作ったModalPartialTagHelperはHTMLを複数箇所指定できませんが、1箇所だけでもそこそこ使える場面はあるんじゃないかなと思います。