5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

BlazorAdvent Calendar 2022

Day 24

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

Posted at

シナリオ

CSS の flex レイアウトを使用して、縦積み・横積みのレイアウトを簡単に組み上げられるよう、Stack という Razor コンポーネントを作ることにしました。ソースコードはこんな感じ。

Stack.razor
<div style="display:flex; flex-direction: @(this.Direction.ToString().ToLower());">
    @ChildContent
</div>

@code {
    [Parameter]
    public StackDirection Direction { get; set; } = StackDirection.Row;

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

<div> 要素に display:flex を指定し、その子要素をレンダリングするだけのシンプルな実装です。この Stack コンポーネントを使えば、下記のようにマークアップすることで...

App.razor
<Stack Direction="StackDirection.Column">
    <button>Element 1</button>
    <button>Element 2</button>
</Stack>

こんな感じで容易に flex レイアウトによる縦積みが組めます。イイ感じですね。
image.png

div じゃなくて任意の要素を指定できるようにしたい

ところが、この Stack コンポーネントを広く配布したところ、あちこちから次のような要望が寄せられ始めました。

<div> で囲うのではなくて、任意の要素、例えば <header> とか <main> とかでも囲えるようにしてほしい」

つまりは下記のとおりです。

Stack.razor
//👇 この "div" の部分を、パラメータで指定された任意の文字列で出力したい!
<div style="display:flex; flex-direction: @(this.Direction.ToString().ToLower());">
    ...

例えば、この Stack コンポーネントに [Parameter] string Component {get; set;} というパラメータープロパティを追加して、使う側で以下のようにマークアップしたら、

*.razor
                         <!-- ここで "header" を指定 👇 -->
<Stack Direction="StackDirection.Column" Component="header">
    ...
</Stack>

以下の HTML が出力されるようにしたい、っていうことです。

レンダリングされたHTML
<!-- 👇 "Component" プロパティで指定した HTML タグ ("header") でレンダリングされている! -->
<header style="display:flex; flex-direction: column;">
    ...
</header>

しかし、そんなことできるのでしょうか??

.razor ファイルは C# のソースコードに変換されている

上記要望を叶えるには、実は .razor ファイルは C# のソースコードに変換されている、ということを知っておくとよいかもしれません。.razor ファイルから変換された C# ソースコードを確認するには、いくつかやり方があるようですが、例えば Blaozr アプリケーションのプロジェクトファイル (*.csproj) をエディタで編集して、以下の設定を書き足しておいてから、その Blazor アプリケーションを再ビルトする、という方法があります。

*.csproj
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
  <PropertyGroup>
    ...
    <!-- 👇この EmitCompilerGeneratedFiles = true の指定を追加する -->
    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
    ...

そうすると、"obj" フォルダ以下、"generated" という名前のサブフォルダ配下に、変換後の C# ソースコードがファイルとして出力されます (通常はファイルとして出力せずに、コンパイラの内部で生成されるのみ)。
image.png
上図がそうやってファイルに保存するようにした、.razor ファイルから変換されたあとの C# ソースコードの様子です。変換元の .razor ファイルの名前をもとに、 "~_razor.g.cs" を末尾に付け足した命名則で C# ソースコードファイルとして保存されています。この変換後の C# ソースコードを開いてみましょう。
image.png
自動生成された C# コードなので、型名が global:: から始まる完全限定名で指定されていたり、各種プラグマによるコンパイラ制御が多数記載されていたり、なかなかに読みにくいですね。それを我慢して忍耐強くコードの構成を読んでみると、以下のことが見えてきます。

  • ComponentBase クラスから派生していること
  • ComponentBase クラスは BuildRenderTree という仮想メソッドを持っていること
  • この BuildRenderTree 仮想メソッドをオーバーライドして、その中で DOM を構築しているらしいこと

とくに BuildRenderTree 仮想メソッドオーバーライドのコードを整理して書き直してみると、以下のようになっています。

protected override void BuildRenderTree(RenderTreeBuilder builder)
{
    builder.OpenElement(0, "div");
    builder.AddAttribute(1, "style", "display:flex;...);
    builder.AddContent(2, ChildContent);
    builder.CloseElement();
}

すると、
RenderTreeBuilderOpenElement メソッドで、指定したタグの HTML タグを開いて...」
AddAttribute メソッドで属性追加して...」
AddContent メソッドで子要素を書いて...」
CloseElement メソッドでタグを閉じる」
という構造になっているのがわかりますね。

どうでしょう、ここまでわかると、.razor ファイルを使わずに、直接 C# コードのみで Razor コンポーネントを実装できそうな気がしてきませんか? そして直接 C# コードのみで Razor コンポーネントを実装するのなら、「RenderTreeBuilderOpenElement メソッドで、指定したタグの HTML タグを開いて...」の部分で、レンダリングする HTML タグを任意の文字列で指定できますよね?

やってみた

ということで、Stack.razor は削除し、改めて Stack.cs として、C# コードのみで Stack Razor コンポーネントを実装しなおしてみました。幸いにして Stack コンポーネントの構造は至極シンプルなので、以下に全文掲載できます。

Stack.cs
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;

public class Stack2 : ComponentBase
{
    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    [Parameter]
    public StackDirection Direction { get; set; } = StackDirection.Row;

    // この Stack コンポーネントがレンダリングする HTML タグを指定できるパラメータープロパティを追加。
    // 指定が省略された場合の既定値は "div" とする。
    [Parameter]
    public string? Component { get; set; } = "div";

    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        if (string.IsNullOrEmpty(Component)) throw new ArgumentNullException(nameof(Component));

        // "OpenElement" メソッドで HTML タグを開く際に、"Component" パラメータープロパティに
        // 設定された文字列を指定する
        builder.OpenElement(0, this.Component);
        builder.AddAttribute(1, "style", $"display:flex; flex-direction:{this.Direction.ToString().ToLower()};");
        builder.AddContent(2, this.ChildContent);
        builder.CloseElement();
    }
}

その上で、改めてこの新生 Stack コンポーネントを使ってみます。

App.razor
                         <!-- ここで "header" を指定 👇 -->
<Stack Direction="StackDirection.Column" Component="header">
    <button>Element 1</button>
    <button>Element 2</button>
</Stack>

すると、おめでとうございます、無事、Component パラメータープロパティに指定した "header" 要素でレンダリングされることが確認できました!

image.png

まとめ

Razor コンポーネントはつまるところは ComponentBase クラスから派生したただのクラスに過ぎず、BuildRenderTree 仮想メソッドのオーバーライドにて、DOM 構造を構築する処理を実装しているのでした。そのため、.razor ではなく直接 C# コードのみで Razor コンポーネントを実装できることがわかりました。もちろん大抵の場合は、Razor 構文を使って .razor ファイルとして Razor コンポーネントを実装するほうがラクに実装できるはずです。いっぽうで .razor ファイルを使わずに C# コードで直接記述することにより、Razor 構文では実現困難なコンポーネントを実装できる能力が手に入ります。一般的なアプリケーションのレイヤでは出番はないかもしれませんが、ユーザーインターフェース部品的なコンポーネント実装では、C# コードで直接 Razor コンポーネントを実装する技法が役に立つことがあるかもしれませんね。

Happy Coding :)

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?