Avoid 'multi steps refreshing' on page transition in Blazor solution for the entire layout

1.6k Views Asked by At

With Blazor WebAssembly WASM, everything is about components. It means that each component load his content (nearly) independently form other components. Therefore when navigation occurs and the new active page loads some parts of the DOM are displayed before others. For example the header and the footer are probably loading quicker than the main container in which some fetching occurs (API calls). This has the (side) effect that at one point we can see the header and the footer one below the other (as glued) and 2 seconds after seeing the main container injected between the two. I am thinking of a solution to get around this problem. I would like that during page loading nothing is visible until everything is in place (binding + images loaded, etc...).

You will understand that i am not talking about the initial (big) load of blazor but rather the transition between pages.

For example, below is my MainLayout.razor

<div class="container page">
    <HeaderSection />
    @Body
    <NewsLetterSection />
    <FooterSection />
</div>

As you know, each (blazor) pages are injected in the @Body.

Example of page is Todo.razor

@page "/todo"

@if (todolist != null)
{
    foreach (var todo in todolist)
    {
         @todo.task
    }
}

If user navigate to another (blazor) page of the site, at first page should be totally blank (header, main container, footer, ...) until everything is 'ready' to make the page visible again (with everything in place) and appearing without the blinking effect.

I know there are some solutions made available by some people but these does not fit my needs because these solutions mainly concentrate the effort on the page itself and not the entire layout. As explained, when page transition occurs, I would like everything in my MainLayout.razor to be masked until everything is in place on the destination page.

I already try something like explained on the accepted answer here: Call method in MainLayout from a page component in Blazor but not succeed.

I know when navigation occurrs: on NavigationManager.LocationChanged event but I cannot detect when everything is in place on the page. It would have been beneficial to have Task OnAfterRenderAsync(bool firstRender, bool lastRender).

Any idea to put me on the right track ?


UPDATE 1

I came up to a solution based on a State Container. For more info see point 3 here

VisibilityStateContainer.cs

public class VisibilityStateContainer
{
    public bool IsVisible { get; private set; }
    public event Action OnChange;
    public void SetVisibility(bool value)
    {
        IsVisible = value;
        NotifyStateHasChanged();
    }
    private void NotifyStateHasChanged() => OnChange?.Invoke();        
}

Below code of the component who fetch data

@implements IDisposable
@inject VisibilityStateContainer VisibilityStateContainer 
@code
{      
    protected override async Task OnInitializedAsync()
    {
        VisibilityStateContainer.OnChange += StateHasChanged;
        VisibilityStateContainer.SetVisibility(false);
        SomeData = await MyService.GetData();
        VisibilityStateContainer.SetVisibility(true);
    }
    public void Dispose()
    {
        VisibilityStateContainer.OnChange -= StateHasChanged;
    } 
}

@if (VisibilityStateContainer.IsVisible)
{
    <div>
       ...
     </div>
}

Below code of any other component

@implements IDisposable
@inject VisibilityStateContainer VisibilityStateContainer 
@code
{      
    protected override async Task OnInitializedAsync()
    {
        VisibilityStateContainer.OnChange += StateHasChanged;
    }
    public void Dispose()
    {
        VisibilityStateContainer.OnChange -= StateHasChanged;
    } 
}

@if (VisibilityStateContainer.IsVisible)
{
    <div>
       ...
     </div>
}

This basic solution works when there is only one (main) component which load data asynchronously while others just 'listen' for the OnChange state. Everytime navigation occurs, everything is hided at first until IsVisible flag is set (by the component who fetch data). Only then all components are displayed at the same time. If multiple components may load data asynchronously (and thus takes time to render) then the State Container should be modified to take this into account not simply with a boolean IsVisible but maybe an array of boolean.


UPDATE 2 - visual representation of the problem

When you navigate from page A to page B, multiple components (on the page) should be displayed. Data need to be loaded (fetch API) in the initialization event of the main component (green colored). While data is fetched, we can display a spinner or loading message like below in this component. Meanwhile we can already see other Blazor components below.

enter image description here

When data is loaded successfully (see below), the loading message disappear and the page is showing all the content consequently pushing newsletter and footer lower on the page. The side effect of this is a kind of ugly effect (at my point of view).

enter image description here

I agree this is frequent to see pages building in multiple refreshing steps. This is not specific to blazor! When navigation occurs, personnally I prefer to hide everything (except the header) until the page can be displayed in 1 unique refresh. Hope this gives you a better understanding of the problem.

So I suggest a solution in my update 1 (see above). It fit my needs but please let me know if there are better alternative.

2

There are 2 best solutions below

1
Tommy Rikberg On

