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

注意

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

警告

このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、 .NET および .NET Core サポート ポリシーを参照してください。 現在のリリースについては、 この記事の .NET 10 バージョンを参照してください。

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

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

注意

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

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

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

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

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; }
}
// "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]
    private Dalek? Dalek { get; set; }

    [CascadingParameter(Name = "AlphaGroup")]
    public Dalek? AlphaGroupDalek { get; set; }
}
@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]
    private Dalek? Dalek { get; set; }

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

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

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

    return source;
});

警告

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

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

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

システム階層における通知対応のルートレベルカスケード値

更新通知を発行する NotifyChangedAsync を呼び出すと、カスケード値が変更されたことを複数の Razor コンポーネント サブスクライバーに通知できます。 静的サーバー側レンダリング (静的 SSR) を採用するサブスクライバーは通知を使用できないため、サブスクライバーは対話型レンダリング モードを採用する必要があります。

次に例を示します。

  • NotifyingDalek は、プロパティ値が変更されたことをクライアントに通知する INotifyPropertyChanged を実装します。 Units プロパティが設定されると、PropertyChangedEventHandler (PropertyChanged) が呼び出されます。
  • SetUnitsToOneThousandAsyncメソッドは、サブスクライバーによってトリガーされ、シミュレートされた処理遅延でUnitsを 1,000 に設定できます。

運用環境コードでは、状態の変更 (クラスのプロパティ値の変更) によって、使用されている状態のどの部分に関係なく、サブスクライブされているすべてのコンポーネントが再レンダリングされる点に注意してください。 詳細なクラスを作成し、特定のサブスクリプションで個別にカスケードして、アプリケーション状態の特定の部分にサブスクライブされているコンポーネントのみが変更の影響を受けられるようにすることをお勧めします。

注意

サーバーおよびクライアント (Blazor Web App) プロジェクトで構成される.Client ソリューションの場合、次のNotifyingDalek.cs ファイルが.Client プロジェクトに配置されます。

NotifyingDalek.cs:

using System.ComponentModel;
using System.Runtime.CompilerServices;

public class NotifyingDalek : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;
    private int units;

    public int Units
    {
        get => units;
        set
        {
            if (units != value)
            {
                units = value;
                OnPropertyChanged();
            }
        }
    }

    protected virtual void OnPropertyChanged(
        [CallerMemberName] string? propertyName = default)
            => PropertyChanged?.Invoke(this, new(propertyName));

    public async Task SetUnitsToOneThousandAsync()
    {
        // Simulate a three second delay in processing
        await Task.Delay(3000);

        Units = 1000;
    }
}

次のCascadingStateServiceCollectionExtensionsでは、CascadingValueSource<TValue>を実装する型からINotifyPropertyChangedを作成します。

注意

サーバーおよびクライアント (Blazor Web App) プロジェクトで構成される.Client ソリューションの場合、次のCascadingStateServiceCollectionExtensions.cs ファイルが.Client プロジェクトに配置されます。

CascadingStateServiceCollectionExtensions.cs:

using System.ComponentModel;
using Microsoft.AspNetCore.Components;

namespace Microsoft.Extensions.DependencyInjection;

public static class CascadingStateServiceCollectionExtensions
{
    public static IServiceCollection AddNotifyingCascadingValue<T>(
        this IServiceCollection services, T state, bool isFixed = false)
        where T : INotifyPropertyChanged
    {
        return services.AddCascadingValue<T>(sp =>
        {
            return new CascadingStateValueSource<T>(state, isFixed);
        });
    }

    private sealed class CascadingStateValueSource<T>
        : CascadingValueSource<T>, IDisposable where T : INotifyPropertyChanged
    {
        private readonly T state;
        private readonly CascadingValueSource<T> source;

        public CascadingStateValueSource(T state, bool isFixed = false)
            : base(state, isFixed = false)
        {
            this.state = state;
            source = new CascadingValueSource<T>(state, isFixed);
            this.state.PropertyChanged += HandlePropertyChanged;
        }

        private void HandlePropertyChanged(object? sender, PropertyChangedEventArgs e)
        {
            _ = NotifyChangedAsync();
        }

        public void Dispose()
        {
            state.PropertyChanged -= HandlePropertyChanged;
        }
    }
}

型の PropertyChangedEventHandler (HandlePropertyChanged) は、 CascadingValueSource<TValue>の NotifyChangedAsync メソッドを呼び出して、カスケード値が変更されたことをサブスクライバーに通知します。 呼び出しは同期コンテキストへのディスパッチの期間のみを表すので、Taskを呼び出すと、NotifyChangedAsyncは破棄されます。 例外は、更新を受け取る際に例外をスローしたコンポーネントのコンテキスト内で内部的にレンダラーへ渡されて処理されます。 これは、通知受信者内で発生する例外に関する通知を受け取らない CascadingValue<TValue>で例外が処理されるのと同じ方法です。 メモリ リークを防ぐために、イベント ハンドラーは Dispose メソッドで切断されます。

