blazormauimaui-blazor

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?


Solution

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