44

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();
    }
}
Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
David23g
  • 443
  • 1
  • 4
  • 6

8 Answers8

69

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

dani herrera
  • 48,760
  • 8
  • 117
  • 177
  • Which version of Blazor / the browser are you using for this. On both FireFox (v60.5.0esr) and Chrome (75.0.3770.142) with Blazor (installed about a week ago, should be preview v6) this view results in a browser that hangs. – tobriand Jul 29 '19 at 11:16
  • @tobriand, just tested on blazor server side preview7 and still running. – dani herrera Jul 29 '19 at 11:52
  • I wonder if it's that it's server side rather than client. Might be that the runtime in use for server-side is more robust to async-with-tasks vs async-with-IO, e.g. if it's "real" .Net Core. I'm running this client-side (and trying to suss out exactly what Blazor needs to be given in order to yield cleanly mid long-running-operation). Will try with preview 7 though and see... – tobriand Jul 29 '19 at 13:30
  • @tobriand, maybe issue is in client side with task delay wait. Just call your long running operations instead fake demo operation. – dani herrera Jul 29 '19 at 13:42
  • 2
    thank you, thank you, thank you. so glad I found this. – portia Oct 09 '19 at 21:52
  • 1
    I have been searching for hours and `Task.Run` is what I was missing while returning data from an Entity Framework DBContext, in an async function. Thank you! – chrisbyte Feb 28 '20 at 20:43
  • Thanks a lot @HenkHolterman about edit the post. Be free to improve it if needed. – dani herrera Apr 29 '20 at 13:31
  • 1
    Your own last edit making it two options is a lot better. I'll make some small touch-ups. – H H May 02 '20 at 21:22
  • Hi @HenkHolterman, thanks about your answer improvements. I appreciate that. – dani herrera May 02 '20 at 21:57
  • Of course @HenkHolterman, always learning from you! Some day I will understand async internals. – dani herrera May 02 '20 at 22:36
  • 1
    Great answer. Especially the part about OnAfterRender in server-side blazor projects. Thanks a lot. – n-develop Nov 17 '20 at 21:14
  • @n-decelop, credits to [Ed Charbeneau](https://stackoverflow.com/users/757937/ed-charbeneau) – dani herrera Nov 17 '20 at 21:39
  • Option 1 on Blazor Server Pre-rendered. Thank you! – GisMofx Jan 10 '21 at 04:20
  • 1
    You have no idea how glad I am to have found this... – Andreas Forslöw Apr 16 '21 at 09:20
  • hmm.. I think the second implementation is hackier then the first one. see [Task.Run Etiquette Examples - Stephen Cleary](https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html). and subsequent [Should I expose asynchronous wrappers for synchronous methods? - Stephen Toub](https://devblogs.microsoft.com/pfxteam/should-i-expose-asynchronous-wrappers-for-synchronous-methods/). this approach using `await Task.Run(Func)` seems to rely on the 'unexpected' nature of handling the task (the overhead;probably context switch) to the desired affect. – Brett Caswell Jun 24 '21 at 05:08
  • also, looking back through your edit/revisions of this answer, your characterizations aren't really well qualified. (i.e. "wrong way", "right way", "trick") .. there is no telling how many people implemented the 'right' way or steered clear of 'trick' option on this matter. So as a general "improve your answer" comment, consider removing and refraining using unqualified characterizations in your answers. i.e. how is `await Task.Yield()` a trick? why was it the "wrong" way? – Brett Caswell Jun 24 '21 at 05:44
  • @BrettCaswell, thanks about your feedback, appreciate. I will keep in mind. Also, be free to improve the answer in any way to help people with this issue. – dani herrera Jun 24 '21 at 06:36
  • Can I get a version of this answer that checks to see if the `LongOperation()` or `LongOperationAsync()` is still not completed in under 500 milliseconds, and if so, THEN display the loading icon? I don't want to display it immediately especially if the data is coming back from cache and it causes a quick loading flash and is a little visually awkward. Thanks. – Jason Ayer Jul 18 '22 at 17:43
  • FYI - Henk Holterman's answer just links back to this page. I was really hoping to understand the blazor internals here. – Scuba Steve Jun 30 '23 at 20:17
  • @ScubaSteve answer was removed. Thanks. – dani herrera Jul 01 '23 at 08:42
9

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

Animation of result

Updates 2023:

In practice I've found it useful to have the SpinnerService listen for LocationChanged events from the NavigationManager. In this way, if the user navigates away from the page having the wait spinner, it can be appropriately hidden, and not carry over to the next page.

**SpinnerService.cs**

internal sealed class SpinnerService : ISpinnerService, IDisposable
{
    private readonly NavigationManager _navMan;
    private bool _holdSpinner = false;

    public SpinnerService(NavigationManager navigationManager)
    {
        _navMan = navigationManager;
        _navMan.LocationChanged += LocationChanged;
    }

    public event Action? OnShow;
    public event Action? OnHide;

    public void Show()
    {
        OnShow?.Invoke();
    }

    public void Hide()
    {
        OnHide?.Invoke();
    }

    private void LocationChanged(object? sender, LocationChangedEventArgs e)
    {
        Hide();
    }

    public void Dispose()
    {
        _navMan.LocationChanged -= LocationChanged;
    }
}

Also, in the Spinner.razor, it is best practice to implement IDisposable and to unsubscribe from the SpinnerService's events.

**Spinner.razor**

@inject SpinnerService SpinnerService
@implements IDisposable

@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();
    }

    public void Dispose()
    {
        SpinnerService.OnShow -= ShowSpinner;
        SpinnerService.OnHide -= HideSpinner;
    }
}
Brian
  • 315
  • 5
  • 10
  • 1
    Awesome post! This was almost exactly what I was looking for, as I wanted to add a "loading/processing" indicator to an existing project, but didn't want to have to modify every page/component to add "spinner" code. The only issue I seem to be running into is when there are multiple simultaneous calls to the backend API and some calls take longer than others. With this code, the spinner will be hidden whenever the first call finishes. I think I can probably tweak it a bit to "know" if there's a call still being processed before hiding it though. Thanks again! – Redwing19 Sep 17 '21 at 16:55
  • @Redwing19, glad I could help and appreciate the enthusiasm. Regarding multiple parallel API requests, it's situational dependent. I watered down this answer so folks could customize as needed. For my use, I solved the multiple request problem by adding a boolean full-property to the SpinnerService, to act as an override for not turning off the spinner when Hide() is called. When I'm done loading data, my component calls the property's set method, which will call Hide() and then turn off the spinner. But that's just one example. If you come up with something more generic, please share. – Brian Sep 20 '21 at 04:35
  • 2
    thanks for the info! I wound up adding a static int _callCounter member variable to the SpinnerHandler class. It's incremented before calling SendAsync, and decremented after. And then, I only call _spinnerService.Hide() if the _callCounter is 0. It's not perfect, as it will flicker a bit if there are multiple calls done in succession and the 1st call is completed before the 2nd call begins, but it works great for concurrent calls and serves my needs well. – Redwing19 Sep 20 '21 at 23:40
  • @Redwing19 I know it's been a while since this comment, but could you share a gist of public repo for how you implemented your solution? – Chris Langston Mar 24 '23 at 20:26
  • Here is a blog post with an almost identical implementation and a repo :) https://bipinpaul.com/posts/display-spinner-on-each-api-call-automatically-in-blazor – VinKel Apr 26 '23 at 14:26
