Question

Display wait or spinner on API call

In my Blazor app I am making an API call to a back end server that could take some time. I need to display feedback to the user, a wait cursor or a "spinner" image. How is this done in Blazor?

I have tried using CSS and turning the CSS on and off but the page is not refreshed until the call is completed. Any suggestions would be greatly appreciated.

@functions {
    UserModel userModel = new UserModel();
    Response response = new Response();
    string errorCss = "errorOff";
    string cursorCSS = "cursorSpinOff";

    protected void Submit()
    {
        //Show Sending...
        cursorCSS = "";
        this.StateHasChanged();
        response = Service.Post(userModel);
        if (response.Errors.Any())
        {
            errorCss = "errorOn";
        }
        //turn sending off
        cursorCSS = "cursorSpinOff";
        this.StateHasChanged();
    }
}
 46  56073  46
1 Jan 1970

Solution

 74

Option 1: Using Task.Delay(1)

  • Use an async method.
  • Use await Task.Delay(1) or await 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

a spinner loading data

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

2019-06-19

Solution

 12

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.

  1. 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.

  1. 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);
}
2020-05-02