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
}
}
}