My preferred solution is to add a div with a loading indicator, which is shown while the data is being fetched, on the individual pages themselves. This div, with appropriate css, acts as a placeholder that keeps the layout from jumping when the content is ready.

It's also better UX to navigate to the new page immediately based on a user action and then indicate that the data is loading. A progress bar would be the best option if the data takes more than a a couple of seconds, but often it is good enough to show a wait spinner or a "Loading..." text as below.

@page "/todo"

@if (todolist == null)
{
    <div class="place-holder">Loading tasks...</div>
}
else
{
    foreach (var todo in todolist)
    {
         @todo.task
    }
}
4
MrC aka Shaun Curtis On

Revised Version

Take a look at this. Your solutions works, this is a different approach.

The basic premise is to replace the Layout with your own Layout component that only displays the layout when the main component sets a boolean parameter.

Create a new Layout LoaderLayout.razor - basically just renders @Body - you can't set @layout = null.

@inherits LayoutComponentBase
@Body

Create a UILoader component that is effectively your new layout controller and manages what gets displayed. I've created it with the standard Blazor Layout. I've also shown it with two content sections. You can have as many as you like.

The razor code for UILoader.razor:

@inherits ComponentBase
@if (this.Loaded)
{
    <div class="page">
        <div class="sidebar">
            <NavMenu />
        </div>

        <div class="main">
            <div class="top-row px-4">
                <a href="https://learn.microsoft.com/aspnet/" target="_blank">About</a>
            </div>

            <div class="content px-4">
                @this.ChildContent
            </div>
            <div class="px-4">
                @this.Footer
            </div>
        </div>
    </div>
}
else
{
    <div>
        <div class="mt-4" style="margin-right:auto; margin-left:auto; width:100%;">
            <div class="loader"></div>
            <div style="width:100%; text-align:center;"><h4>@Message</h4></div>
        </div>
    </div>
}

@code {
    [Parameter] public RenderFragment ChildContent { get; set; }

    [Parameter] public RenderFragment Footer { get; set; }

    [Parameter] public bool Loaded { get; set; }

    [Parameter] public string Message { get; set; } = "Display Loading";
}

The Component CSS to make it pretty - includes the MainLayout css

*/ Copy into here the contents of mainlayout.razor.css/*
*/ To big to show!/*

.page-loader {
    position: absolute;
    left: 50%;
    top: 50%;
    z-index: 1;
    width: 150px;
    height: 150px;
    margin: -75px 0 0 -75px;
    border: 16px solid #f3f3f3;
    border-radius: 50%;
    border-top: 16px solid #3498db;
    width: 120px;
    height: 120px;
    -webkit-animation: spin 2s linear infinite;
    animation: spin 2s linear infinite;
}

.loader {
    border: 16px solid #f3f3f3;
    /* Light grey */
    border-top: 16px solid #3498db;
    /* Blue */
    border-radius: 50%;
    width: 120px;
    height: 120px;
    animation: spin 2s linear infinite;
    margin-left: auto;
    margin-right: auto;
}

@-webkit-keyframes spin {
    0% {
        -webkit-transform: rotate(0deg);
    }

    100% {
        -webkit-transform: rotate(360deg);
    }
}

@keyframes spin {
    0% {
        transform: rotate(0deg);
    }

    100% {
        transform: rotate(360deg);
    }
}

Now the test page. Enclose all the code that depends on load completion inside the UILoader component. I use async coding and a 2 second delay to emulate a slow data load. You need to make sure all your data loading is done before changing Loaded to true. Reload destroys all the components inside UILoader and renders new instances when Loaded is set back to true. LoadEverything should call a loader in a service to get all the data all the components in the page need loaded before the root component sets Loaded to true.

@page "/loader"
@layout LoaderLayout
<UILoader Loaded="this.Loaded">
    <ChildContent>
        <h3>UILoader Test</h3>
        <button class="btn btn-success" @onclick="Reload">Reload</button>
    </ChildContent>
    <Footer>
        <div class="bg-primary text-white m-2 p-4">My Newsletter Data</div>
    </Footer>
</UILoader>

@code {
    private bool Loaded = false;

    protected async override Task OnInitializedAsync()
    {
        await LoadEverything();
        this.Loaded = true;
    }

    private async Task LoadEverything()
    {
        await Task.Delay(2000);
        // make sure everything is loaded before completing i.e. returning a completed Task
    }

    private async Task Reload(MouseEventArgs e)
    {
        this.Loaded = false;
        await InvokeAsync(StateHasChanged);
        await LoadEverything();
        this.Loaded = true;
        await InvokeAsync(StateHasChanged);
    }
}

You can find: