3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

既存の Razor コンポーネントから "派生" した Razor コンポーネントを作る

Last updated at Posted at 2024-12-12

Razor コンポーネントは C# クラスなので派生できる

既存の Razor コンポーネントに対して、あとちょっと機能を足したいという場合がままあります。そのような場合は、C# の拡張メソッドを実装するとか、その既存の Razor コンポーネントを内包・ラップした新たな Razor コンポーネントを作る、といった方法があります。しかし、そう滅多にあることではないのですが、希に、既存の Razor コンポーネントから "派生" した新たな Razor コンポーネントを作りたい、という場合があります。

幸い、Razor コンポーネントは、つまるところは、単なる C# のクラスに過ぎません。例えば <Foo /> という Razor コンポーネントは、つまりは Foo クラスです。これから派生した新たな Razor コンポーネント、<Bar /> を作りたい、という場合は、プロジェクトに Bar.cs という C# ソースファイルを追加し、そこで、普通に C# の構文で「Foo クラスから派生した Bar クラス」を実装すれば実現できます (下記コード例)。

Bar.cs
public class Bar : Foo {
   // ここに追加の実装を記述
}

.cs で実装すると *.razor.js が error BLAZOR106 を引き起こす

しかし、さらにここで、その派生して作成した <Bar /> に固有の JavaScript モジュールファイル、Bar.razor.js を併設したいとしましょう。C# 側のソースコードは以下のようなイメージです。

Bar.cs
public class Bar : Foo {

  [Inject]
  public IJSRuntime JSRuntime { get; init; } = null!;

  protected override async Task OnAfterRenderAsync(bool firstRender)
  {
    await base.OnAfterRenderAsync(firstRender);
    
    if (firstRender)
    {
      // 👇 JavaScript モジュールファイル、Bar.razor.js を読み込む
      await using var module = await this.JSRuntime.InvokeAsync<IJSObjectReference>(
                                                               "import", "./Bar.razor.js");

      // ここで、Bar.razor.js 内で export された関数を呼び出したりする
      // 例) await module.InvokeAsync<...>(...);
    }
  }
}

ということで、プロジェクトに Bar.razor.js を追加してビルドしてみたところ、残念ながら以下のビルドエラーが発生してしまいました。

error BLAZOR106:
The JS module file '...\Bar.razor.js' was defined but no associated razor component or
view was found for it.

どうやら、Blazor アプリケーションとしてのビルド処理において、*.razor.js のような、Razor コンポーネントに抱き合わせの併設ファイルを配置するには、その親 (?) となる *.razor ファイルが実在していないとダメらしいのです。

では、ということで、C# ソースファイル、すなわち、.cs ファイルではなく、Razor コンポーネント形式、すなわち、.razor ファイルで、Bar コンポーネントを実装してみることにします。Razor コンポーネント (.razor ファイル) においてクラスの派生を用いるには、@inherits ディレクティブを使えばよいです。ということで、実装は以下のようになります。

Bar.razor
@inherits Foo

@code {
  // 以下略
}

これで無事ビルドがとおるようになり、Bar.razor.js を import して使えるようになります。

.cs ではなく .razor で派生すると、派生元コンポーネントのレンダリングが現れなくなる

しかし今度は、せっかく <Foo /> コンポーネントから派生したのに、<Foo /> コンポーネントとしてのレンダリング結果が出力されません。というのも、Razor コンポーネント、すなわち、.razor ファイルを Razor ソースジェネレーターが C# ソースコードに変換する際は、BuildRenderTree メソッドもオーバーライドして、.razor ファイル内に記述されたマークアップをレンダリングするようにコード生成されるからなのですね。つまり、Bar.razor 内のマークアップをレンダリングするよう BuildRenderTree メソッドが上書きされるため、<Foo /> コンポーネントで実装されていた BuildRenderTree メソッドの処理内容は一切呼び出されず使用されないわけです。

そこで、<Foo /> コンポーネントで実装されていた BuildRenderTree の処理内容を、派生先の <Bar /> コンポーネントでも今一度明示的にレンダリングするように、実装を追加します。

まず、派生元のレンダリング処理を保持する RenderFragment 型のフィールド変数を用意し、

Bar.razor
@inherits Foo

@code {
  // 👇 RenderFragment フィールド変数を用意
  private RenderFragment _baseRenderer;
  ...
}

続けて、コンストラクタを追加して、そこで派生元のレンダリング処理、すなわち、派生元の BuildRenderTree メソッド呼び出しを、用意しておいたフィールド変数に格納します。

Bar.razor
@inherits Foo

@code {
  private RenderFragment _baseRenderer;

  // 👇コンストラクタを追加し...
  public Bar() {

    // 👇派生元の BuildRenderTree を呼び出す RenderFragment (ラムダ式) を作って
    //   先に用意したフィールド変数に格納
    _baseRenderer = (builder) => base.BuildRenderTree(builder);
  }
  ...
}

そうしてフィールド変数に格納された RenderFragment を、派生先 .razor 内のマークアップにてレンダリングします。

Bar.razor
@inherits Foo

<!--
// 👇フィールド変数に格納された、派生元の BuildRenderTree を呼び出す RenderFragment を、
//   マークアップ内でレンダリング -->
@_baseRenderer

@code {
  private RenderFragment _baseRenderer;

  public Bar() {
    _baseRenderer = (builder) => base.BuildRenderTree(builder);
  }
  ...
}

おわりに

以上の実装にて、既存の Razor コンポーネントから "派生" した新たな Razor コンポーネントを実装することができました。

そうそう出番のある技法とも思いませんが、バイナリでしか提供されていない既存の Razor コンポーネントにモンキーパッチを当てたい、などのケースで使えたりします。ご参考までに。

3
0
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
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?