ASP.NET Core Blazor の値とパラメーターのカスケード

注意

これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

重要

この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。

現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

この記事では、先祖の Razor コンポーネントから子孫のコンポーネントにデータをフローさせる方法について説明します。

"カスケード値とパラメーター" の使用は、コンポーネント階層で先祖コンポーネントから下位の任意の数の子孫コンポーネントにデータをフローさせる便利な方法です。 カスケード値およびパラメーターでは、コンポーネント パラメーターとは異なり、データが使用される各子孫コンポーネントに属性を割り当てる必要がありません。 また、カスケード値とパラメーターを使用すると、コンポーネント階層全体でコンポーネントを相互連携させることができます。

Note

この記事のコード例では、null 許容参照型 (NRT) と .NET コンパイラの null 状態スタティック分析を採用しています。これは、.NET 6 以降の ASP.NET Core でサポートされています。 ASP.NET Core 5.0 以前をターゲットとする場合は、記事の例の CascadingType?@ActiveTab?RenderFragment?ITab?TabSet?string? 型から null 型の指定 (?) を削除します。

ルート レベルのカスケード値

ルート レベルのカスケード値は、コンポーネント階層全体に登録できます。 更新通知の名前付きカスケード値とサブスクリプションがサポートされています。

このセクションの例では、次のクラスを使用します。

Dalek.cs:

// "Dalek" ©Terry Nation https://www.imdb.com/name/nm0622334/
// "Doctor Who" ©BBC https://www.bbc.co.uk/programmes/b006q2x0

namespace BlazorSample;

public class Dalek
{
    public int Units { get; set; }
}

アプリの Program ファイルでは、AddCascadingValue を使って次の登録が行われます。

  • Units のプロパティ値を持つ Dalek は固定カスケード値として登録されます。
  • 別の Units のプロパティ値を持つ 2 つ目の Dalek 登録の名前は "AlphaGroup" です。
builder.Services.AddCascadingValue(sp => new Dalek { Units = 123 });
builder.Services.AddCascadingValue("AlphaGroup", sp => new Dalek { Units = 456 });

次の Daleks コンポーネントは、カスケードされた値を表示します。

Daleks.razor:

@page "/daleks"

<PageTitle>Daleks</PageTitle>

<h1>Root-level Cascading Value Example</h1>

<ul>
    <li>Dalek Units: @Dalek?.Units</li>
    <li>Alpha Group Dalek Units: @AlphaGroupDalek?.Units</li>
</ul>

<p>
    Dalek© <a href="https://www.imdb.com/name/nm0622334/">Terry Nation</a><br>
    Doctor Who© <a href="https://www.bbc.co.uk/programmes/b006q2x0">BBC</a>
</p>

@code {
    [CascadingParameter]
    public Dalek? Dalek { get; set; }

    [CascadingParameter(Name = "AlphaGroup")]
    public Dalek? AlphaGroupDalek { get; set; }
}

次の例では、DalekCascadingValueSource<T> を使用してカスケード値として登録されます。ここで、<T> は型です。 isFixed フラグは、値が固定されているかどうかを示します。 false の場合、すべての受信者が更新通知に登録します。これは、NotifyChangedAsync を呼び出すことで発行されます。 サブスクリプションではオーバーヘッドが発生し、パフォーマンスが低下するため、値が変更されない場合は isFixedtrue に設定します。

builder.Services.AddCascadingValue(sp =>
{
    var dalek = new Dalek { Units = 789 };
    var source = new CascadingValueSource<Dalek>(dalek, isFixed: false);
    return source;
});

警告

コンポーネントの型をルートレベルのカスケード値として登録しても、その型の追加サービスが登録されたり、コンポーネントでのサービスのアクティブ化が許可されたりすることはありません。

必要なサービスをカスケード値とは別に扱い、カスケードされた型とは別に登録します。

