Note
これは、この記事の最新バージョンではありません。 現在のリリースについては、 この記事の .NET 10 バージョンを参照してください。
警告
このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、 .NET および .NET Core サポート ポリシーを参照してください。 現在のリリースについては、この記事の .NET 9 バージョンを参照してください。
Blazor では、同期コンテキスト (SynchronizationContext) を使用して、1 つの実行の論理スレッドを強制します。 コンポーネントのライフサイクル メソッドと、Blazor によって発生するイベント コールバックは、同期コンテキストで実行されます。
Blazor のサーバー側の同期コンテキストでは、ブラウザーの WebAssembly モデル (シングル スレッド) と厳密に一致するように、シングルスレッド環境のエミュレートが試行されます。 このエミュレーションのスコープは個々の回線のみです。つまり、2 つの異なる回線を並列で実行できます。 回線内のどの時点でも、作業は 1 つのスレッドでのみ実行され、1 つの論理スレッドであるという印象になります。 同じ回線内で 2 つの操作は同時に実行されません。
1 つの実行の論理スレッドは、単一の非同期制御フローを意味するものではありません。 コンポーネントは、不完全な Taskを待つ任意の時点で再登録されます。 ライフサイクル メソッド または コンポーネント破棄メソッド は、 Task の完了を待機した後、非同期制御フローが再開される前に呼び出される場合があります。 そのため、コンポーネントは、Taskが不完全である可能性がある場合に備えて、待機する前に有効な状態であることを確認する必要があります。 特に、コンポーネントは、 OnInitializedAsync または OnParametersSetAsync 戻ったときにレンダリングに有効な状態であることを確認する必要があります。 これらのメソッドのいずれかが不完全な Taskを返す場合は、完了したメソッドの部分が、レンダリングに有効な状態でコンポーネントを残すようにする必要があります。
再入コンポーネントに関連する問題の一つは、メソッドが終了した後に、Taskに渡すことでComponentBase.InvokeAsyncを延期できないことです。
ComponentBase.InvokeAsyncを呼び出すと、Taskが次のawait演算子に到達するまで遅延される場合があります。
コンポーネントは、IDisposableまたはIAsyncDisposableを実装して、コンポーネントが破棄されたときに取り消されるCancellationTokenからCancellationTokenSourceを使用して非同期メソッドを呼び出す場合があります。 ただし、これは実際にはシナリオによって異なります。 それが正しい動作であるかどうかを判断するのは、コンポーネントの作成者が行う必要があります。 たとえば、保存ボタンを選択したときにローカル データをデータベースに保持する SaveButton コンポーネントを実装する場合、ユーザーがボタンを選択して別のページにすばやく移動すると、コンポーネント作成者は実際に変更を破棄する可能性があります。これは、非同期保存が完了する前にコンポーネントを破棄する可能性があります。
破棄可能なコンポーネントは、コンポーネントのTaskを受け取らないCancellationTokenを待機した後、破棄を確認できます。 不完全な Taskにより、破棄されたコンポーネントのガベージ コレクションが妨げられる場合もあります。
ComponentBaseは、Taskキャンセルによって発生する例外を無視します (より正確には、待機しているが取り消された場合Task無視します)、コンポーネント メソッドはTaskCanceledExceptionとOperationCanceledExceptionを処理する必要はありません。
ComponentBase は、派生コンポーネントの有効な状態を構成するものを概念化せず、それ自体が IDisposable または IAsyncDisposableを実装していないため、上記のガイドラインに従うことはできません。 OnInitializedAsyncがTaskを使用しない不完全なCancellationTokenを返し、Taskが完了する前にコンポーネントが破棄された場合でも、ComponentBaseはOnParametersSetを呼び出し、OnParametersSetAsyncを待機します。 破棄可能なコンポーネントが CancellationTokenを使用していない場合は、 OnParametersSet と OnParametersSetAsync は、コンポーネントが破棄されているかどうかを確認する必要があります。
スレッドをブロックする呼び出しを避ける
一般に、コンポーネントでは次のメソッドは呼び出さないでください。 次のメソッドでは実行スレッドがブロックされます。そのため、基になる Task が完了するまで、アプリの動作が再開されなくなります。
Note
このセクションに示されているスレッド ブロック メソッドを使用する Blazor ドキュメントの例では、推奨されるコーディング ガイダンスとしてではなく、デモンストレーション目的でのみメソッドを使用しています。 たとえば、いくつかのコンポーネント コードのデモでは、Thread.Sleep を呼び出して、実行時間の長いプロセスをシミュレートします。
状態を更新するために外部でコンポーネント メソッドを呼び出す
タイマーやその他の通知などの外部イベントに基づいてコンポーネントを更新する必要がある場合は、InvokeAsync メソッドを使用します。これにより、Blazor の同期コンテキストにコードの実行がディスパッチされます。 たとえば、リッスンしているコンポーネントに、更新状態について通知できる次の ''通知サービス'' を考えてみます。
Update メソッドは、アプリ内のどこからでも呼び出すことができます。
TimerService.cs:
namespace BlazorSample;
public class TimerService(NotifierService notifier,
ILogger<TimerService> logger) : IDisposable
{
private int elapsedCount;
private static readonly TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
private readonly ILogger<TimerService> logger = logger;
private readonly NotifierService notifier = notifier;
private PeriodicTimer? timer;
public async Task Start()
{
if (timer is null)
{
timer = new(heartbeatTickRate);
logger.LogInformation("Started");
using (timer)
{
while (await timer.WaitForNextTickAsync())
{
elapsedCount += 1;
await notifier.Update("elapsedCount", elapsedCount);
logger.LogInformation("ElapsedCount {Count}", elapsedCount);
}
}
}
}
public void Stop()
{
if (timer is not null)
{
timer.Dispose();
timer = null;
logger.LogInformation("Stopped");
}
}
public void Dispose()
{
timer?.Dispose();
// The following prevents derived types that introduce a
// finalizer from needing to re-implement IDisposable.
GC.SuppressFinalize(this);
}
}
namespace BlazorSample;
public class TimerService(NotifierService notifier,
ILogger<TimerService> logger) : IDisposable
{
private int elapsedCount;
private static readonly TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
private readonly ILogger<TimerService> logger = logger;
private readonly NotifierService notifier = notifier;
private PeriodicTimer? timer;
public async Task Start()
{
if (timer is null)
{
timer = new(heartbeatTickRate);
logger.LogInformation("Started");
using (timer)
{
while (await timer.WaitForNextTickAsync())
{
elapsedCount += 1;
await notifier.Update("elapsedCount", elapsedCount);
logger.LogInformation("ElapsedCount {Count}", elapsedCount);
}
}
}
}
public void Dispose()
{
timer?.Dispose();
// The following prevents derived types that introduce a
// finalizer from needing to re-implement IDisposable.
GC.SuppressFinalize(this);
}
}
public class TimerService : IDisposable
{
private int elapsedCount;
private static readonly TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
private readonly ILogger<TimerService> logger;
private readonly NotifierService notifier;
private PeriodicTimer? timer;
public TimerService(NotifierService notifier,
ILogger<TimerService> logger)
{
this.notifier = notifier;
this.logger = logger;
}
public async Task Start()
{
if (timer is null)
{
timer = new(heartbeatTickRate);
logger.LogInformation("Started");
using (timer)
{
while (await timer.WaitForNextTickAsync())
{
elapsedCount += 1;
await notifier.Update("elapsedCount", elapsedCount);
logger.LogInformation("elapsedCount: {ElapsedCount}", elapsedCount);
}
}
}
}
public void Dispose()
{
timer?.Dispose();
}
}
public class TimerService : IDisposable
{
private int elapsedCount;
private static readonly TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
private readonly ILogger<TimerService> logger;
private readonly NotifierService notifier;
private PeriodicTimer? timer;
public TimerService(NotifierService notifier,
ILogger<TimerService> logger)
{
this.notifier = notifier;
this.logger = logger;
}
public async Task Start()
{
if (timer is null)
{
timer = new(heartbeatTickRate);
logger.LogInformation("Started");
using (timer)
{
while (await timer.WaitForNextTickAsync())
{
elapsedCount += 1;
await notifier.Update("elapsedCount", elapsedCount);
logger.LogInformation("elapsedCount: {ElapsedCount}", elapsedCount);
}
}
}
}
public void Dispose()
{
timer?.Dispose();
}
}
using System;
using System.Timers;
using Microsoft.Extensions.Logging;
public class TimerService : IDisposable
{
private int elapsedCount;
private readonly ILogger<TimerService> logger;
private readonly NotifierService notifier;
private Timer timer;
public TimerService(NotifierService notifier, ILogger<TimerService> logger)
{
this.notifier = notifier;
this.logger = logger;
}
public void Start()
{
if (timer is null)
{
timer = new();
timer.AutoReset = true;
timer.Interval = 10000;
timer.Elapsed += HandleTimer;
timer.Enabled = true;
logger.LogInformation("Started");
}
}
private async void HandleTimer(object source, ElapsedEventArgs e)
{
elapsedCount += 1;
await notifier.Update("elapsedCount", elapsedCount);
logger.LogInformation("elapsedCount: {ElapsedCount}", elapsedCount);
}
public void Dispose()
{
timer?.Dispose();
}
}
using System;
using System.Timers;
using Microsoft.Extensions.Logging;
public class TimerService : IDisposable
{
private int elapsedCount;
private readonly ILogger<TimerService> logger;
private readonly NotifierService notifier;
private Timer timer;
public TimerService(NotifierService notifier, ILogger<TimerService> logger)
{
this.notifier = notifier;
this.logger = logger;
}
public void Start()
{
if (timer is null)
{
timer = new Timer();
timer.AutoReset = true;
timer.Interval = 10000;
timer.Elapsed += HandleTimer;
timer.Enabled = true;
logger.LogInformation("Started");
}
}
private async void HandleTimer(object source, ElapsedEventArgs e)
{
elapsedCount += 1;
await notifier.Update("elapsedCount", elapsedCount);
logger.LogInformation("elapsedCount: {ElapsedCount}", elapsedCount);
}
public void Dispose()
{
timer?.Dispose();
}
}
NotifierService.cs:
namespace BlazorSample;
public class NotifierService
{
public async Task Update(string key, int value)
{
if (Notify != null)
{
await Notify.Invoke(key, value);
}
}
public event Func<string, int, Task>? Notify;
}
namespace BlazorSample;
public class NotifierService
{
public async Task Update(string key, int value)
{
if (Notify != null)
{
await Notify.Invoke(key, value);
}
}
public event Func<string, int, Task>? Notify;
}
public class NotifierService
{
public async Task Update(string key, int value)
{
if (Notify != null)
{
await Notify.Invoke(key, value);
}
}
public event Func<string, int, Task>? Notify;
}
public class NotifierService
{
public async Task Update(string key, int value)
{
if (Notify != null)
{
await Notify.Invoke(key, value);
}
}
public event Func<string, int, Task>? Notify;
}
using System;
using System.Threading.Tasks;
public class NotifierService
{
public async Task Update(string key, int value)
{
if (Notify != null)
{
await Notify.Invoke(key, value);
}
}
public event Func<string, int, Task> Notify;
}
using System;
using System.Threading.Tasks;
public class NotifierService
{
public async Task Update(string key, int value)
{
if (Notify != null)
{
await Notify.Invoke(key, value);
}
}
public event Func<string, int, Task> Notify;
}
サービスを登録します。
クライアント側の開発では、サービスをクライアント側の
Programファイルにシングルトンとして登録します。builder.Services.AddSingleton<NotifierService>(); builder.Services.AddSingleton<TimerService>();サーバー側の開発では、サービスをサーバー
Programファイルにスコープ済みとして登録します。builder.Services.AddScoped<NotifierService>(); builder.Services.AddScoped<TimerService>();
NotifierService を使用して、コンポーネントを更新します。
Notifications.razor:
@page "/notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer
<PageTitle>Notifications</PageTitle>
<h1>Notifications Example</h1>
<h2>Timer Service</h2>
<button @onclick="StartTimer">Start Timer</button>
<button @onclick="StopTimer">Stop Timer</button>
<h2>Notifications</h2>
<p>
Status:
@if (lastNotification.key is not null)
{
<span>@lastNotification.key = @lastNotification.value</span>
}
else
{
<span>Awaiting notification</span>
}
</p>
@code {
private (string key, int value) lastNotification;
protected override void OnInitialized() => Notifier.Notify += OnNotify;
public async Task OnNotify(string key, int value)
{
await InvokeAsync(() =>
{
lastNotification = (key, value);
StateHasChanged();
});
}
private void StartTimer() => _ = Task.Run(Timer.Start);
private void StopTimer() => Timer.Stop();
public void Dispose() => Notifier.Notify -= OnNotify;
}
Notifications.razor:
@page "/notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer
<PageTitle>Notifications</PageTitle>
<h1>Notifications Example</h1>
<h2>Timer Service</h2>
<button @onclick="StartTimer">Start Timer</button>
<h2>Notifications</h2>
<p>
Status:
@if (lastNotification.key is not null)
{
<span>@lastNotification.key = @lastNotification.value</span>
}
else
{
<span>Awaiting notification</span>
}
</p>
@code {
private (string key, int value) lastNotification;
protected override void OnInitialized() => Notifier.Notify += OnNotify;
public async Task OnNotify(string key, int value)
{
await InvokeAsync(() =>
{
lastNotification = (key, value);
StateHasChanged();
});
}
private void StartTimer() => _ = Task.Run(Timer.Start);
public void Dispose() => Notifier.Notify -= OnNotify;
}
ReceiveNotifications.razor:
@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer
<h1>Receive Notifications</h1>
<h2>Timer Service</h2>
<button @onclick="StartTimer">Start Timer</button>
<h2>Notifications</h2>
<p>
Status:
@if (lastNotification.key is not null)
{
<span>@lastNotification.key = @lastNotification.value</span>
}
else
{
<span>Awaiting notification</span>
}
</p>
@code {
private (string key, int value) lastNotification;
protected override void OnInitialized()
{
Notifier.Notify += OnNotify;
}
public async Task OnNotify(string key, int value)
{
await InvokeAsync(() =>
{
lastNotification = (key, value);
StateHasChanged();
});
}
private void StartTimer()
{
_ = Task.Run(Timer.Start);
}
public void Dispose()
{
Notifier.Notify -= OnNotify;
}
}
ReceiveNotifications.razor:
@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer
<h1>Receive Notifications</h1>
<h2>Timer Service</h2>
<button @onclick="StartTimer">Start Timer</button>
<h2>Notifications</h2>
<p>
Status:
@if (lastNotification.key is not null)
{
<span>@lastNotification.key = @lastNotification.value</span>
}
else
{
<span>Awaiting notification</span>
}
</p>
@code {
private (string key, int value) lastNotification;
protected override void OnInitialized()
{
Notifier.Notify += OnNotify;
}
public async Task OnNotify(string key, int value)
{
await InvokeAsync(() =>
{
lastNotification = (key, value);
StateHasChanged();
});
}
private void StartTimer()
{
_ = Task.Run(Timer.Start);
}
public void Dispose()
{
Notifier.Notify -= OnNotify;
}
}
ReceiveNotifications.razor:
@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer
<h1>Receive Notifications</h1>
<h2>Timer Service</h2>
<button @onclick="StartTimer">Start Timer</button>
<h2>Notifications</h2>
<p>
Status:
@if (lastNotification.key is not null)
{
<span>@lastNotification.key = @lastNotification.value</span>
}
else
{
<span>Awaiting notification</span>
}
</p>
@code {
private (string key, int value) lastNotification;
protected override void OnInitialized()
{
Notifier.Notify += OnNotify;
}
public async Task OnNotify(string key, int value)
{
await InvokeAsync(() =>
{
lastNotification = (key, value);
StateHasChanged();
});
}
private void StartTimer()
{
Timer.Start();
}
public void Dispose()
{
Notifier.Notify -= OnNotify;
}
}
ReceiveNotifications.razor:
@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer
<h1>Receive Notifications</h1>
<h2>Timer Service</h2>
<button @onclick="StartTimer">Start Timer</button>
<h2>Notifications</h2>
<p>
Status:
@if (lastNotification.key != null)
{
<span>@lastNotification.key = @lastNotification.value</span>
}
else
{
<span>Awaiting notification</span>
}
</p>
@code {
private (string key, int value) lastNotification;
protected override void OnInitialized()
{
Notifier.Notify += OnNotify;
}
public async Task OnNotify(string key, int value)
{
await InvokeAsync(() =>
{
lastNotification = (key, value);
StateHasChanged();
});
}
private void StartTimer()
{
Timer.Start();
}
public void Dispose()
{
Notifier.Notify -= OnNotify;
}
}
前の例の場合:
- タイマーは、Blazor の
_ = Task.Run(Timer.Start)との同期コンテキストの外部で開始されます。 -
NotifierServiceはコンポーネントのOnNotifyメソッドを呼び出します。InvokeAsyncを使用して正しいコンテキストに切り替え、レンダリングをエンキューします。 詳しくは、「ASP.NET Core Razor コンポーネントのレンダリング」をご覧ください。 - コンポーネントでは IDisposable を実装します。
OnNotifyデリゲートのサブスクライブはDisposeメソッドで解除されます。このメソッドは、コンポーネントが破棄されたときにフレームワークによって呼び出されます。 詳細については、ASP.NET Core Razor コンポーネントの破棄を参照してください。
-
NotifierServiceの同期コンテキスト外でOnNotifyからコンポーネントの Blazor メソッドが呼び出されます。InvokeAsyncを使用して正しいコンテキストに切り替え、レンダリングをエンキューします。 詳しくは、「ASP.NET Core Razor コンポーネントのレンダリング」をご覧ください。 - コンポーネントでは IDisposable を実装します。
OnNotifyデリゲートのサブスクライブはDisposeメソッドで解除されます。このメソッドは、コンポーネントが破棄されたときにフレームワークによって呼び出されます。 詳細については、ASP.NET Core Razor コンポーネントの破棄を参照してください。
重要
バックグラウンド スレッドからトリガーされるイベントを Razor コンポーネントで定義している場合、ハンドラーが登録された時点で実行コンテキスト (ExecutionContext) を取り込んで復元することが必要になることがあります。 詳細については、「InvokeAsync(StateHasChanged) を呼び出すと、ページが既定のカルチャにフォールバックする (dotnet/aspnetcore #28521)」を参照してください。
キャッチされた例外をバックグラウンド TimerService からコンポーネントにディスパッチして、例外を通常のライフサイクル イベント例外と同様に処理する方法については、「Razor コンポーネントのライフサイクル外でキャッチされた例外を処理する」セクションを参照してください。
Razor コンポーネントのライフサイクル外でキャッチされた例外を処理する
ComponentBase.DispatchExceptionAsync コンポーネントで Razor を使うと、コンポーネントのライフサイクル呼び出し履歴の外部でスローされた例外を処理できます。 これにより、コンポーネントのコードでは、ライフサイクル メソッドの例外であるかのように例外を処理できます。 その後、Blazor のエラー処理メカニズム (エラー境界など) で例外を処理できます。
Note
ComponentBase.DispatchExceptionAsync は、Razor から継承した .razor コンポーネント ファイル (ComponentBase) で使われます。
implement IComponent directly コンポーネントを作成する場合は、RenderHandle.DispatchExceptionAsync を使います。
Razor コンポーネントのライフサイクル外でキャッチされた例外を処理するには、DispatchExceptionAsync に例外を渡し、その結果を待機します。
try
{
...
}
catch (Exception ex)
{
await DispatchExceptionAsync(ex);
}
前述のアプローチの一般的なシナリオは、コンポーネントが非同期操作を開始するが Task を待機しない場合です。これは、"ファイア アンド フォーゲット" パターンと呼ばれることがよくあります。メソッドが "ファイア" (開始) され、メソッドの結果が "フォーゲット" (破棄) されるからです。 操作が失敗した場合、次のいずれかの目的のために、そのエラーをコンポーネントでコンポーネント ライフサイクルの例外として扱うようにすることができます。
- コンポーネントをエラー状態にする (たとえば、エラー境界をトリガーするため)。
- エラー境界がない場合は回線を終了します。
- ライフサイクル例外の場合と同じログをトリガーする。
次の例では、ユーザーが [レポートの送信] ボタンを選ぶと、レポートを送信するバックグラウンド メソッド ReportSender.SendAsync がトリガーされます。 ほとんどの場合、コンポーネントは非同期呼び出しの Task を待機し、UI を更新して操作が完了したことを示します。 次の例で、SendReport メソッドは Task を待機せず、結果をユーザーに報告しません。 コンポーネントは Task の SendReport を意図的に破棄しているので、非同期エラーは通常のライフサイクル呼び出し履歴の外部で発生します。したがって Blazor には認識されません。
<button @onclick="SendReport">Send report</button>
@code {
private void SendReport()
{
_ = ReportSender.SendAsync();
}
}
エラーをライフサイクル メソッドの例外のように扱うには、次の例で示すように、DispatchExceptionAsync を使って例外をコンポーネントに明示的にディスパッチします。
<button @onclick="SendReport">Send report</button>
@code {
private void SendReport()
{
_ = SendReportAsync();
}
private async Task SendReportAsync()
{
try
{
await ReportSender.SendAsync();
}
catch (Exception ex)
{
await DispatchExceptionAsync(ex);
}
}
}
別のアプローチでは、Task.Run を利用します。
private void SendReport()
{
_ = Task.Run(async () =>
{
try
{
await ReportSender.SendAsync();
}
catch (Exception ex)
{
await DispatchExceptionAsync(ex);
}
});
}
実際のデモでは、「状態を更新するために、コンポーネント メソッドを外部で呼び出す」タイマー通知の例を実装します。
Blazor アプリでは、タイマー通知の例から次のファイルを追加し、セクションの説明に従って Program ファイルにサービスを登録します。
TimerService.csNotifierService.csNotifications.razor
この例では、Razor コンポーネントのライフサイクルの外部でタイマーを使っています。通常、ハンドルされない例外は、Blazorなどの のエラー処理メカニズムによって処理されません。
まず、TimerService.cs のコードを変更して、コンポーネントのライフサイクルの外部に人為的な例外を作成します。
while の TimerService.cs ループで、elapsedCount が値 2 になったときに例外をスローします。
if (elapsedCount == 2)
{
throw new Exception("I threw an exception! Somebody help me!");
}
アプリのメイン レイアウトにエラー境界を配置します。
<article>...</article> マークアップを次のマークアップで置き換えます。
MainLayout.razor:
<article class="content px-4">
<ErrorBoundary>
<ChildContent>
@Body
</ChildContent>
<ErrorContent>
<p class="alert alert-danger" role="alert">
Oh, dear! Oh, my! - George Takei
</p>
</ErrorContent>
</ErrorBoundary>
</article>
エラー境界が静的 Blazor Web App コンポーネントにのみ適用される MainLayout では、境界は静的なサーバー側レンダリング (静的 SSR) フェーズ中にのみアクティブになります。 境界は、コンポーネント階層の下位にあるコンポーネントが対話型であるという理由だけでアクティブになるわけではありません。
MainLayout コンポーネントとその他のコンポーネントに対して広範な対話機能を有効にするには、コンポーネント階層のさらに下の部分で、HeadOutlet コンポーネント (Routes) のAppおよびComponents/App.razorコンポーネント インスタンスの対話型レンダリングを有効にします。 次の例では、対話型サーバー (InteractiveServer) レンダリング モードを採用しています。
<HeadOutlet @rendermode="InteractiveServer" />
...
<Routes @rendermode="InteractiveServer" />
この時点でアプリを実行すると、経過カウントが値 2 に達したときに例外がスローされます。 ただし、UI は変わりません。 エラー境界にはエラー内容が表示されません。
例外をタイマー サービスから Notifications コンポーネントにディスパッチするために、コンポーネントに次の変更が加えられます。
-
try-catchステートメントでタイマーを開始します。catchブロックのtry-catch句では、Exception を DispatchExceptionAsync に渡して結果を待機することで、例外がコンポーネントにディスパッチされます。 -
StartTimerメソッドで、Action の Task.Run デリゲートで非同期タイマー サービスを開始し、返された Task を意図的に破棄します。
StartTimer コンポーネント (Notifications) の Notifications.razor メソッド:
private void StartTimer()
{
_ = Task.Run(async () =>
{
try
{
await Timer.Start();
}
catch (Exception ex)
{
await DispatchExceptionAsync(ex);
}
});
}
タイマー サービスが実行され、カウントが 2 に達すると、例外が Razor コンポーネントにディスパッチされます。これによってエラー境界がトリガーされ、<ErrorBoundary> コンポーネントの MainLayout のエラー コンテンツが次のように表示されます。
ああ! あらららら! - ジョージ武井
ASP.NET Core