Program ファイルでは、NotifyingDalekが渡され、初期CascadingValueSource<TValue>値が 888 単位のUnitが作成されます。

builder.Services.AddNotifyingCascadingValue(new NotifyingDalek() { Units = 888 });

注意

サーバーおよびクライアント (Blazor Web App) プロジェクトで構成される.Client ソリューションの場合、上記のコードは各プロジェクトのProgram ファイルに配置されます。

次のコンポーネントを使用して、 NotifyingDalek.Units の値を変更してサブスクライバーに通知する方法を示します。

Daleks.razor:

<h2>Daleks component</h2>

<div>
    <b>Dalek Units:</b> @Dalek?.Units
</div>

<div>
    <label>
        <span style="font-weight:bold">New Unit Count:</span>
        <input @bind="dalekCount" />
    </label>
    <button @onclick="Update">Update</button>
</div>

<div>
    <button @onclick="SetOneThousandUnits">Set Units to 1,000</button>
</div>

<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 {
    private int dalekCount;

    [CascadingParameter]
    private NotifyingDalek? Dalek { get; set; }

    private void Update()
    {
        if (Dalek is not null)
        {
            Dalek.Units = dalekCount;
            dalekCount = 0;
        }
    }

    private async Task SetOneThousandUnits()
    {
        if (Dalek is not null)
        {
            await Dalek.SetUnitsToOneThousandAsync();
        }
    }
}

複数のサブスクライバー通知を示すために、次の DaleksMain コンポーネントは 3 つの Daleks コンポーネントをレンダリングします。 1 つのUnits コンポーネントのユニット数 (Dalek) が更新されると、他の 2 つのDalek コンポーネント サブスクライバーが更新されます。

DaleksMain.razor:

@page "/daleks-main"

<PageTitle>Daleks Main</PageTitle>

<h1>Daleks Main</h1>

<Daleks />

<Daleks />

<Daleks />

DaleksMainのNavMenu.razor コンポーネントにナビゲーション リンクを追加します。

<div class="nav-item px-3">
    <NavLink class="nav-link" href="daleks-main">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Daleks
    </NavLink>
</div>

この例の CascadingValueSource<TValue>の型 (NotifyingDalek) はクラス型であるため、ほぼすべての状態管理機能の仕様要件を満たすことができます。 ただし、サブスクリプションではオーバーヘッドが発生し、パフォーマンスが低下するため、アプリでこのアプローチのパフォーマンスをベンチマークし、他の 状態管理アプローチ と比較してから、処理とメモリのリソースが制限された運用アプリで採用します。

状態の変更 (クラスのプロパティ値の変更) により、使用する状態のどの部分に関係なく、サブスクライブされているすべてのコンポーネントが再レンダリングされます。 グローバル アプリケーションの状態全体を表す 1 つの大規模なクラスを作成しないでください。 代わりに、きめ細かいクラスを作成し、それらをカスケード パラメーターに対する特定のサブスクリプションと個別にカスケードして、アプリケーション状態の特定の部分にサブスクライブされているコンポーネントのみが変更の影響を受けないようにします。

CascadingValue コンポーネント

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

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

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

注意

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

ThemeInfo.cs:

namespace BlazorSample;

public class ThemeInfo
{
    public string? ButtonClass { get; set; }
}
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>
    <span class="dismiss">🗙</span>
</div>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
@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 App 方式には、1 つのレイアウト ファイルを介して提供する場合より広くアプリに適用されるカスケード値に関して、別の方法が用意されています。

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

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

    Routes.razor:

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

    注意

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

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

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

    Program.cs

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

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

[CascadingParameter] 属性

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

ほとんどの場合、パラメーターはコンポーネントのクラス内でのみ使用できるようにスコープを設定する必要があるため、カスケード パラメーターには private アクセス修飾子をお勧めします。 サブクラス化が必要な場合は、 protected アクセス修飾子を使用します。

次のコンポーネントは、オプションで 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]
    private ThemeInfo? ThemeInfo { get; set; }

    private void IncrementCount() => currentCount++;
}
@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]
    private 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]
    private 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]
    private 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]
    private 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]
    private ThemeInfo ThemeInfo { get; set; }

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

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

MainLayout.razor:

<main>
    <div class="top-row px-4">
        <a href="https://learn.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 シリアル化可能な場合は自動的にシリアル化され、そうでない場合はエラーがスローされます。
    • 永続的なコンポーネント状態に格納されている状態は、JSON シリアル化可能な場合、またはエラーがスローされた場合に自動的にシリアル化および復旧されます。

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

推奨事項:

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

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

    builder.Services.AddLibraryCascadingParameters();
    

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

複数値をカスケード

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

次の例では、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 タブ セットの例を考えてみてください。

注意

このセクションの例では、アプリの名前空間は 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 コンポーネントは、一連のタブを維持します。 このセクションで後で作成するタブ セットの Tab コンポーネントによって、リスト (<ul>...</ul>) のリスト項目 (<li>...</li>) が提供されます。

子 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]
    private 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;
}