コンポーネントの型をカスケード値として登録するために AddCascadingValue を使わないでください。 代わりに、Routes コンポーネント (Components/Routes.razor) 内の <Router>...</Router> をコンポーネントで囲み、グローバル対話型サーバー側レンダリング (対話型 SSR) を採用します。 例については、「CascadingValue コンポーネント」セクションを参照してください。

CascadingValue コンポーネント

先祖コンポーネントは、コンポーネント階層のサブツリーをラップし、そのサブツリー内のすべてのコンポーネントに単一の値を提供する Blazor フレームワークの CascadingValue コンポーネントを使用して、カスケード値を提供します。

次の例では、子コンポーネントのボタンに CSS 形式のクラスを提供する、コンポーネント階層におけるテーマ情報のフローを示しています。

次の ThemeInfo C# クラスは、テーマの情報を指定します。

Note

このセクションの例では、アプリの名前空間は BlazorSample です。 自分独自のサンプル アプリでコードを試す場合は、アプリの名前空間をお使いのサンプル アプリの名前空間に変更します。

ThemeInfo.cs:

namespace BlazorSample;

public class ThemeInfo
{
    public string? ButtonClass { get; set; }
}
namespace BlazorSample.UIThemeClasses;

public class ThemeInfo
{
    public string? ButtonClass { get; set; }
}
namespace BlazorSample.UIThemeClasses;

public class ThemeInfo
{
    public string? ButtonClass { get; set; }
}
namespace BlazorSample.UIThemeClasses
{
    public class ThemeInfo
    {
        public string ButtonClass { get; set; }
    }
}
namespace BlazorSample.UIThemeClasses
{
    public class ThemeInfo
    {
        public string ButtonClass { get; set; }
    }
}

次のレイアウト コンポーネントは、Body プロパティのレイアウト本体を構成するすべてのコンポーネントに、テーマ情報 (ThemeInfo) をカスケード値として指定しています。 ButtonClass には、Bootstrap ボタン形式の btn-success 値が割り当てられています。 ButtonClass プロパティは、ThemeInfo カスケード値を介し、コンポーネント階層内のすべての子孫コンポーネントで使用できます。

MainLayout.razor:

@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
        </div>

        <CascadingValue Value="@theme">
            <article class="content px-4">
                @Body
            </article>
        </CascadingValue>
    </main>
</div>

<div id="blazor-error-ui" data-nosnippet>
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
        </div>

        <CascadingValue Value="@theme">
            <article class="content px-4">
                @Body
            </article>
        </CascadingValue>
    </main>
</div>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <CascadingValue Value="@theme">
            <div class="content px-4">
                @Body
            </div>
        </CascadingValue>
    </main>
</div>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <div class="main">
        <CascadingValue Value="@theme">
            <div class="content px-4">
                @Body
            </div>
        </CascadingValue>
    </div>
</div>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses

<div class="sidebar">
    <NavMenu />
</div>

<div class="main">
    <CascadingValue Value="theme">
        <div class="content px-4">
            @Body
        </div>
    </CascadingValue>
</div>

@code {
    private ThemeInfo theme = new ThemeInfo { ButtonClass = "btn-success" };
}

Blazor Web アプリには、1 つのレイアウト ファイルを介して提供する場合より広くアプリに適用されるカスケード値に関して、別の方法が用意されています。

  • Routes コンポーネントのマークアップを CascadingValue コンポーネントにラップして、データをアプリのすべてのコンポーネントのカスケード値として指定します。

    次の例では、Routes コンポーネントから ThemeInfo データをカスケードします。

    Routes.razor:

    <CascadingValue Value="theme">
        <Router ...>
            ...
        </Router>
    </CascadingValue>
    
    @code {
        private ThemeInfo theme = new() { ButtonClass = "btn-success" };
    }
    

    Note

    App コンポーネント (Components/App.razor) 内の Routes コンポーネント インスタンスを CascadingValue コンポーネントでラップすることはサポートされていません

  • サービス コレクション ビルダーで AddCascadingValue 拡張メソッドを呼び出して、"ルート レベルのカスケード値" をサービスとして指定します。

    次の例では、Program ファイルから ThemeInfo データをカスケードします。

    Program.cs

    builder.Services.AddCascadingValue(sp => 
        new ThemeInfo() { ButtonClass = "btn-primary" });
    

