Blazor - Display wait or spinner on API call
Solution 1:
Option 1: Using Task.Delay(1)
- Use an async method.
- Use
await Task.Delay(1)
orawait Task.Yield();
to flush changes
private async Task AsyncLongFunc() // this is an async task
{
spinning=true;
await Task.Delay(1); // flushing changes. The trick!!
LongFunc(); // non-async code
currentCount++;
spinning=false;
await Task.Delay(1); // changes are flushed again
}
Option 1 is a simple solution that runs ok but looks like a trick.
Option 2: Using Task.Run() (not for WebAssembly)
On January'2020. @Ed Charbeneau published BlazorPro.Spinkit project enclosing long processes into task to don't block the thread:
Ensure your LongOperation()
is a Task
, if it is not, enclose it into a Task
and await for it:
async Task AsyncLongOperation() // this is an async task
{
spinning=true;
await Task.Run(()=> LongOperation()); //<--here!
currentCount++;
spinning=false;
}
Effect
Spinner and server side prerendering
Because Blazor Server apps use pre-rendering the spinner will not appear, to show the spinner the long operation must be done in OnAfterRender.
Use OnAfterRenderAsync over OnInitializeAsync to avoid a delayed server-side rendering
// Don't do this
//protected override async Task OnInitializedAsync()
//{
// await LongOperation();
//}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await Task.Run(()=> LongOperation());//<--or Task.Delay(0) without Task.Run
StateHasChanged();
}
}
More samples
Learn more about how to write nice spinner you can learn from open source project BlazorPro.Spinkit, it contains clever samples.
More Info
See Henk Holterman's answer with blazor internals explanation.
Solution 2:
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);
}
Solution 3:
Lot's of great discussion surrounding StateHasChanged(), but to answer OP's question, here's another approach for implementing a spinner, universally, for HttpClient calls to a backend API.
This code is from a Blazor Webassembly app...
Program.cs
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<SpinnerService>();
builder.Services.AddScoped<SpinnerHandler>();
builder.Services.AddScoped(s =>
{
SpinnerHandler spinHandler = s.GetRequiredService<SpinnerHandler>();
spinHandler.InnerHandler = new HttpClientHandler();
NavigationManager navManager = s.GetRequiredService<NavigationManager>();
return new HttpClient(spinHandler)
{
BaseAddress = new Uri(navManager.BaseUri)
};
});
await builder.Build().RunAsync();
}
SpinnerHandler.cs
Note: Remember to uncomment the artificial delay. If you use the out-of-the-box Webassembly template in Visual Studio, click the Weather Forecast to see a demo of the spinner in action.
public class SpinnerHandler : DelegatingHandler
{
private readonly SpinnerService _spinnerService;
public SpinnerHandler(SpinnerService spinnerService)
{
_spinnerService = spinnerService;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
_spinnerService.Show();
//await Task.Delay(3000); // artificial delay for testing
var response = await base.SendAsync(request, cancellationToken);
_spinnerService.Hide();
return response;
}
}
SpinnerService.cs
public class SpinnerService
{
public event Action OnShow;
public event Action OnHide;
public void Show()
{
OnShow?.Invoke();
}
public void Hide()
{
OnHide?.Invoke();
}
}
MainLayout.razor
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<div class="top-row px-4">
<a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
</div>
<div class="content px-4">
@Body
<Spinner />
</div>
</div>
</div>
Spinner.razor
Note: To add some variety, you could generate a random number in the OnIntialized() method, and use a switch statement inside the div to pick a random spinner type. In this method, with each HttpClient request, the end user would observe a random spinner type. This example has been trimmed to just one type of spinner, in the interest of brevity.
@inject SpinnerService SpinnerService
@if (isVisible)
{
<div class="spinner-container">
<Spinner_Wave />
</div>
}
@code
{
protected bool isVisible { get; set; }
protected override void OnInitialized()
{
SpinnerService.OnShow += ShowSpinner;
SpinnerService.OnHide += HideSpinner;
}
public void ShowSpinner()
{
isVisible = true;
StateHasChanged();
}
public void HideSpinner()
{
isVisible = false;
StateHasChanged();
}
}
Spinner-Wave.razor
Credit to: https://tobiasahlin.com/spinkit/
Note: There is a Nuget package for this spin kit. The drawback to the Nuget package is that you don't have direct access to the CSS to make tweaks. Here I've tweaked thee size of the spinner, and set the background color to match the site's primary color, which is helpful if you are using a CSS theme throughout your site (or perhaps multiple CSS themes)
@* Credit: https://tobiasahlin.com/spinkit/ *@
<div class="spin-wave">
<div class="spin-rect spin-rect1"></div>
<div class="spin-rect spin-rect2"></div>
<div class="spin-rect spin-rect3"></div>
<div class="spin-rect spin-rect4"></div>
<div class="spin-rect spin-rect5"></div>
</div>
<div class="h3 text-center">
<strong>Loading...</strong>
</div>
<style>
.spin-wave {
margin: 10px auto;
width: 200px;
height: 160px;
text-align: center;
font-size: 10px;
}
.spin-wave .spin-rect {
background-color: var(--primary);
height: 100%;
width: 20px;
display: inline-block;
-webkit-animation: spin-waveStretchDelay 1.2s infinite ease-in-out;
animation: spin-waveStretchDelay 1.2s infinite ease-in-out;
}
.spin-wave .spin-rect1 {
-webkit-animation-delay: -1.2s;
animation-delay: -1.2s;
}
.spin-wave .spin-rect2 {
-webkit-animation-delay: -1.1s;
animation-delay: -1.1s;
}
.spin-wave .spin-rect3 {
-webkit-animation-delay: -1s;
animation-delay: -1s;
}
.spin-wave .spin-rect4 {
-webkit-animation-delay: -0.9s;
animation-delay: -0.9s;
}
.spin-wave .spin-rect5 {
-webkit-animation-delay: -0.8s;
animation-delay: -0.8s;
}
@@-webkit-keyframes spin-waveStretchDelay {
0%, 40%, 100% {
-webkit-transform: scaleY(0.4);
transform: scaleY(0.4);
}
20% {
-webkit-transform: scaleY(1);
transform: scaleY(1);
}
}
@@keyframes spin-waveStretchDelay {
0%, 40%, 100% {
-webkit-transform: scaleY(0.4);
transform: scaleY(0.4);
}
20% {
-webkit-transform: scaleY(1);
transform: scaleY(1);
}
}
</style>
It's beautiful
Solution 4:
To answer the notice in @daniherrera's solution, there is three more elegant solution proposed here.
In short :
-
Implement
INotifyPropertyChanged
to the Model and invokeStateHasChanged()
on aPropertyChangedEventHandler
event property from the Model. -
Use delegates to invoke
StateHasChanged()
on the Model. -
Add a
EventCallBack<T>
parameter to the component or page of the View and assign it to the function that should change the render of the component and their parents. (StateHasChanged()
isn't necessary in this one`)
The last option is the most simple, flexible and high level, but choose at your convenience.
Overall, I'll advise to use one of those solutions presented more than the await Task.Delay(1);
one if security of your app is a concern.
Edit : After more reading, this link provide a strong explanation on how to handle events in C#, mostly with EventCallBack
.