Skip to content

Conversation

@guardrex
Copy link
Collaborator

@guardrex guardrex commented Apr 2, 2025

Fixes #35111
Addresses dotnet/aspnetcore#49056

Thanks @divvjson! 🚀 ... I'll leave this up overnight to give you a chance to compare notes with what you may have done based on Javier's advice and what I came up with. The scenario that you're working with on the DIFF is in the Global Interactive WebAssembly rendering without prerendering section.


Internal previews

📄 File 🔗 Preview link
aspnetcore/blazor/components/lifecycle.md aspnetcore/blazor/components/lifecycle
aspnetcore/blazor/components/rendering.md aspnetcore/blazor/components/rendering
aspnetcore/blazor/fundamentals/startup.md aspnetcore/blazor/fundamentals/startup
aspnetcore/security/data-protection/implementation/key-storage-providers.md aspnetcore/security/data-protection/implementation/key-storage-providers
aspnetcore/signalr/redis-backplane.md aspnetcore/signalr/redis-backplane

@guardrex guardrex self-assigned this Apr 2, 2025
@divvjson
Copy link

divvjson commented Apr 3, 2025

@guardrex I am a bit unsure about the example. How would ContentLoading show the progress indicator before the WebAssembly has been loaded? ContentLoading is a WebAssembly component in itself right? We need a loading indicator that is shown while the WebAssembly is downloaded and it should be removed when the application is ready.

@guardrex
Copy link
Collaborator Author

guardrex commented Apr 3, 2025

Yes ... that's right. 🤔 I think Javier must be suggesting that a JS initializer be used ... or some approach involving Blazor.start() ... to display the static content before the client-side app starts downloading its client-side assets. You see that I'm asking him to confirm my hunch on your PU issue. Let me hear what he says, and then I'll take a stab 🔪 at getting it to work and into this PR.

Still tho ... even if that's the case ... I think what I did isn't bad for displaying a loading indicator while long-running work is taking place after the app loads. It also dovetails with what we show for kicking off long-running cancelable background work.

@guardrex
Copy link
Collaborator Author

guardrex commented Apr 3, 2025

I've moved the post-runtime-loading approach to the Cancelable background work section in the Lifecycle article, where it most appropriately goes.

I left a placeholder in the Startup article for a JS initializer (or Blazor.start())-based approach. I'm just waiting to see if Javier has anything else he wants to say. If not tho, I have an idea on how to proceed based on the remark that he made.

@guardrex
Copy link
Collaborator Author

guardrex commented Apr 3, 2025

BTW ... Even if I don't hear back from Javier, I'll take a shot at it with a JS initializer-based approach. I do have a vague idea how it might work that way to immediately show a loading indicator while the WebAssembly .NET app/runtime bundle comes down and the app starts. I'll see if I can get back to this by EOD ... but if not, I'll see if I can work it out tomorrow (Friday) morning.

@guardrex
Copy link
Collaborator Author

guardrex commented Apr 3, 2025

🦆

This looks good using a JS initializer approach ...

  • Add a JS initializer (the name "BlazorSample" is the assembly name).
  • The wwwroot folder here is the one in the BWA server-side project, not the .Client project.

wwwroot/BlazorSample.lib.module.js:

export function beforeWebStart(options) {
  var progress = document.createElement("progress");
  progress.id = 'progressBar';
  progress.ariaLabel = 'Blazor loading…';
  progress.style = 'position:absolute;top:50%;left:50%;margin-right:-50%;transform:translate(-50%,-50%);';
  document.body.appendChild(progress);
}

export function afterWebAssemblyStarted(blazor) {
  var progress = document.getElementById('progressBar');
  progress.remove();
}

That one isn't designed to track actual progress, but it would get devs going in the right direction with a progress indicator that updates progress based on boot resources arriving.

AND

It requires the workaround in the App component in .NET 8 and .NET 9 for dotnet/aspnetcore#54049 ...

<script src="_framework/blazor.web.js" autostart="false"></script>
<script>
    Blazor.start({
        webAssembly: {}
    });
</script>

I'll consider if I want to work up a full-blown percentage (%) progress indicator tomorrow morning. In the meantime, give that a try and see what you think.

@guardrex
Copy link
Collaborator Author

guardrex commented Apr 3, 2025