詳細については、この記事の以下のセクションを参照してください。

[CascadingParameter] 属性

子孫コンポーネントでは、[CascadingParameter] 属性を使用してカスケード型パラメーターを宣言し、カスケード値を使用します。 カスケード値は、型でカスケード型パラメーターにバインドされます。 同じ型の複数の値のカスケードについては、後でこの記事の「複数の値のカスケード」セクションで説明します。

次のコンポーネントは、オプションで同じ ThemeInfo 名を使用してカスケード型パラメーターに ThemeInfo カスケード値をバインドします。 このパラメーターは、 Increment Counter (Themed) ボタンの CSS クラスを設定するのに使用されます。

ThemedCounter.razor:

@page "/themed-counter"

<PageTitle>Themed Counter</PageTitle>

<h1>Themed Counter Example</h1>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">
        Increment Counter (Unthemed)
    </button>
</p>

<p>
    <button 
        class="btn @(ThemeInfo is not null ? ThemeInfo.ButtonClass : string.Empty)" 
        @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    [CascadingParameter]
    protected ThemeInfo? ThemeInfo { get; set; }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/themed-counter"
@using BlazorSample.UIThemeClasses

<h1>Themed Counter</h1>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">
        Increment Counter (Unthemed)
    </button>
</p>

<p>
    <button 
        class="btn @(ThemeInfo is not null ? ThemeInfo.ButtonClass : string.Empty)" 
        @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    [CascadingParameter]
    protected ThemeInfo? ThemeInfo { get; set; }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/themed-counter"
@using BlazorSample.UIThemeClasses

<h1>Themed Counter</h1>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">
        Increment Counter (Unthemed)
    </button>
</p>

<p>
    <button 
        class="btn @(ThemeInfo is not null ? ThemeInfo.ButtonClass : string.Empty)" 
        @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    [CascadingParameter]
    protected ThemeInfo? ThemeInfo { get; set; }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/themed-counter"
@using BlazorSample.UIThemeClasses

<h1>Themed Counter</h1>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">
        Increment Counter (Unthemed)
    </button>
</p>

<p>
    <button class="btn @ThemeInfo.ButtonClass" @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    [CascadingParameter]
    protected ThemeInfo ThemeInfo { get; set; }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/themed-counter"
@using BlazorSample.UIThemeClasses

<h1>Themed Counter</h1>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">
        Increment Counter (Unthemed)
    </button>
</p>

<p>
    <button class="btn @ThemeInfo.ButtonClass" @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    [CascadingParameter]
    protected ThemeInfo ThemeInfo { get; set; }

    private void IncrementCount()
    {
        currentCount++;
    }
}

通常のコンポーネント パラメーターと同じく、カスケード型パラメーターを受け取るコンポーネントはカスケード値が変更されたときに再レンダリングされます。 たとえば、別のテーマ インスタンスを構成すると、「CascadingValue コンポーネント」セクションからの ThemedCounter コンポーネントが再レンダリングされます。

MainLayout.razor:

<main>
    <div class="top-row px-4">
        <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
    </div>

    <CascadingValue Value="theme">
        <article class="content px-4">
            @Body
        </article>
    </CascadingValue>
    <button @onclick="ChangeToDarkTheme">Dark mode</button>
</main>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };

    private void ChangeToDarkTheme()
    {
        theme = new() { ButtonClass = "btn-secondary" };
    }
}

CascadingValue<TValue>.IsFixed を使用すると、初期化後にカスケード型パラメーターが変更されないことを示すことができます。

カスケード値/パラメーターとレンダリング モードの境界

