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

327 views Asked by At

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.

2

There are 2 answers

4
MrC aka Shaun Curtis On BEST ANSWER

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.

7
MrC aka Shaun Curtis On

If I'm reading your intent correctly, you just need to do something equivalent to a modal dialog that "hides" the loading content.

Here's a demo using the Weather page.

Loading.razor:

<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 if the norm now]. 
            You're at call position 999.  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; }
}

Loading.razor.css

div.loading {
    display: block;
    position: fixed;
    z-index: 101; /* Sit on top */
    left: 0;
    top: 0;
    width: 100%; /* Full width */
    height: 100%; /* Full height */
    overflow: auto; /* Enable scroll if needed */
    background-color: rgba(0,0,0,0.95); /* Black w/ opacity */
}

div.loaded {
    display: none;
}

This should work:

@page "/weather"
@attribute [StreamRendering(true)]

<PageTitle>Weather</PageTitle>
<Loading IsLoading="_loading" />

    <h1>Weather</h1>

    <p>This component demonstrates showing data.</p>

    @if (_loading)
    {
        <p><em>Loading...</em></p>
    }
    else
    {
       //...
    }
@code {
    private WeatherForecast[]? forecasts;
    private bool _loading => forecasts == null;

//...
}

I've coded it so you can also do this:

@page "/weather"
@attribute [StreamRendering(true)]

<PageTitle>Weather</PageTitle>
<Loading IsLoading="_loading">

    <h1>Weather</h1>

    <p>This component demonstrates showing data.</p>

    @if (_loading)
    {
        <p><em>Loading...</em></p>
    }
    else
    {
       //...
    }
</Loading>
@code {
    private WeatherForecast[]? forecasts;
    private bool _loading => forecasts == null;

//...
}

Change the transparency of background-color: rgba(0,0,0,0.95); to let what's happening in the background bleed through.