🌲

BlazorのRenderTreeBuilderを使用して動的に描画を行う

に公開

はじめに

Blazorについて勉強し始めて、「似たようなページがたくさんあるので、もっと抽象的にまとめたいなあ」と思ってた矢先、RenderTreeBuilderに出会いました。
もうなんでもアリ(誉め言葉)だな、と感じましたので、RenderTreeBuilderのご紹介です。

RenderTreeBuilderとは

RenderTreeBuilderは、BlazorでUIコンポーネントをレンダリングするために使用されるクラスで、コンポーネントのHTMLを動的に構築する際に利用されます。具体的には、RenderTreeBuilderを使って、コンポーネントがレンダリングする要素(タグやコンテンツ)を「ツリー」形式で構築します。
Blazorでは、コンポーネントのUIは、通常、Renderメソッド内でRenderTreeBuilderを使って構築されます。RenderTreeBuilderには、以下のような主なメソッドがあります:

  • OpenElement: 新しいHTML要素を開く
  • AddContent: 要素にテキストコンテンツを追加する
  • CloseElement: 要素を閉じる
  • AddAttribute: 要素に属性を追加する
  • AddComponentParameter: コンポーネントのParameter属性変数に値を渡す

(AI検索より引用)

要するに、C#側でRazor構文を組み立てれますよ、という認識です。

環境

  • .NET 8
  • Blazor WASM

テストページ準備

以下のようにTestPage2とComponentTest2 , ComponentTest3を追加したBlazorサンプルプロジェクトがあるとします。
TestPage2はNavMenuに追加表示し、TestPage2を表示するとします。

TestPage2内では@rfを表示しているとします。

TestPage2

TestPage2.razor
@*TestPage2.razor*@
@page "/mytest2"
<PageTitle>MyTest2</PageTitle>
<span>
    @rf
</span>
TestPage2.razor.cs
// TestPage2.razor.cs
public partial class TestPage2
{
    private RenderFragment? rf;

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
    }
}

次項以降でRenderFragment? rfを編集し、ページ内の表示を制御します。

動作確認

基本的な使い方

ComponentTest2をTestPage2からRenderTreeBuilderを使用して表示を行います。

以下のようにComponentTest2を用意します。ComponentTest2にはParameter属性のMsgとKVが存在し、ComponentTest2内で表示するとします。

ComponentTest2

ComponentTest2.razor
@*ComponentTest2.razor*@
<h3>ComponentTest2</h3>  
@*メッセージの変数を表示*@
<span>@Msg</span>

@*KeyValueペアを列挙*@
<ul>  
   @foreach (var o in KV)  
   {  
       <li>@o.Key: @o.Value</li>  
   }  
</ul>  
ComponentTest2.razor.cs
// ComponentTest2.razor.cs
public partial class ComponentTest2
{
    [Parameter]
    public string? Msg { get; set; }

    [Parameter]
    public Dictionary<string, object> KV { get; set; } = [];
}

TestPage2にて、RenderTreeBuilder.AddComponentParameterメソッドでComponentTest2の変数に値をセットします。

TestPage2

TestPage2.razor
@*TestPage2.razor*@
@page "/mytest2"
<PageTitle>MyTest2</PageTitle>
<span>
    @rf
</span>
TestPage2.razor.cs
// TestPage2.razor.cs
public partial class TestPage2
{
    private RenderFragment? rf;

    protected override async Task OnInitializedAsync()
    {
        // RenderTreeBuilderを使用して、コンポーネントを動的に生成する
        rf = new RenderFragment(builder =>
        {
            int seq = 0;//コード行の番号
            builder.OpenComponent<ComponentTest2>(seq++);
            // Parameter属性の変数Msgに値をセットする
            builder.AddComponentParameter(seq++, "Msg", "RenderTreeBuilder Test !");
            // Parameter属性の変数KVに値をセットする
            builder.AddComponentParameter(seq++, "KV", new Dictionary<string, object>
                {
                    { "Key1", "TestPage2Value1" },
                    { "Key2", "TestPage2Value1" },
                    { "Key3", "TestPage2Value1" },
                    { "Key4", "TestPage2Value1" }
                });
            builder.CloseComponent();
        });
        await base.OnInitializedAsync();
    }
}

実行結果:
RenderTreeBuilderで編集を行ったRenderFragmentが、TestPage2上のrfに表示されました。

応用的な使い方

ComponentTest2 , ComponentTest3をTestPage2からRenderTreeBuilderを使用して表示を行います。

以下のようにComponentTest2にRenderFragment? rf_comp2を準備し、さらにComponentTest2内でrf_comp2を表示するとします。

ComponentTest2