カスケード パラメーターは、レンダー モードの境界を越えてデータを渡しません:

  • 対話型セッションは、静的サーバー側レンダリング (静的 SSR) を使用するページとは異なるコンテキストで実行されます。 ページを生成するサーバーが、後で対話型サーバー セッションをホストするコンピューターと同じコンピューターである必要はありません。たとえば、サーバーがクライアントとは異なるコンピューターである WebAssembly コンポーネントの場合も同様です。 静的サーバー側レンダリング (静的 SSR) の利点は、純粋なステートレス HTML レンダリングの完全なパフォーマンスを実現することです。

  • 静的レンダリングと対話型レンダリングの境界を越える状態は、シリアル化可能である必要があります。 コンポーネントは、レンダラー、DI コンテナー、およびすべての DI サービス インスタンスなど、他のオブジェクトの膨大なチェーンを参照する任意のオブジェクトです。 明示的に静的 SSR から状態をシリアル化して、後続の対話形式でレンダリングされたコンポーネントで使用できるようにする必要があります。 次の 2 つの方法が採用されています:

    • Blazor フレームワークを介して、静的 SSR 経由で対話型レンダリング境界に渡されるパラメータは、JSON シリアル化可能な場合、またはエラーがスローされた場合に、自動的にシリアル化されます。
    • PersistentComponentState に格納されている状態は、JSON シリアル化可能な場合、またはエラーがスローされた場合に自動的にシリアル化および復旧されます。

カスケード パラメーターの一般的な使用パターンは DI サービスに類似したものであるため、カスケード パラメーターは JSON シリアル化できません。 多くの場合、カスケード パラメーターにはプラットフォーム固有のバリエーションがあるため、フレームワークによって開発者がサーバー対話型固有のバージョンまたは WebAssembly 固有のバージョンを持つことを停止した場合、開発者には役に立ちません。 また、一般的に多くのカスケード パラメーター値はシリアル化できないため、すべての非シリアル化可能なカスケード パラメーター値の使用を停止する必要がある場合は、既存のアプリを更新することは現実的ではありません。

レコメンデーション:

  • カスケード パラメーターとしてすべての対話型コンポーネントで状態を使用できるようにする必要がある場合は、ルート レベルのカスケード値使用することをお勧めします。 ファクトリ パターンを使用でき、アプリはアプリの起動後に更新された値を出力できます。 ルート レベルのカスケード値は、DI サービスとして処理されるため、対話型コンポーネントを含むすべてのコンポーネントで使用できます。

  • コンポーネント ライブラリ作成者の場合は、次のようなライブラリ コンシューマー用の拡張メソッドを作成できます:

    builder.Services.AddLibraryCascadingParameters();
    

    拡張機能メソッドを呼び出すように開発者に指示します。 これは、MainLayout コンポーネントに <RootComponent> コンポーネントを追加するように指示しなくて済むための安全な代替手段です。

複数の値のカスケード

同じサブツリー内で同じ型の値を複数カスケードするには、各 CascadingValue コンポーネントとそれに対応する [CascadingParameter] 属性に一意の Name 文字列を指定します。

次の例では、2 つの CascadingValue コンポーネントが、CascadingType の異なるインスタンスをカスケードしています。

<CascadingValue Value="parentCascadeParameter1" Name="CascadeParam1">
    <CascadingValue Value="ParentCascadeParameter2" Name="CascadeParam2">
        ...
    </CascadingValue>
</CascadingValue>

@code {
    private CascadingType? parentCascadeParameter1;

    [Parameter]
    public CascadingType? ParentCascadeParameter2 { get; set; }
}

子孫コンポーネントで、カスケードされたパラメーターはそれらのカスケードされた値を、次のように Name を使用して、先祖コンポーネントから受け取ります。

@code {
    [CascadingParameter(Name = "CascadeParam1")]
    protected CascadingType? ChildCascadeParameter1 { get; set; }

    [CascadingParameter(Name = "CascadeParam2")]
    protected CascadingType? ChildCascadeParameter2 { get; set; }
}

コンポーネント階層に渡ってデータを渡す

