In addition to @dani's answer here I'd like to point out that there are two separate problems here, and it pays to separate them.
- When to call
StateHasChanged()
Blazor will (conceptually) call StateHasChanged() after initialization and before and after events. That means you usually don't need to call it, only when your method has several distinct steps and you want to update the UI in the middle do you need to call it. And that is the case with a spinner.
You do need to call it when you use fire-and-forget (async void
) or when changes come from a different source, like a Timer or events from another layer in your program.
- How to make sure the UI is updated after calling
StateHasChanged()
StateHasChanged() by itself does not update the UI. It merely queus a render operation. You can think of it as setting a 'dirty flag'.
Blazor will update the UI as soon as the render engine gets to run on its Thread again. Much like any other UI framework all UI operations have to be done on the main thread. But your events are also running (initially) on that same thread, blocking the renderer.
To resolve that, make sure your events are async by returning async Task
. Blazor fullly supports that. Do not use async void
. Only use void
when you do not need async behaviour.
2.1 Use an async operation
When your method awaits an async I/O operation quickly after StateHasChanged() then you are done. Control will return to the Render engine and your UI will update.
statusMessage = "Busy...";
StateHasChanged();
response = await SomeLongCodeAsync(); // show Busy
statusMessage = "Done.";
2.2 Insert a small async action
When your code is CPU intensive it will not quickly release the main thread. When you call some external code you don't always know 'how async' it really is. So we have a popular trick:
statusMessage = "Busy...";
StateHasChanged();
await Task.Delay(1); // flush changes - show Busy
SomeLongSynchronousCode();
statusMessage = "Done.";
the more logical version of this would be to use Task.Yield()
but that tends to fail on WebAssembly.
2.3 Use an extra Thread with Task.Run()
When your eventhandler needs to call some code that is non-async, like CPU-bound work, and you are on Blazor-Server you can enlist an extra pool Thread with Task.Run() :
statusMessage = "Busy...";
StateHasChanged();
await Task.Run( _ => SomeLongSynchronousCode()); // run on other thread
statusMessage = "Done.";
When you run this on Blazor-WebAssembly it has no effect. There are no 'extra threads' available in the Browser environment.
When you run this on Blazor-Server you should be aware that using more Threads may harm your scalability. If you plan to run as many concurrent clients as possible on a server then this is a de-optimization.
When you want to experiment:
void SomeLongSynchronousCode()
{
Thread.Sleep(3000);
}
Task SomeLongCodeAsync()
{
return Task.Delay(3000);
}