2019 年 3 月

Volume 34 Number 3

[Cutting Edge]

階層的 Blazor コンポーネント

執筆者: Dino Esposito

Dino Espositoシングルページ アプリケーション (SPA) ファミリへの最新のフレームワークの追加によって、Blazor では、Angular や React などの他のフレームワークの最善の特性を利用する機会を得ることができました。Blazor の中心概念は C# と Razor を活用して SPA アプリケーションをビルドすることですが、他のフレームワークによって明らかにインスパイアされた 1 つの側面は、コンポーネントの使用です。

Blazor コンポーネントは Razor 言語を使用して記述されますが、その方法は MVC ビューのビルド方法とほぼ同じであり、これこそが開発者を強く引き付ける点です。ASP.NET Core では、タグ ヘルパーと呼ばれる新しい言語の成果物を通して、先例のないレベルの表現度に到達することができます。タグ ヘルパーは、特定のマークアップ ツリーを解析して有効な HTML5 に変換することを命令される C# クラスです。HTML の複雑なカスタム チャンクを作成するときに発生するすべてのブランチがコード内で処理され、開発者がテキスト ファイルに記述するのは、シンプルなマークアップだけです。タグ ヘルパーを使用すると、コード スニペットの量が大幅に減少します。タグ ヘルパーは有用ですが、それでもプログラミング上の欠点がいくつかあります。Blazor コンポーネントは、それらを見事に解決します。この記事では、Bootstrap 4 フレームワーク のサービスを使用してモーダル ダイアログ ボックスを表示する新しい Blazor コンポーネントを作成します。これを行いながら、Blazor テンプレート コンポーネントとカスケード パラメーターについて説明していきます。

タグ ヘルパーの欠点

自著『Programming ASP.NET Core』 (Microsoft Press、2018 年発行) の中で、私は、上に述べたジョブとほぼ同じことを実行するタグ ヘルパーの例を提示しました。それは、HTML 以外のその場限りのマークアップを、モーダル ダイアログ ボックス用の Bootstrap 固有のマークアップに変換するものです (bit.ly/2RxmWJS をご覧ください)。

入力マークアップと目的の出力の間のすべての変換は、C# コードを通して実行されます。タグ ヘルパーは、実際には、基底クラス TagHelper を継承し、単一のメソッドをオーバーライドするシンプルな C# クラスです。問題は、この変換とマークアップの組み合わせをコードで表現する必要があることです。これは柔軟性を高めますが、変更時のコンパイルも必要になります。特に、C# コードを使用して、すべての属性と子要素のセットが指定された DIV ツリーを記述する必要があります。

Blazor では、Bootstrap のモーダル ダイアログ ボックスなどの洗練された要素のためのわかりやすいマークアップ構文の作成でタグ ヘルパーに頼る必要がないため、作業は非常に簡単になります。それでは、Blazor でモーダル コンポーネントを作成する方法を見ていきましょう。

モーダル ダイアログ ボックス

考え方として、Bootstrap のモーダル ダイアログ ボックス コンポーネントをラップする再利用可能な Blazor コンポーネントを設定します。図 1 は、Bootstrap (バージョン 3.x と 4.x の両方) が動作するために必要な、よくご存じの HTML5 マークアップ ツリーを表しています。

図 1: モーダル ダイアログ ボックス用の Bootstrap のマークアップ

<button type="button" class="btn btn-primary"
        data-toggle="modal"
        data-target="#exampleModal">
  Open modal
</button>
<div class="modal">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title">Modal title</h5>
        <button type="button" class="close" data-dismiss="modal">
          <span>&times;</span>
        </button>
      </div>
      <div class="modal-body">
        <p>Modal body text goes here.</p>
      </div>
      <div class="modal-footer">
        <button type="button"
                class="btn btn-secondary"
                data-dismiss="modal">Close</button>
      </div>
    </div>
  </div>
</div>

複数のビューとページを表示するためにこのマークアップを何度も何度も繰り返すことを楽しいと思っている Web 開発者は 1 人もいません。このマークアップの大半は単なるレイアウトであり、可変情報は、表示するテキストと、おそらくはいくつかのスタイルとボタンだけです。もっと覚えやすく、もっと表現力のあるマークアップがあります。

<Modal>
  <Toggle class="btn"> Open </Toggle>
  <Content>
    <HeaderTemplate> ... </HeaderTemplate>
    <BodyTemplate> ... </BodyTemplate>
    <FooterTemplate> ... </FooterTemplate>
  </Content>
</Modal>

この表現力のあるマークアップ コード内のモーダル コンポーネントの構成要素は、即座にわかります。このマークアップには、ラッパーである Modal 要素と 2 つの子サブツリー (1 つはトグル ボタン用、1 つは実際のコンテンツ用) が含まれています。

Bootstrap のモーダルの構文に従うと、何らかのダイアログを表示するにはトリガーが必要です。このトリガーは、通常は、1 対のデータ切り替え属性とデータ ターゲット属性で修飾されたボタン要素です。ただし、モーダルは、JavaScript を使用してトリガーすることもできます。Toggle サブコンポーネントは、単にトリガー マークアップのコンテナーとして機能します。代わりに、Content サブコンポーネントが、ヘッダー、本文、およびフッターという 3 つのセグメントに分割されるダイアログ ボックスのコンテンツ全体をラップします。

手短に言うと、上記のコード スニペットに基づく UI は、"Open (開く)" というラベルの付いた主ボタンで構成されます。 そのボタンをクリックすると、ヘッダー、ボディ、およびフッターの 3 つのレイヤーで構成された DIV がポップアップします。

モーダル ダイアログ ボックスに必要な入れ子のコンポーネントを作成するには、テンプレート化されたコンポーネントとカスケード パラメーターを処理する必要があります。カスケード型パラメーターを使用するには、Blazor 0.7.0 以降を実行する必要があることに注意してください。

モーダル コンポーネント

図 2 に示したコードを見ていきましょう。このマークアップは最小限のものであり、テンプレート化されたマークアップの周囲の DIV 要素のみが含まれています。図 2 の modal.cshtml ファイルでは、(十分と思われる) すべての子コンテンツを収集する ChildContent という名前のテンプレート プロパティが宣言されています。このマークアップを実行すると、切り替えマークアップと、ダイアログに表示する実際のコンテンツの両方を収集する DIV 要素がプッシュされます。

図 2: モーダル コンポーネントのソース コード

<CascadingValue Value="@Context">
  <div>
    @ChildContent
  </div>
</CascadingValue>
@functions
{
  protected override void OnInit()
  {
    Context = new ModalContext
    {
      Id = Id,
      AutoClose = AutoClose
    };
  }
  ModalContext Context { get; set; }
  [Parameter] private string Id { get; set; }
  [Parameter] private bool AutoClose { get; set; }
  [Parameter] RenderFragment ChildContent { get; set; }
}

一見したところ、このコンテナー コンポーネントは非常に有用であるようには思えません。それにもかかわらず、これは、Bootstrap ダイアログ ボックス用のマークアップで必要な構造を考えると、重要な役割を果たします。Toggle コンポーネントと Content コンポーネントは、どちらも、モーダル ダイアログを一意に識別する同じ ID を共有します。ラッパー コンポーネントを使用することで、この ID 値を 1 か所でキャプチャし、それをツリーの下方向に向かってカスケード処理できます。ただし、この特定のケースでは、ID は、マークアップの最も内側のレイヤーを通してカスケード処理したい唯一のパラメーターではありません。モーダル ダイアログ には、必要に応じて、閉じるボタンをヘッダーに含めたり、ダイアログやアニメーションのサイズに関連するその他の属性も指定できます。これらすべての情報をカスタム データ転送オブジェクトにグループ化し、ツリーを通してカスケード処理できます。

次のコードに示すように、閉じるボタン用の ID とブール値の収集には ModalContext クラスが使用されます。