カスケード型パラメーターにより、コンポーネントがコンポーネント階層間でデータを渡せるようにすることもできます。 タブ セット コンポーネントによって一連の個別タブが維持される、次の UI タブ セットの例を考えてみてください。

Note

このセクションの例では、アプリの名前空間は BlazorSample です。 自分独自のサンプル アプリでコードを試す場合は、名前空間をお使いのサンプル アプリの名前空間に変更します。

UIInterfaces という名前のフォルダーに、タブが実装する ITab インターフェイスを作成します。

UIInterfaces/ITab.cs:

using Microsoft.AspNetCore.Components;

namespace BlazorSample.UIInterfaces;

public interface ITab
{
    RenderFragment ChildContent { get; }
}

注意

RenderFragment について詳しくは、ASP.NET Core Razor コンポーネントに関する記事を参照してください。

一連のタブは、次の TabSet コンポーネントによって維持されます。 リスト (<ul>...</ul>) のリスト項目 (<li>...</li>) は、このセクションで後で作成するタブ セットの Tab コンポーネントによって提供されます。

Tab コンポーネントは、TabSet にパラメーターとして明示的に渡されません。 代わりに、子 Tab コンポーネントは、TabSet の子コンテンツに含まれます。 ただし、ヘッダーとアクティブなタブをレンダリングできるように、TabSet は、各 Tab コンポーネントをまだ参照する必要があります。追加のコードを必要とせずにこの調整を可能にするために、TabSet コンポーネントでは、それ自体をカスケード値として指定し、その後に子孫 Tab コンポーネントによって取得できるようにします。

TabSet.razor:

@using BlazorSample.UIInterfaces

<!-- Display the tab headers -->

<CascadingValue Value="this">
    <ul class="nav nav-tabs">
        @ChildContent
    </ul>
</CascadingValue>

<!-- Display body for only the active tab -->

<div class="nav-tabs-body p-4">
    @ActiveTab?.ChildContent
</div>

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    public ITab? ActiveTab { get; private set; }

    public void AddTab(ITab tab)
    {
        if (ActiveTab is null)
        {
            SetActiveTab(tab);
        }
    }

    public void SetActiveTab(ITab tab)
    {
        if (ActiveTab != tab)
        {
            ActiveTab = tab;
            StateHasChanged();
        }
    }
}

子孫 Tab コンポーネントは、カスケード型パラメーターとして含まれる TabSet を取得します。 Tab コンポーネントは、アクティブなタブの設定のために自身を TabSet と座標に追加します。

Tab.razor:

@using BlazorSample.UIInterfaces
@implements ITab

<li>
    <a @onclick="ActivateTab" class="nav-link @TitleCssClass" role="button">
        @Title
    </a>
</li>

@code {
    [CascadingParameter]
    public TabSet? ContainerTabSet { get; set; }

    [Parameter]
    public string? Title { get; set; }

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    private string? TitleCssClass => 
        ContainerTabSet?.ActiveTab == this ? "active" : null;

    protected override void OnInitialized()
    {
        ContainerTabSet?.AddTab(this);
    }

    private void ActivateTab()
    {
        ContainerTabSet?.SetActiveTab(this);
    }
}

次の ExampleTabSet コンポーネントは、3 つの Tab コンポーネントを含む TabSet コンポーネントを使用しています。

ExampleTabSet.razor:

@page "/example-tab-set"

<TabSet>
    <Tab Title="First tab">
        <h4>Greetings from the first tab!</h4>

        <label>
            <input type="checkbox" @bind="showThirdTab" />
            Toggle third tab
        </label>
    </Tab>

    <Tab Title="Second tab">
        <h4>Hello from the second tab!</h4>
    </Tab>

    @if (showThirdTab)
    {
        <Tab Title="Third tab">
            <h4>Welcome to the disappearing third tab!</h4>
            <p>Toggle this tab from the first tab.</p>
        </Tab>
    }
</TabSet>

@code {
    private bool showThirdTab;
}

その他のリソース