ComponentTest2.razor
@*ComponentTest2.razor*@
<h3>ComponentTest2</h3>  
@*メッセージの変数を表示*@
<span>@Msg</span>

@*KeyValueペアを列挙*@
<ul>  
   @foreach (var o in KV)  
   {  
       <li>@o.Key: @o.Value</li>  
   }  
</ul>  

@*パラメータで渡されたComponentを表示*@
<span>@rf_comp2</span>
ComponentTest2.razor.cs
// ComponentTest2.razor.cs
public partial class ComponentTest2
{
    [Parameter]
    public string? Msg { get; set; }

    [Parameter]
    public Dictionary<string, object> KV { get; set; } = [];

    [Parameter]
    public RenderFragment? rf_comp2 { get; set; }
}

さらにComponentTest3を準備します。ComponentTest3はMsgというParameter属性の変数を表示するのみとします。

ComponentTest3

ComponentTest3.razor
@*ComponentTest3.razor*@
<h3>ComponentTest3</h3>  
@*メッセージの変数を表示*@
<span>@Msg</span>
ComponentTest3.razor.cs
// ComponentTest3.razor.cs
public partial class ComponentTest3
{
    [Parameter]
    public string? Msg { get; set; }
}

TestPage2にて、RenderTreeBuilder.AddComponentParameterメソッドでComponentTest2の変数に値をセットします。
このときに、ComponentTest2のrf_comp2変数にRenderTreeBuilderを使用して、ComponentTest3もセットします。

TestPage2

TestPage2.razor
@*TestPage2.razor*@
@page "/mytest2"
<PageTitle>MyTest2</PageTitle>
<span>
    @rf
</span>
TestPage2.razor.cs
// TestPage2.razor.cs
public partial class TestPage2
{
    private RenderFragment? rf;

    protected override async Task OnInitializedAsync()
    {
        // RenderTreeBuilderを使用して、コンポーネントを動的に生成する
        rf = new RenderFragment(builder =>
        {
            int seq = 0;//コード行の番号
            builder.OpenComponent<ComponentTest2>(seq++);
            // Parameter属性の変数Msgに値をセットする
            builder.AddComponentParameter(seq++, "Msg", "RenderTreeBuilder Test !");
            // Parameter属性の変数KVに値をセットする
            builder.AddComponentParameter(seq++, "KV", new Dictionary<string, object>
                {
                    { "Key1", "TestPage2Value1" },
                    { "Key2", "TestPage2Value1" },
                    { "Key3", "TestPage2Value1" },
                    { "Key4", "TestPage2Value1" }
                });
            // RenderTreeBuilderを使用して、rf_comp2用のRenderFragmentコンポーネントを動的に生成する
            builder.AddAttribute(seq++, "rf_comp2", new RenderFragment(builder2 =>
            {
                int seq2 = 0;//コード行の番号
                builder2.OpenComponent<ComponentTest3>(seq2++);
                // Parameter属性の変数Msgに値をセットする
                builder2.AddComponentParameter(seq2++, "Msg", "RenderTreeBuilder Test2 !");
                builder2.CloseComponent();
            }));
            builder.CloseComponent();
        });
        await base.OnInitializedAsync();
    }
}

実行結果:
RenderTreeBuilderで編集を行ったRenderFragmentが、TestPage2上のrfに表示されました。ComponentTest2上のrf_comp2もComponentTest3が指定され、表示されてます。

利用ケース

上記にも記載しましたが、1ページ内にコンポーネントの入れ子の入れ子、、とする場合、かつ最上位コンポーネントで下位を制御したい場合に私はよく使用します。

まとめ

Blazorでの簡単なRenderTreeBuilderの動きを確認しました。
Razor構文で記載することが正攻法ですが、ページの内の一部をより動的に表現したい場合などに便利です。
何かのお役に立てれば幸いです。

参考

  • Microsoft Learn ASP.NET Core Blazor テンプレート コンポーネント

https://learn.microsoft.com/ja-jp/aspnet/core/blazor/components/templated-components?view=aspnetcore-9.0

  • Microsoft Learn ASP.NET Core Blazor の高度なシナリオ (レンダー ツリーの構築)

https://learn.microsoft.com/ja-jp/aspnet/core/blazor/advanced-scenarios?view=aspnetcore-9.0

  • Qiita パラメータで指定した HTML タグ要素をレンダリングする Razor コンポーネントを書く

https://qiita.com/jsakamoto/items/bd5e1541f0524de40183

  • Blazor UIコンポーネントのタグ内に任意のHTMLを描画させる

https://tex2e.github.io/blog/dotnet/Blazor-RenderFragment

Discussion