🦖 NOTE TO SELF

  • Add the example to the list in the JS initializers coverage. Done! 👍
  • Add coverage for the WASM before/after FNs workaround for .NET 8 and .NET 9. It will be fixed for .NET 10. Done! 👍

@guardrex
Copy link
Collaborator Author

guardrex commented Apr 4, 2025

@divvjson ... This looks good. I have the following on here ...

  • The added approach that works well with initialization that triggers long-running background work. That's placed in the Lifecycle article, explicitly for that approach.
  • I add a bit about the framework bug for beforeWebAssemblyStart(options, extensions)/afterWebAssemblyStarted(blazor) for just .NET 8/9 (will be fixed for .NET 10) in the Startup article where JS initializers are explained.
  • Going with a simple <progress> indicator without actual progress for all of these examples that add an indicator. We'll leave it to devs to customize if they want actual "progress" to be shown by the indicator based on loading boot resources. I remark on that and cross-link to the section on loading boot resources to get them started with the idea.
  • Add the approach for Global Interactive WebAssembly rendering with prerendering ... that's using RendererInfo.IsInteractive and wrapping @Body with the loading component.
  • Add the approach for Global Interactive WebAssembly rendering without prerendering ... using the JS initializer approach, and I have a paragraph to see the issue that you opened about using the MainLayout/OnAfterRenderAsync approach. So, that will be visible to folks without having to carry it in the article.

... that should do the trick 🐇🎩🪄 for now.

I'll read all of this one more time and then merge it. Let me know if you think I missed anything.

@divvjson
Copy link

divvjson commented Apr 4, 2025

I didn't get a clear understanding on why the approach with removing the progress indicator in the MainLayout.razor was not the best approach? Is it common to have many layouts in an app, and if so, it could still work? Other than that I am happy :)

@guardrex
Copy link
Collaborator Author

guardrex commented Apr 4, 2025

Correct, it's not a good centralized solution to the problem like the afterWebAssemblyStarted approach is.

However ......

I did just notice that the Routes component can do it ........

Routes.razor.js:

export function removeIndicator() {
  var progress = document.getElementById('progressBar');
  progress.remove();
}

Routes.razor:

@implements IAsyncDisposable
@inject IJSRuntime JS

<Router AppAssembly="typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
        <FocusOnNavigate RouteData="routeData" Selector="h1" />
    </Found>
</Router>

@code {
    private IJSObjectReference? module;

    protected async override Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import",
                "./Routes.razor.js");

            await module.InvokeVoidAsync("removeIndicator");
        }
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            try
            {
                await module.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
            }
        }
    }
}

I think I'll mention both in the issue and in the article's paragraph. Again, I'd like to take feedback on the use and stability of these before actually placing them into the article.

@divvjson
Copy link

divvjson commented Apr 4, 2025

Cool.

Do you need to create private IJSObjectReference? module; as an instance field? I guess you could just dispose it after you used it in OnAfterRenderAsync?

@guardrex
Copy link
Collaborator Author

guardrex commented Apr 4, 2025

Yes, you must adopt the normal module pattern with IJSObjectReference and IAsyncDisposable.DisposeAsync.

The general pattern is covered with the JsCollocation2 example in the JS Location article ... it's the second example in that section.

@guardrex
Copy link
Collaborator Author

guardrex commented Apr 4, 2025

BTW ... I'm going to change the code example naming away from "progress" bar because all of the examples are really just loading indicators. There won't be actual "progress" displayed with the article's examples. Devs can make their own real "progress" indicators if they want following the client-side boot resource loading guidance with a few lines to update a progress indicator as files come down to the client.

@guardrex
Copy link
Collaborator Author

guardrex commented Apr 4, 2025

OK ... all good. Thanks for the issue @divvjson. This led to a lot of nice content updates and additions. I'll merge this into the live doc set immediately, and it should appear on the live site within ten or twenty minutes.

FYI @Rick-Anderson @wadepickett ... I'm also fixing in passing here a bunch of broken Azure Redis Cache cross-links (/azure/redis-cache/) that popped up on the build report.

@guardrex guardrex merged commit 595f5c3 into main Apr 4, 2025
3 checks passed
@guardrex guardrex deleted the guardrex/blazor-progress-indicator branch April 4, 2025 15:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Document an approach for a loading indicator that works with global Interactive WebAssembly without prerendering

3 participants