Matthew Warr
Matthew Warr

Reputation: 168

Issue with Blazor (MAUI Hybrid) and page lifecycle

I am trying to improve the user experience of my app by showing loading indicators while it retrieves data from a server, but there seems to be something wrong with my understanding of how the lifecycle events fire.

On app launch, I direct users to a Landing page component with a loading spinner whilst it preloads some stuff from a server, before redirecting users to a Dashboard page component.

What I'd like it to do is open the Dashboard page as instantly as possible, with the default content being a loading spinner.

The content side of things I have no problem with.

`OnInitializedAsync' does nothing much. It is at this point that I would expect the user interface to be rendered to the user. But it doesn't.

protected override async Task OnInitializedAsync()
{
    Quote = CommonFunctions.RandomQuote();

    _pageHistoryService.AddPageToHistory("/dashboard");
}

I also have an OnAfterRenderAsync method. This method I would expect to run in the background after the page has loaded.

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {   
        GetData();

        using var timer = new PeriodicTimer(TimeSpan.FromSeconds(15));
        while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync())
        {
            GetData();
        }
        return;
    }
}

In reality, the UI doesn't display the DOM until after the OnAfterRenderAsync method is completed. Which doesn't make any sense to me.

Is there something I am misunderstanding, or some setting or parameter I need to set in order to get the behaviour I want?

Upvotes: 0

Views: 70

Answers (1)

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30310

There are several issues in your code. The principle one is you are setting up and running the timer within the component lifecycle: the first call to OnAfterRenderAsync never completes. You should set up the timer within OnInitialized{Async} and then let the timer drive updates outside the lifecycle.

Here's a simple demo to show one way to refactor the code you've shown. For simplicity, it just updates the time every second. There's commentary in the code to explain various points.

@page "/"
@implements IDisposable
<h1>Hello, world!</h1>

Welcome to your new app.

<div class="bg-dark text-white m-2 p-2">
    @if(_value is null)
    {
        <pre>Loading....</pre>
    }
    else
    {
        <pre>VALUE: @_value</pre>
    }
</div>

@code{
    private Timer? _timer;
    private string? _value;

    protected override Task OnInitializedAsync()
    {
        // Set up time to run every second with the initial run immediately
        _timer = new(this.OnTimerElapsed, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
        return Task.CompletedTask;
    }

    // This is the timer callback method.  
    // It's run outside the component lifecycle and potentially on another thread
    private async void OnTimerElapsed(object? state)
    {
        // Fake async call to get data
        await Task.Delay(100);
        _value = DateTime.Now.ToLongTimeString();
        // invoke on the UI Thread/Sync Context 
        await this.InvokeAsync(StateHasChanged);
    }

    protected override void OnAfterRender(bool firstRender)
    {
        // General rule: Do nothing here except JS stuff
    }

    // Need to implement IDisposable and dispose the timer properly
    public void Dispose()
    {
        _timer?.Dispose();
    }
}

Also review these answers [and many more] which explain more about OnAfterRender.

Upvotes: 0

Related Questions