2

To answer the notice in @daniherrera's solution, there is three more elegant solution proposed here.

In short :

  • Implement INotifyPropertyChanged to the Model and invoke StateHasChanged() on a PropertyChangedEventHandler 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.

PepperTiger
  • 564
  • 3
  • 8
  • 21
  • 2
    "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." what do you mean? how do these renderer mechanism differ as it relates to security? – Brett Caswell Jun 24 '21 at 06:05
2

Not just for API call, but for every service call:

SpinnerService:

 public class SpinnerService
 {
    public static event Action OnShow;
    public static event Action OnHide;

    public void Show()
    {
        OnShow?.Invoke();
    }

    public void Hide()
    {
        OnHide?.Invoke();
    }
}

Spinner component:

Your spinner hier, in my case I have MudProgressCircular

@if (IsVisible)
{
    <MudProgressCircular Color="Color.Primary"
                         Style="position: absolute;top: 50%;left: 50%;"
                         Indeterminate="true" />
}

@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();
    }
}

ServiceCaller:

public class ServiceCaller
{
    private readonly IServiceProvider services;
    private readonly SpinnerService spinnerService;

    public ServiceCaller(IServiceProvider services, SpinnerService spinnerService)
    {
        this.services = services;
        this.spinnerService = spinnerService;
    }

    public async Task<TResult> CallAsync<TService, Task<TResult>>(Func<TService, TResult> method)
        where TService : class
    {
        var service = this.services.GetRequiredService<TService>();

        try
        {
            spinnerService.Show();

            await Task.Delay(500); // ToDo: this line is not necessary

            TResult? serviceCallResult = await Task.Run(() => method(service));

            return serviceCallResult;
        }
        finally
        {
            spinnerService.Hide();
        }
    }

     public async Task CallAsync<TService, TAction>(Func<TService, Action> method)
        where TService : class
    {
        var service = this.services.GetRequiredService<TService>();

        try
        {
            spinnerService.Show();

            await Task.Delay(500); // ToDo: this line is not necessary

            await Task.Run(() => method(service).Invoke());
        }
        finally
        {
            spinnerService.Hide();
        }
    }
}

