シナリオ
CSS の flex レイアウトを使用して、縦積み・横積みのレイアウトを簡単に組み上げられるよう、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 コンポーネントを使えば、下記のようにマークアップすることで...
<Stack Direction="StackDirection.Column">
<button>Element 1</button>
<button>Element 2</button>
</Stack>
こんな感じで容易に flex レイアウトによる縦積みが組めます。イイ感じですね。

div じゃなくて任意の要素を指定できるようにしたい
ところが、この Stack コンポーネントを広く配布したところ、あちこちから次のような要望が寄せられ始めました。
「<div> で囲うのではなくて、任意の要素、例えば <header> とか <main> とかでも囲えるようにしてほしい」
つまりは下記のとおりです。
//👇 この "div" の部分を、パラメータで指定された任意の文字列で出力したい!
<div style="display:flex; flex-direction: @(this.Direction.ToString().ToLower());">
...
例えば、この Stack コンポーネントに [Parameter] string Component {get; set;} というパラメータープロパティを追加して、使う側で以下のようにマークアップしたら、
<!-- ここで "header" を指定 👇 -->
<Stack Direction="StackDirection.Column" Component="header">
...
</Stack>
以下の HTML が出力されるようにしたい、っていうことです。
<!-- 👇 "Component" プロパティで指定した HTML タグ ("header") でレンダリングされている! -->
<header style="display:flex; flex-direction: column;">
...
</header>
しかし、そんなことできるのでしょうか??
.razor ファイルは C# のソースコードに変換されている
上記要望を叶えるには、実は .razor ファイルは C# のソースコードに変換されている、ということを知っておくとよいかもしれません。.razor ファイルから変換された C# ソースコードを確認するには、いくつかやり方があるようですが、例えば Blaozr アプリケーションのプロジェクトファイル (*.csproj) をエディタで編集して、以下の設定を書き足しておいてから、その Blazor アプリケーションを再ビルトする、という方法があります。
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
...
<!-- 👇この EmitCompilerGeneratedFiles = true の指定を追加する -->
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
...
そうすると、"obj" フォルダ以下、"generated" という名前のサブフォルダ配下に、変換後の C# ソースコードがファイルとして出力されます (通常はファイルとして出力せずに、コンパイラの内部で生成されるのみ)。

上図がそうやってファイルに保存するようにした、.razor ファイルから変換されたあとの C# ソースコードの様子です。変換元の .razor ファイルの名前をもとに、 "~_razor.g.cs" を末尾に付け足した命名則で C# ソースコードファイルとして保存されています。この変換後の C# ソースコードを開いてみましょう。

自動生成された 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();
}
すると、
「RenderTreeBuilder の OpenElement メソッドで、指定したタグの HTML タグを開いて...」
「AddAttribute メソッドで属性追加して...」
「AddContent メソッドで子要素を書いて...」
「CloseElement メソッドでタグを閉じる」
という構造になっているのがわかりますね。
どうでしょう、ここまでわかると、.razor ファイルを使わずに、直接 C# コードのみで Razor コンポーネントを実装できそうな気がしてきませんか? そして直接 C# コードのみで Razor コンポーネントを実装するのなら、「RenderTreeBuilder の OpenElement メソッドで、指定したタグの HTML タグを開いて...」の部分で、レンダリングする HTML タグを任意の文字列で指定できますよね?
やってみた
ということで、Stack.razor は削除し、改めて Stack.cs として、C# コードのみで Stack Razor コンポーネントを実装しなおしてみました。幸いにして Stack コンポーネントの構造は至極シンプルなので、以下に全文掲載できます。
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 コンポーネントを使ってみます。
<!-- ここで "header" を指定 👇 -->
<Stack Direction="StackDirection.Column" Component="header">
<button>Element 1</button>
<button>Element 2</button>
</Stack>
すると、おめでとうございます、無事、Component パラメータープロパティに指定した "header" 要素でレンダリングされることが確認できました!
まとめ
Razor コンポーネントはつまるところは ComponentBase クラスから派生したただのクラスに過ぎず、BuildRenderTree 仮想メソッドのオーバーライドにて、DOM 構造を構築する処理を実装しているのでした。そのため、.razor ではなく直接 C# コードのみで Razor コンポーネントを実装できることがわかりました。もちろん大抵の場合は、Razor 構文を使って .razor ファイルとして Razor コンポーネントを実装するほうがラクに実装できるはずです。いっぽうで .razor ファイルを使わずに C# コードで直接記述することにより、Razor 構文では実現困難なコンポーネントを実装できる能力が手に入ります。一般的なアプリケーションのレイヤでは出番はないかもしれませんが、ユーザーインターフェース部品的なコンポーネント実装では、C# コードで直接 Razor コンポーネントを実装する技法が役に立つことがあるかもしれませんね。
Happy Coding :)
