blazorblazor-server-sidebunit

How can I turn on/off a Loading… while a page initializes in Blazor?


I want to put up a Loading… overlay while a page is loading. If everything was in my razor page then I could make it visible as the first line in OnInitializedAsync() and hide it as the last line.

But I have children components in the page. And as they all also have their own OnInitializedAsync(), and that is async for all of them, they complete in a random order.

So, for making the overlay visible, is the containing page’s OnInitializedAsync the first to be called?

And is OnAfterRenderAsyncwhere I should then hide it? Or can I do so in OnInitialized (no Async)? Or somewhere else?

I need this not only for the UI letting the user know the page is loading, but also for my bUnit tests to have it WaitForState() until the page is fully rendered. I can test for the IsLoading property to be false.


Solution

  • Here's a demo to show how the render sequence works and how the LoadingOverlay can work with sub-components.

    Take the LoadingOverlay from the other answer:

    <div class="@_css">
        <div class="container text-center">
            <div class="alert alert-warning m-5 p-5">We're experiencing very high call volumes today [nothing out of the norm now]. 
                You're at call position 199.  You're business, not you, is important to us!</div>
        </div>
    </div>
    @if(this.ChildContent is not null)
    {
        @this.ChildContent
    }
    @code {
        private string _css => this.IsLoading ? "loading" : "loaded";
    
        [Parameter] public bool IsLoading { get; set; }
        [Parameter] public RenderFragment? ChildContent { get; set; }
    }
    

    Add the Blazr.BaseComponents package to the project. DocumentedComponentBase is used as the base component for the components to document their lifecycle and event sequences.

    Add WeatherList.Razor

    @inherits Blazr.BaseComponents.ComponentBase.DocumentedComponentBase
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in _forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
    <LoadingOverlay IsLoading="_loading" />
    
    
    @code {
        [Parameter] public IEnumerable<WeatherForecast>? Forecasts { get; set; }
    
        private bool _loading = true;
        private IEnumerable<WeatherForecast> _forecasts = Enumerable.Empty<WeatherForecast>();
    
        protected override async Task OnInitializedAsync()
        {
            await Task.Delay(1000);
            _forecasts = this.Forecasts ?? Enumerable.Empty<WeatherForecast>();
            _loading = false;
        }
    }
    

    Update FetchData:

    @page "/fetchdata"
    @inject WeatherForecastService ForecastService
    @inherits Blazr.BaseComponents.ComponentBase.DocumentedComponentBase
    
    <PageTitle>Weather forecast</PageTitle>
    <LoadingOverlay IsLoading="_loading" />
    
    
    <h1>Weather forecast</h1>
    
    <p>This component demonstrates fetching data from a service.</p>
    
    @if (forecasts == null)
    {
        <p><em>Loading...</em></p>
    }
    else
    {
            <WeatherList Forecasts="forecasts" />
    }
    
    @code {
        private WeatherForecast[]? forecasts;
        private bool _loading => forecasts == null;
    
        protected override async Task OnInitializedAsync()
        {
            await Task.Delay(1000);
    
            forecasts = await ForecastService.GetForecastAsync(DateOnly.FromDateTime(DateTime.Now));
        }
    }
    

    If you now run this code you will see the following output to the console by DocumentedComponentBase:

    Note that FetchData runs it's OnInitialized sequence to completion and renders and runs it's first OnAfterRenderAsync before WeatherList is even created.

    ===========================================
    4755 - FetchData => Component Initialized
    4755 - FetchData => Component Attached
    4755 - FetchData => SetParametersAsync Started
    4755 - FetchData => OnInitialized sequence Started
    4755 - FetchData => Awaiting Task completion
    4755 - FetchData => StateHasChanged Called
    4755 - FetchData => Render Queued
    4755 - FetchData => Component Rendered
    4755 - FetchData => OnAfterRenderAsync Started
    4755 - FetchData => OnAfterRenderAsync Completed
    4755 - FetchData => OnInitialized sequence Completed
    4755 - FetchData => OnParametersSet Sequence Started
    4755 - FetchData => StateHasChanged Called
    4755 - FetchData => Render Queued
    4755 - FetchData => Component Rendered
    ===========================================
    d15e - WeatherList => Component Initialized
    d15e - WeatherList => Component Attached
    d15e - WeatherList => SetParametersAsync Started
    d15e - WeatherList => OnInitialized sequence Started
    d15e - WeatherList => Awaiting Task completion
    d15e - WeatherList => StateHasChanged Called
    d15e - WeatherList => Render Queued
    d15e - WeatherList => Component Rendered
    4755 - FetchData => OnParametersSet Sequence Completed
    4755 - FetchData => SetParametersAsync Completed
    4755 - FetchData => OnAfterRenderAsync Started
    4755 - FetchData => OnAfterRenderAsync Completed
    d15e - WeatherList => OnAfterRenderAsync Started
    d15e - WeatherList => OnAfterRenderAsync Completed
    d15e - WeatherList => OnInitialized sequence Completed
    d15e - WeatherList => OnParametersSet Sequence Started
    d15e - WeatherList => StateHasChanged Called
    d15e - WeatherList => Render Queued
    d15e - WeatherList => Component Rendered
    d15e - WeatherList => OnParametersSet Sequence Completed
    d15e - WeatherList => SetParametersAsync Completed
    d15e - WeatherList => OnAfterRenderAsync Started
    d15e - WeatherList => OnAfterRenderAsync Completed
    

    The above documented sequence demonstrates that there is no way you can set a flag in FetchData that shows a Loading message that you can guarantee will not complete before sub-components start and run their OnInitialized sequences.