Pozor na await periodicTimer.WaitForNextTickAsync(). Tato metoda láká svojí asynchronní signaturou k pohodlnému uspořádání periodických úloha a zejména v Blazoru vás tak může zlákat k implementaci aktualizací UI:
protected override async Task OnInitializedAsync()
{
await StartTimerAsync();
}
private async Task StartTimerAsync()
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(10));
while (await timer.WaitForNextTickAsync())
{
// do some UI updates here
}
}
POZOR! Takovéto uspořádání sice neblokuje UI thread a díky async-await to jede dál, problém je však v tom, že metoda, z které se takovýto kód volá, nikdy neskončí.
Pokud tedy například takovýto StartTimeAsync() přímo zavolám z OnIntializeAsync/OnParametersSetAsync/OnAfterRenderAsync/action-callbacku, pak tato parent-metoda nikdy neskončí a může to mít dost nečekané následky. Například:
- pokud to zavolám z
HxButton.OnClickobsluhy tlačítka, zůstane mi tam viset spinner, který nikdy neskončí,- tlačítko navíc zůstane pod single-click-protection, disabled, nepoužitelné,
- pokud to zavolám z
override OnInitializedAsync(), nedojde v prvním roundtripu k zavoláníOnParametersSet[Async]()a dokud nepřijde další roundtrip, tak se miOnParametersSet[Async]()neprovedou, - pokud to zavolám z
override OnParametersSetAsync(), zůstává mi viset nedokončenýTaskvComponentBase.CallStateHasChangedOnAsyncCompletion(), navíc musím ošetřit, že seOnParametersSetAsync()volá opakovaně a nechci si timerů pustit více, - pokud to zavolám
z override OnAfterRenderAsync(bool firstRender), mohu si tím zablokovat voláníawait base.OnAfterRenderAsync(firstRender), což mi může narušit funkčnost definovanou předkem při dědění (zejména profirstRender = true, které se provádí jen jednou)
PeriodicTimer.WaitForNextTickAsync() se tedy hodí do scénářů, kdy je zaručeno, že volající kód nikam nepokračuje a mohu se na příslušném místě točit „do nekonečna“ (např. jsem v metodě Main a točím nějakou cyklickou úlohu v konzolové aplikaci, nebo jsem v BackgroundService.ExecuteAsync()). Obecně však je potřeba dovolit dokončení volající metody a tedy pro spouštění použít tradiční uspořádání s Task.Run(..), kdy umístíme timer (může to být i klasický Timer, asynchronní metoda WaitForNextTickAsync() zde již nehraje tak velkou roli) na ThreadPool a neawaitujeme v aktuální metodě jeho dokončení (fire & forget). V případě Blazoru si v takovém případě musíme ručně volat StateHasChanged(), popř. DispatchExceptionAsync().
public MyComponent : IDisposable
{
private PeriodicTimer timer;
protected override async Task OnInitializedAsync()
{
_ = Task.Run(StartTimerAsync);
}
private async Task StartTimerAsync()
{
timer = new PeriodicTimer(TimeSpan.FromSeconds(10));
while (await timer.WaitForNextTickAsync())
{
// do some UI updates here
StateHasChanged(); // as needed
}
}
public void Dispose()
{
timer?.Dispose();
}
}
Nesmíme samozřejmě zapomenout na úklid – timer.Dispose(), jinak nám Timer zůstane běžet i po zániku komponenty, vzniká resource-leak atd.
Viz též ASP.NET Core Blazor dokumentace: