シナリオ
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 :)