How do I show a loading indicator while images are loading for a Blazor Server application?

I’m working on Blazor Server project which has to load images from a private blob storage location, and display them to the user, according to the docs the way to do this, is to create a JS interop function which handles the streaming part to the client, and have it fill in the <img> src attribute when the streaming is complete. The interop function then gets called in the OnAfterRenderAsync lifecycle override (this last part wasn’t clear from the docs, since they used a button press to trigger loading of the images, and thus might be a wrong conclusion of myself).

As there are multiple images which must be loaded, this can take a while, depending on the user’s connection. Therefor I would like to display some alternate HTML like a loading spinner or similar, but I can’t figure out how to do this.

Perhaps beyond the scope of this question, but expanding on it: It’s clear to me that Blazor rendering happens synchronously, and offers some pre-/post-render lifecycle hooks which can run be used to perform asynchronous calls. But how do components like e.g. MudBlazor’s Table component, which offers server-side filtering,sorting, etc… accomplish this with asynchronous calls?

I created a minimal example which demonstrates what I’m trying to accomplish, I started from the basic dotnet new blazorserver project and added the page below, as well as the JS interop file as shown in the MS docs:

@page "/images"
@inject HttpClient Http
@inject IJSRuntime JS

<div>
    @foreach(var imageSrc in ImageSources)
    {
        <div>
            <img hidden="@(!imageSrc.Loaded)" id="@imageSrc.Id" />
            @if(!imageSrc.Loaded)
            {
                <span>Loading...</span>
            }
        </div>
    }
</div>

@code {
    private List<ImageSource> ImageSources = new(){
        new(Guid.NewGuid(), "https://picsum.photos/200/300"),
        new(Guid.NewGuid(), "https://picsum.photos/200/300"),
        new(Guid.NewGuid(), "https://picsum.photos/200/300"),
        new(Guid.NewGuid(), "https://picsum.photos/200/300")
    };

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        foreach(var imageSrc in ImageSources)
        {
            var stream = await Http.GetStreamAsync(imageSrc.Url);
            await SetImage(imageSrc.Id.ToString(), stream);
            imageSrc.Loaded = true;
        }
    }
    public async Task SetImage(string imageElementId, Stream imageStream)
    {
        await Task.Delay(1000);
        var dotnetImageStream = new DotNetStreamReference(imageStream);
        await JS.InvokeVoidAsync("setImage", imageElementId, dotnetImageStream);
    }

    public record ImageSource(Guid Id, string Url)
    {
        public bool Loaded {get;set;}
    }
}

This example should run fine for anyone with .NET 7 installed by just running dotnet watch run in the repo’s root.

To be clear, this code works without the conditional ‘Loading…’ span and hiding of the img element, but with the code as above, I just get 4 times ‘Loading…’ on the page forever.
So it seems like Blazor is not re-evaluating the Loaded property of the ImageSource objects.

>Solution :

with the code as above, I just get 4 times ‘Loading…’ on the page forever.

OnAfterRender is unlike the other lifecycle events in that it does not do any implicit StateHasChanged() calls. If it did you’d have an endless loop. Your code runs that risk too, you need to use firstRender

protected override async Task OnAfterRenderAsync(bool firstRender)
{
  if(firstRender) // only once
  {
    foreach(var imageSrc in ImageSources)
    {
        var stream = await Http.GetStreamAsync(imageSrc.Url);
        await SetImage(imageSrc.Id.ToString(), stream);
        imageSrc.Loaded = true;
        StateHasChanged();       // render again 
    }
  }
}

Leave a Reply