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

## シナリオ

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

```csharp: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` コンポーネントを使えば、下記のようにマークアップすることで...

```html:App.razor
<Stack Direction="StackDirection.Column">
    <button>Element 1</button>
    <button>Element 2</button>
</Stack>
```
こんな感じで容易に flex レイアウトによる縦積みが組めます。イイ感じですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/95fe0094-ff55-3ebb-9c4c-7670c9314f30.png)

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

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

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

つまりは下記のとおりです。
```csharp:Stack.razor
//👇 この "div" の部分を、パラメータで指定された任意の文字列で出力したい!
<div style="display:flex; flex-direction: @(this.Direction.ToString().ToLower());">
    ...
```
例えば、この `Stack` コンポーネントに `[Parameter] string Component {get; set;}` というパラメータープロパティを追加して、使う側で以下のようにマークアップしたら、

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

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

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

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

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

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

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

そうすると、"obj" フォルダ以下、"generated" という名前のサブフォルダ配下に、変換後の C# ソースコードがファイルとして出力されます (通常はファイルとして出力せずに、コンパイラの内部で生成されるのみ)。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/447fe810-1ac9-75ac-dbae-8d663765e6a8.png)
上図がそうやってファイルに保存するようにした、`.razor` ファイルから変換されたあとの C# ソースコードの様子です。変換元の `.razor` ファイルの名前をもとに、 "～_razor.g.cs" を末尾に付け足した命名則で C# ソースコードファイルとして保存されています。この変換後の C# ソースコードを開いてみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/f055c69f-55d3-bd54-7216-ea2e7d0e9381.png)
自動生成された C# コードなので、型名が `global::` から始まる完全限定名で指定されていたり、各種プラグマによるコンパイラ制御が多数記載されていたり、なかなかに読みにくいですね。それを我慢して忍耐強くコードの構成を読んでみると、以下のことが見えてきます。

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

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

```csharp
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
    builder.OpenElement(0, "div");
    builder.AddAttribute(1, "style", "display:flex;...);
    builder.AddContent(2, ChildContent);
    builder.CloseElement();
}
```
すると、
「`RenderTreeBuilder` の `OpenElement` メソッドで、指定したタグの HTML タグを開いて...」
「`AddAttribute` メソッドで属性追加して...」
「`AddContent` メソッドで子要素を書いて...」
「`CloseElement` メソッドでタグを閉じる」
という構造になっているのがわかりますね。

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

## やってみた

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

```csharp: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` コンポーネントを使ってみます。
```html:App.razor
                         <!-- ここで "header" を指定 👇 -->
<Stack Direction="StackDirection.Column" Component="header">
    <button>Element 1</button>
    <button>Element 2</button>
</Stack>
```
すると、おめでとうございます、無事、`Component` パラメータープロパティに指定した "header" 要素でレンダリングされることが確認できました!

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/9d963a63-b756-6869-5136-9a584f921424.png)

## まとめ

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

_Happy Coding :)_