public class ModalContext
{
  public string Id { get; set; }
  public bool AutoClose { get; set; }
}

CascadingValue 要素は、指定された式をキャプチャし、明示的にバインドされている最も内側のすべてのコンポーネントとそれを自動的に共有します。カスケード型パラメーター機能がなければ、複雑な階層的コンポーネントで値を共有するには、必要なときに常に明示的に値を注入する必要があります。この機能がない場合は、次のコードに示すように、同じ ID を 2 回指定する必要があります。

<Modal>
  <Toggle id="myModal" class="btn btn-primary btn-lg">
    ...
  </Toggle>
  <Content id="myModal">
    ...
  </Content>
</Modal>

カスケード型の値は、複数のサブコンポーネントで構成される複合コンポーネントの階層に沿って同じ値セットを渡す必要がある状況で役立ちます。カスケード型の値は、単一のコンテナー内にグループ化する必要があることに注意してください。そのため、複数のスカラー値に渡す必要がある場合は、コンテナー オブジェクトをまず定義する必要があります。モーダル コンポーネントの階層のパラメーターのフローを 図 3 に示します。

階層的コンポーネントでのカスケード型の値
図 3: 階層的コンポーネントでのカスケード型の値

Modal コンポーネントの内側のコンテンツは再帰的に解析されますが、それは Toggle コンポーネントと Content コンポーネントによって処理されます。Toggle.cshtml コンポーネントのソースを次に示します。

<button class="@Class"
        data-toggle="modal"
        data-target="#@OutermostEnv.Id">
  @ChildContent
</button>
@functions
{
  [CascadingParameter] protected ModalContext OutermostEnv { get; set; }
  [Parameter] string Class { get; set; }
  [Parameter] RenderFragment ChildContent { get; set; }
}

現在の実装では、トグル要素のスタイルは、Class という名前のパブリック プロパティを介して設定されます。ボタンのコンテンツは、ChildContent という名前のテンプレート化されたプロパティを介してキャプチャされます。Blazor では、ChildContent という名前のテンプレート プロパティによって、親要素の子マークアップ全体が自動的に キャプチャされることに注意してください。また、Blazor 内のテンプレート プロパティは、RenderFragment 型 のプロパティです。

上記のソース コードで興味深いのは、カスケード型の値に対するバインドです。CascadingParameter 属性を使用して、OutermostEnv などのコンポーネント プロパティを修飾します。このプロパティには、最も内側のレベルのカスケードされた値が設定されます。その結果、OutermostEnv では、ルート コンポーネントの Init メソッド内に新たに作成された ModalContext インスタンスに割り当てられた値が使用されます (図 2 を参照してください)。

Toggle コンポーネントでは、カスケードされた Id 値を使用して、data-target 属性の値が設定されます。Bootstrap の用語では、ダイアログのトグル ボタンの data-target 属性は、このトグルがクリックされたときにポップアップされる DIV の ID を識別します。

モーダル ダイアログのコンテンツ

Bootstrap のダイアログ ボックスは、垂直方向にレイアウトされる最大 3 つの DIV ブロック (ヘッダー、本文、およびフッター) で構成されます。それらはすべて省略可能ですが、ユーザーに最小限のフィードバックを与えるために、少なくとも 1 つを定義しておきたいと考えるでしょう。テンプレート化されたコンポーネントは、その目的にぴったり合っています。Content.cshtml ファイルの結果としての Content コンポーネントのパブリック インターフェイスを次に示します。

@functions
{
  [CascadingParameter] ModalContext OutermostEnv { get; set; }
  [Parameter] RenderFragment HeaderTemplate { get; set; }
  [Parameter] RenderFragment BodyTemplate { get; set; }
  [Parameter] RenderFragment FooterTemplate { get; set; }
}

OutermostEnv カスケード型パラメーターは、Content コンポーネントの領域外で定義されたデータを持ち込みます。ここでは、ID プロパティと AutoClose プロパティの両方が使用されます。Id 値は、ダイアログ ボックスの最も外側のコンテナーを識別するために使用されます。モーダルがトリガーされると、ID で署名された DIV がポップアップします。AutoClose 値は、ヘッダー バーに閉じるボタンを移動する必要があるかどうかを決定する IF ステートメントを制御するために 使用されます。

最後に、3 つの RenderFragment テンプレート プロパティによって、カスタマイズ可能領域の実際のコンテンツであるヘッダー、フッター、および本文が定義されます。

図 4 からわかるように、モーダル ダイアログ ボックス用に想定される Bootstrap のマークアップをレンダリングするための作業の大半は Content コンポーネントによって実行されます。これは、HTML のレイアウト全体を定義し、テンプレート プロパティを使用して、特定のダイアログを固有のものにするヘッダー、フッター、および本文マークアップの詳細をインポートします。Blazor テンプレートによって、実際のマークアップを呼び出し元ページ内のインライン コンテンツとして指定できます。呼び出し元ページのソース コード (サンプル アプリケーションでは Cascade と呼ばれています) は、図 3 に示されています。

図 4: Content コンポーネントのマークアップ

<div class="modal" id="@OutermostEnv.Id">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title">
          @HeaderTemplate
        </h5>
        @if (OutermostEnv.AutoClose)
        {
          <button type="button" class="close"
                  data-dismiss="modal">
            <span>&times;</span>
          </button>
        }
      </div>
      <div class="modal-body">
        @BodyTemplate
      </div>
      <div class="modal-footer">
        @FooterTemplate
      </div>
    </div>
  </div>
</div>

カスケード型の値とカスケード型パラメーターの詳細

カスケード型の値は、サブコンポーネントの積み重ねの中で、値を効率的に下に渡すという問題を解決します。カスケード型の値は、複雑な階層の中でさまざまなレベルで定義することができ、先祖コンポーネントからすべての子孫コンポーネントに移動できます。各先祖要素は単一のカスケード型の値を定義でき、複数のスカラー値をまとめて収集する複合オブジェクトになる可能性があります。

カスケードされた値を使用するには、子孫コンポーネントでカスケード パラメーターを宣言します。カスケード パラメーターは、CascadingParameter 属性で修飾されたパブリック プロパティまたは保護されたプロパティです。カスケード値は、次のように Name プロパティに関連付けることができます。

<CascadingValue Value=@Context Name="ModalDialogGlobals">
  ...
</CascadingValue>

この場合、子孫は、次に示すように、Name プロパティを使用してカスケードされた値を取得します。

[CascadingParameter(Name = "ModalDialogGlobals")]
ModalContext OutermostEnv { get; set; }

名前が指定されていない場合、カスケード値は、型によってカスケード パラメーターにバインドされます。

まとめ

カスケード値は、特に階層的コンポーネント向けに設計されていますが、同時に、階層的 (テンプレート化された) コンポーネントは、現実的には、開発者が記述することが想定されている最も一般的な種類の Blazor コンポーネントです。この記事では、カスケード パラメーターとテンプレート化された階層的コンポーネントについて説明しましたが、Razor コンポーネントでハイレベルの構文を使用して特定のマークアップを表現することがどれほど高機能かを示しました。具体例として、Bootstrap のモーダル ダイアログ ボックスをレンダリングするためのカスタム マークアップ構文を作成しました。従来の ASP.NET MVC では、タグ ヘルパーまたは HTML ヘルパーを使用して、標準の ASP.NET Core で同じことを実現できることに注意してください。

この記事のソース コードは bit.ly/2FdGZat で入手できます。


Dino Esposito  氏は、彼の 25 年間のキャリアの中で、20 冊を超える本と 1,000 以上の記事の著者となっています。劇形式の作品『The Sabbatical Break』の著者である Esposito は、BaxEnergy のデジタル ストラテジストとして、より良い世界を構築するためにソフトウェアの記述に取り組んでいます。彼には Twitter (@despos、英語) から連絡できます。

この記事のレビューに協力してくれたマイクロソフト技術スタッフに心より感謝いたします。Daniel Roth