How to use it?

@page "/temp"

@inject ServiceCaller serviceCaller;

<h3>Temp Page</h3>

<MudButton OnClick="CallMethodReturnsString">CallMethodReturnsString</MudButton>

<MudButton OnClick="CallVoidMethodAsync">CallVoidMethodAsync</MudButton>

<MudButton OnClick="CallTaskMethodAsync">CallTaskMethodAsync</MudButton>

<MudButton OnClick="CallMany">CallMany</MudButton>


@if (!string.IsNullOrEmpty(tempMessage)){
    @tempMessage
}

@code{
    string tempMessage = string.Empty;

    // call method returns string
    private async Task CallMethodReturnsString()
    {
        await serviceCaller.CallAsync<ITempService, string>(async x => this.tempMessage = await x.RetrieveStringAsync());
    }

    // call void method
    private async Task CallVoidMethodAsync()
    {
        await serviceCaller.CallAsync<ITempService, Task>(x => () => x.MyVoidMethod());
    }

    // call task method
    private async Task CallTaskMethodAsync()
    {
        await serviceCaller.CallAsync<ITempService, Task>(x => () => x.TaskMethod());
    }

    // call many methods
    private async Task CallMany()
    {
        await serviceCaller.CallAsync<ITempService, Action>(x => async () =>
        {
            this.tempMessage = await x.RetrieveStringAsync();
            x.MyVoidMethod();
            x.TaskMethod();
        });
    }
}
Husam Ebish
  • 4,893
  • 2
  • 22
  • 38
1

Don't do the same mistake as I did by testing wait spinner using Thread.Sleep(n).

protected override async Task OnInitializedAsync()
{
    // Thread.Sleep(3000); // By suspending current thread the browser will freeze.
    await Task.Delay(3000); // This is your friend as dani herrera pointed out. 
                      // It creates a new task that completes 
                      // after a specified number of milliseconds.

    forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
}
Matt
  • 3,793
  • 3
  • 24
  • 24
albin
  • 607
  • 6
  • 10
  • It does not seem to work. I get the warning "Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call." – Rye bread Nov 11 '19 at 15:15
  • Hi, I quess you did not await the call --> await SomethingToAwait.SomeAsyncMethod() – albin Nov 14 '19 at 07:34
  • @rugbrød, the warning is suggesting that you need to await "Task.Delay(3000)", otherwise the delay won't happen. I edited the example to fix this warning. – Matt Nov 18 '19 at 06:21
0

use InvokeAsync(StateHasChanged), hopefully it will work.

protected async void Submit()
    {
        //Show Sending...
        cursorCSS = "";
        this.StateHasChanged();
        response = Service.Post(userModel);
        if (response.Errors.Any())
        {
            errorCss = "errorOn";
        }
        //turn sending off
        cursorCSS = "cursorSpinOff";
        await InvokeAsync(StateHasChanged);
    }
-1

Blazor Serverside - I needed to call StateHasChanged() to force the frontend to update so the spinner would show before the code moves onto the ajax call.

/* Show spinner */
carForm.ShowSpinner = true;

/* Force update of front end */
StateHasChanged();

/* Start long running API/Db call */
await _carRepository.Update(item);
  • Did not work for me in Blazor Server. I needed dani's option 1. It is like the StateHasChanged does not register during an async operation. – Steve Greene Jun 23 '21 at 15:25
  • if calling to `StateHasChanged` (which "force" is probably the wrong term, it essentially indicates/signals 'dirty' state) was a consideration here, then it is likely your answer isn't related to this question's scenario or concern per say. You probably needed to call to `StateHasChanged` because `_carRepository.Update` was not the first or last task completed in this scope. ref [Multiple Asynchronous Phases in an Asynchronous Handler](https://docs.microsoft.com/en-us/aspnet/core/blazor/components/rendering?view=aspnetcore-5.0#an-asynchronous-handler-involves-multiple-asynchronous-phases) – Brett Caswell Jun 24 '21 at 06:22
-1

Easy to use at net7:

  • create model SpinnerModel.cs

     Class SpinnerModel
        Public bool IsShow {get set}
        // add two actions to set property to true and false
    
  • create razor component Spinner.razor

    • add some block with information about waiting
    • add created model as [parameter] to block @code
    • wrap this markup into @if-statement to check property of model

At your some view, add and init field of spinner-model, add the component of spinner to markup and bind field as model parameter at spinner-component.

And them you can set property of field to true and false (or invoke added actions to change binded property) at handlers (for example, at button-click handlers before and after async operation)

It does not working with "oninitialize", but works very well at another custom handlers.

And you can not use some strange code (for example, wraping async operations to "Task.Run")

P.s. sorry, i wrote it via mobile

Alexey
  • 1
  • 1