ASP.NET Core Blazor コンポーネントのレンダリング

コンポーネントは、親コンポーネントによってコンポーネント階層に最初に追加されるときにレンダリングされる "必要" があります。 コンポーネントのレンダリングが必要なのは、このときだけです。 コンポーネントでは、その独自のロジックと規則に従って、他のタイミングでレンダリングすることが "できます"。

ComponentBase のレンダリング規則

既定では、Razor コンポーネントは ComponentBase 基底クラスから継承されます。これには次のタイミングで再レンダリングをトリガーするロジックが含まれています。

次のいずれかに該当する場合、ComponentBase から継承されるコンポーネントの再レンダリングは、パラメーターの更新が理由でスキップされます。

  • パラメーター値はすべて、既知の変更できないプリミティブ型 (intstringDateTime など) であり、前にパラメーター セットが設定されてから変更されていない。
  • コンポーネントの ShouldRender メソッドから false が返される。

ShouldRender の詳細については、「ASP.NET Core Blazor WebAssembly パフォーマンスに関するベスト プラクティス」を参照してください。

レンダリング フローを制御する

ほとんどの場合、イベントの発生後は ComponentBase 規則に従って適切な一部のコンポーネント再レンダリングが行われます。 通常、開発者は、再レンダリングするコンポーネントと、それらを再レンダリングするタイミングをフレームワークに指示する手動のロジックを用意する必要はありません。 フレームワークの規則の全体的な効果は、イベントを受信したコンポーネントが自動的に再レンダリングされることにあります。これにより、パラメーター値が変更された可能性のある子孫コンポーネントの再レンダリングが再帰的にトリガーされます。

フレームワークの規則がパフォーマンスに及ぼす影響と、レンダリングのためにアプリのコンポーネント階層を最適化する方法の詳細については、「ASP.NET Core Blazor WebAssembly パフォーマンスに関するベスト プラクティス」を参照してください。

StateHasChanged を呼び出すタイミング

StateHasChanged を呼び出すと、いつでもレンダリングをトリガーできます。 ただし、StateHasChanged を不必要に呼び出さないように注意してください。これはよくある間違いで、不必要なレンダリング コストがかかる原因になります。

次の場合は、コードで StateHasChanged を呼び出す必要はありません。

  • ComponentBase によってほとんどのルーチン イベント ハンドラーに対するレンダリングがトリガーされるため、同期的または非同期的にかかわらず、イベントを定期的に処理する。
  • ComponentBase によって典型的なライフサイクル イベントに対するレンダリングがトリガーされるため、同期的または非同期的にかかわらず、OnInitializedOnParametersSetAsync などの典型的なライフサイクル ロジックを実装する。

ただし、この記事の次のセクションで説明するケースでは、StateHasChanged を呼び出すことが理にかなっている場合があります。

非同期ハンドラーに複数の非同期フェーズが含まれる

.NET でのタスクの定義方法が理由で、Task の受信側で観察できるのは、中間の非同期状態ではなく、最終的な完了だけとなっています。 したがって、ComponentBase で再レンダリングをトリガーできるのは、Task が最初に返されたときと、Task が最終的に完了したときに限られます。 フレームワークには、他の中間ポイントでのコンポーネントの再レンダリングに対する認識はありません。 中間ポイントで再レンダリングを行いたい場合は、それらのポイントで StateHasChanged を呼び出します。

クリックのたびにカウントを 4 回更新する以下の CounterState1 コンポーネントについて考えてみましょう。

  • 自動レンダリングは、currentCount の最初と最後のインクリメントの後に行われます。
  • 手動レンダリングは、currentCount がインクリメントされる中間処理ポイントでフレームワークによって再レンダリングが自動的にトリガーされない場合に、StateHasChanged の呼び出しによってトリガーされます。

Pages/CounterState1.razor:

@page "/counter-state-1"

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

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

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

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}

Blazor 再レンダリングおよびイベント処理システムの外部の何かからの呼び出しを受信する

ComponentBase によって認識されるのは、その独自のライフサイクル メソッドと Blazor でトリガーされるイベントのみです。 コード内で発生する可能性がある他のイベントについては、ComponentBase によって認識されません。 たとえば、C# カスタム データ ストアによって発生したイベントは、Blazor によって認識されません。 そのようなイベントで再レンダリングをトリガーして、更新された値を UI に表示するためには、StateHasChanged を呼び出します。

System.Timers.Timer を使用してカウントを一定の間隔で更新し、StateHasChanged を呼び出して UI を更新する次の CounterState2 コンポーネントについて考えてみましょう。

  • OnTimerCallback は、Blazor で管理される再レンダリング フローまたはイベント通知の外部で実行されます。 したがって、コールバックでの currentCount への変更は Blazor によって認識されないため、OnTimerCallbackStateHasChanged を呼び出す必要があります。
  • コンポーネントによって IDisposable が実装されます。この場合、フレームワークによって Dispose メソッドが呼び出されると、Timer が破棄されます。 詳細については、「ASP.NET Core Razor コンポーネントのライフサイクル」を参照してください。

コールバックは Blazor の同期コンテキストの外部で呼び出されるため、コンポーネントでは ComponentBase.InvokeAsync 内の OnTimerCallback のロジックをラップして、それをレンダラーの同期コンテキストに移動することが必要です。 StateHasChanged を呼び出すことができるのは、レンダラーの同期コンテキストからのみであり、それ以外の場合は例外がスローされます。 これは、他の UI フレームワーク内の UI スレッドへのマーシャリングに相当します。

Pages/CounterState2.razor:

@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

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

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

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

@code {
    private int currentCount = 0;
    private Timer timer = new Timer(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}

特定のイベントによって再レンダリングされるサブツリーの外部でコンポーネントをレンダリングするには

UI では次のことが必要な場合があります。

  1. 1 つのコンポーネントにイベントをディスパッチする。
  2. 一部の状態を変更する。
  3. イベントを受け取るコンポーネントの子孫ではないまったく別のコンポーネントを再レンダリングする。

このシナリオに対処する方法の 1 つは、複数のコンポーネントに挿入される "状態管理" クラスを、多くの場合は依存関係の挿入 (DI) サービスとして提供することです。 状態マネージャー上で 1 つのコンポーネントによってメソッドが呼び出されると、別個のコンポーネントによって受信される C# イベントがその状態マネージャーによって引き起こされます。

これらの C# イベントは Blazor レンダリング パイプラインの外部にあるため、状態マネージャーのイベントに応答してレンダリングしたい他のコンポーネント上で StateHasChanged を呼び出してください。

これは、前のセクションでの System.Timers.Timer に関する前のケースと似ています。 実行コール スタックは一般にレンダラーの同期コンテキスト上に残っているため、InvokeAsync の呼び出しは通常必要ありません。 InvokeAsync の呼び出しは、ロジックによって同期コンテキストがエスケープされる場合にのみ必要です (Task 上で ContinueWith が呼び出される場合や、ConfigureAwait(false) を使用して Task が待機される場合など)。