Blazor とイベントハンドラの async Task の話

去年執筆した「Blazor入門 第2版」 https://amzn.asia/d/03cj9KQV なのですが、あまりにも評判が悪いので梃入れです。


基本的に、初版の Blazor が wasm ベースの SPA だったのに対して、第2版は Next/Nuxt.js のように SSR(サーバーサイドレンダリング)にシフトした本になっています。基本的なサンプルコードはそのままにして、CSR/SSR 両方でできる技術を引き続きできるとした本なわけですが、そのあたり Blazor があまり広まっていないのか、ちょっと不明です。

async void の問題

先日の amazon の評で気づいたのですが、Blazor の方では async Task を使うことになっていたのを失念していました。

ASP.NET Core Blazor のイベント処理 | Microsoft Learn https://learn.microsoft.com/ja-jp/aspnet/core/blazor/components/event-handling

ボタンをクリックしたときにイベントハンドラは、戻り値が void で定義されているので、戻りの型を変更するおとができなくて、非同期デリゲートを使う時は WinForm なんかは async void を使うので、そのまま Blazor のサンプルコードに持ってきたのですが、

■重要

Blazor フレームワークでは、void を返す非同期メソッド (async) は追跡されません。 その結果、 void が返された場合に例外がキャッチされないと、プロセス全体が失敗します。 非同期メソッドから常に Task/ValueTask を返します。

ということになっていますね。書籍のほうでは、

/// <summary>
/// 検索
/// </summary>
private async void onSearch()
{
    var items = await Http.GetFromJsonAsync<List<Books>>("api/books");
    if (items == null) return;
    this.books = items;
    this.StateHasChanged();
}

と async void を使っていますが、次のように async Task を使います。

/// <summary>
/// 検索
/// </summary>
private async Task onSearch()
{
    var items = await Http.GetFromJsonAsync<List<Books>>("api/books");
    if (items == null) return;
    this.books = items;
    this.StateHasChanged();
}

このあたり、github https://github.com/moonmile/blazor-sample-v2 のサンプルコードを直しておきました。

ちなみに、WinForms の場合、次のように async Task を使うとコンパイル時にエラーになります。

private async Task button1_Click(object sender, EventArgs e)
{
    await Task.Delay(1000);
}

‘Task Form1.button1_Click(object, EventArgs)’ には、不適切な戻り値の型が指定されています

これはイベントハンドラの定義が public delegate void EventHandler(object? sender, EventArgs e); となっているので void しか受け付けないようになっているためです。これは、WPF の場合も同じですね。

このため Blazor 以外では async void を使わないといけないのですが、いま、Visual Studio 2026 でコーディング状態を確認すると、クリックイベントで await を使ったときに自動的に「async Task」に書き変えられす。これが正しい動作なのかは不明なのですが、すくなくとも WinForms でのクリックイベントでは「async Task」のままだとコンパイルエラーになります。

なので Blazor だけ、という注意が必要です。

async Task と async void の動作を確認する

Blazor の簡単な画面を使って、2つの動作を確認してみましょう。ドキュメントには「非同期メソッド(async)が追跡されません」とありますが、現象的にどんなことが起こるのか?ということです。書籍のサンプルを作ったときには、Web API の呼び出しを await で待って画面に表示していたので問題が出ていなかったのですが、次のコードだと2つの書き方で動作が顕著にでるようです。

private int currentCount = 0;

private async Task IncrementCount()
{
    await Task.Delay(2000);
    currentCount++;
    System.Diagnostics.Debug.WriteLine($"async Task: {currentCount}");
}
private async void IncrementCountVoid()
{
    await Task.Delay(2000);
    currentCount++;
    System.Diagnostics.Debug.WriteLine($"async void: {currentCount}");
}

ボタンをクリックして2秒後に currentCount を更新して、画面を再更新します。

左の「async Task 版」のボタンをクリックすると、画面の Current count の数値と、数値の出力の数値が同期しています。ボタンをクリックして2秒後に表示している状態ですが、これが当たり前の状態です。

これを右の「async void 版」のボタンをクリックすると、画面の Current count の数値と出力の数値がずれます。

どうやら画面を更新する StateHasChanged が内部で飛んでいないらしくて、画面の更新が遅れているようです。

この動作は以下のように明示的に StateHasChanged メソッドを呼び出すようにすれば解決できるのですが、そもそも async void でデリゲートの呼び出しがうまくいかなくて画面更新ができていないので、正しい書き方の async Task を使えということです。

この動作は以下のように明示的に StateHasChanged メソッドを呼び出すようにすれば解決できるのですが、そもそも async void でデリゲートの呼び出しがうまくいかなくて画面更新ができていないので、正しい書き方の async Task を使えということです。

private async void IncrementCountVoid()
{
    await Task.Delay(2000);
    currentCount++;
    System.Diagnostics.Debug.WriteLine($"async void: {currentCount}");
    this.StateHasChanged();
}

ちなみに、非同期イベントハンドラの生成は @click=”…” のほうからも作成できます。

先にクリックイベント名を書いてから、Alt+Enter で「考えられる修正内容を表示」させます。

こうすると戻り値が Task のほうのイベントハンドラが作成されるので、適宜 await/async を追加すれば ok です。

ちなみに「C#ユーザーのためのWebアプリ開発パターン ASP.NET Core Blazorによるエンタープライズアプリ開発」https://amzn.asia/d/00yl4hs4 はひと通り読んだ上で、被らないように Blazor 第2版を執筆していたのですが… async Task は見落としでした orz

補足

どうやら Blazor の場合は、.NET 側から JS 側にイベントを通知するために @onclick が非同期のデリゲート Func<T> として定義されている模様。確かに、WinForms や WPF の場合は、UI からのイベントに対して同期的に行うので、EventHandler 当たりを使って async void になるのだが、Blazor の場合は .NET -> JS への通知がそもそも非同期なので async Task を使わないと待ちが発生するような「同期的な処理」ができないということなのだろう。

このあたりは Blazor 特有の動作なので、見落としやすいところ。

カテゴリー: 開発 パーマリンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

*