Open SteveSandersonMS opened 2 months ago
Thanks for adding this!
I would like to propose this is two separate issues.
I think 1 is a user experience issue, whereas 2 is a developer experience issue - and as such should be dealt with at separate times.
I think it would be easy to solve item 1 without any changes to the Blazor framework by updating the default template to something like the attached. This uses the following approach
This means we can inject IWeatherForcecastService into the /Weather page in order to retrieve the data. When server rendering it will use the server implementation and go straight to the simulated Db, and WASM rendering it will be injected with the proxy class instead and call to the API.
The benefits of this are
I recently wrote a Blazor Interactive WebAssembly basics article. https://www.telerik.com/blogs/building-interactive-blazor-apps-webassembly
The end product was quite lengthy because I had to explain and provide examples of all the issues that arise when using the new Web App template. From the beginning, a selling point of Blazor is it's "low learning curve" for .NET developers. However, we now have a host of new features that improve the framework, but also increase complexity. A template to deal with some of the complexity and set best practices is desperately needed.
I think @mrpmorris has a good sample, this is a better organized version of what I wrote in my article.
At the moment, the current template doesn't show the intention or reason behind the web app project/client project.
@mrpmorris Has a good suggestion with the contracts project, demonstrating an interface, the double injection using different providers.
An awesome improvement (no idea on the feasibility) would be to make it 'feel' like writing traditional WASM or Server code, but the prerender persistence/auto issues are handled for you (opt in/out?).
The ideal dev ex would be quick to figure out intention & not have to google issues (find out you should disable prerendering, which is a must have for SEO).
I think optimising for the best DevEx of prerender ON & Auto render mode, will fix a lot of issues on the side.
For example, an app I'm playing with using the Blazor Web App paradigm has to write a lot of code to call 1 Web API endpoint, then display that data as is.
Example page here: https://github.com/Hona/TempusHub2/blob/main/UI/src/TempusHub.WebUI/TempusHub.WebUI.Client/Components/Pages/Leaderboards/Map/MapPage.razor
With regards to the double-render, the following code works fine. Note the code to simulate fetching data has been moved into FetchDataAsync for brevity.
public partial class Weather : ComponentBase
{
private WeatherForecast[]? forecasts;
[Inject]
private PersistentComponentState PersistentComponentState { get; set; } = null!;
protected override async Task OnInitializedAsync()
{
PersistentComponentState.RegisterOnPersisting(SaveStateAsync);
if (!PersistentComponentState.TryTakeFromJson(nameof(forecasts), out forecasts))
{
await FetchDataAsync();
}
}
private Task SaveStateAsync()
{
PersistentComponentState.PersistAsJson(nameof(forecasts), forecasts);
return Task.CompletedTask;
}
private async Task FetchDataAsync()
{
// ...
}
}
Here are my thoughts for improvement.
The code above is nice and easy mainly because there is only one property to persist. Once there are multiple calling x.TryTakeFromState
for each is laborious. I would like to propose being able to write (something like) the following code instead.
public partial class Weather : ComponentBase
{
public WeatherForecast[]? Forecasts; // Made public
public int SelectedIndex;
public string SomeOtherState = "This is serialized too.";
protected override async Task OnInitializedAsync(bool stateWasDeserialized)
{
if (!stateWasDeserialized)
{
await FetchDataAsync();
}
}
// ...
}
I think this gives the easiest experience for new users and also allows more experienced users to tweak more precisely if needed.
Possible alternative names for this parameter hasPreRenderedState
. It would be nice if the meaning could be negated in order to avoid the ! in the if, but I can't think of a good name. Chat-GPT (Cheat-Petey) suggests stateIsUninitialized
- which I think works quite well.
if (stateIsUnitialized)
await FetchDataAsync();
Another benefit of this approach is that it could be retrofitted to .net 8 using a Roslyn Analyzer, the only difference being there would be no override
on OnInitializedAsync - the OnInitialized would have to be implemented by Roslyn, determine if there is state to deserialize, and then call a new partial OnInitializedAsync with the new parameter. But I don't know if that's a requirement or not.
@mrpmorris I love that design. I want to leave my thoughts that follow on from your ideas.
I believe for the success of Blazor there should be a lot more magic/convenience by default, and it be suggested by way of the templates.
The basis is using Auto render mode with prerendering on. My experience of using this is its incredibly clunky & beginner unfriendly (e.g. when does something render, what project do I put this in, is this called on the client, or the server, or both (twice?))
The suggested paradigm relies on server first, client side second.
I'd suggest the following structure:
Contoso
Contoso.Contracts
Contoso.Client
Now, what would this look like?
Contoso/Components/WeatherPage.razor
@page "/weatherforecast"
@inject IWeatherRepository WeatherRepository
<!-- Put a table here -->
@code {
public WeatherForecast[] Forecasts;
protected override async Task OnInitializedAsync(bool stateWasDeserialized)
{
if (!stateWasDeserialized)
{
Forecasts = await WeatherRepository.GetForecastsAsync();
}
}
}
Contoso.Contracts/Models/WeatherForecast.cs
public record WeatherForecast(...);
`Contoso.Contracts/Repositories/IWeatherRepository.cs
public interface IWeatherRepository
{
Task<WeatherForecast[]> GetForecastsAsync();
}
Now the important stuff, showing which is server, which is client implementations.
Contoso/Repositories/WeatherRepository.cs
public class WeatherRepository(AppDbContext DbContext) : IWeatherRepository
{
public Task<WeatherForecast[]> GetForecastsAsync()
{
// Simulating some sort of EF query
return await DbContext.Forecasts
.ToListAsync();
}
}
Contoso.Client/Repositories/WeatherRepository.cs
public class WeatherRepository(HttpClient Client) : IWeatherRepository
{
public Task<WeatherForecast[]> GetForecastsAsync()
{
// Simulating some sort of EF query
return await Client.GetFromJsonAsync<WeatherForecast[]>("/api/weatherforecasts");
}
}
Now the wireup code
Contoso.Client/Program.cs
(I'd prefer something like DependencyInjection.cs
or something that shows this is only for wiring up contract interfaces to implementations)
...
builder.Services.AddSingleton<IWeatherRepository, WeatherRepository>();
...
Contoso/Program.cs
...
builder.Services.AddSingleton<IWeatherRepository, WeatherRepository>();
...
I think in this way there could even be the possibility to automatically detect what is server only, client only, where there is only a server implementation of the interface, it can only be on the server.
There was some code omitted obviously, interactivity models I generally ignored, where prerenders/client and server switches are the priority for me.
I'd even consider a separate method on ComponentBase, taking this snipped from the previous message:
protected override async Task OnInitializedAsync(bool stateWasDeserialized)
{
if (!stateWasDeserialized)
{
await FetchDataAsync();
}
}
I think this leads to two separate concerns being thrown into one method. A fresh load is inside an if, or past a guard clause, then deserialization cleanup/handling is next to it.
I feel there is enough to warrant something like the following (ignoring async overloads):
// Note that this method would be written the same with classic blazor paradigms
protected override async Task OnInitializedAsync()
{
FetchDataAsync();
}
// Note that this is opt in, if the framework's magic is not correct for specific use cases
protected override void OnDeserialized()
{
// Do something
// Maybe provide parameters mentioning what changed?
}
I think if:
Contoso
server project Then, Blazor could be open to wider adoption.
Feedback from the 10 or so different people I've seen try the Blazor Web App paradigm, have all come to the same conclusion as me, where you should just stick with the classic project structures. The foundation is there for the new methods, but let's make it easy to use.
Keen to hear everyone's thoughts :)
After some internal discussion, our inclination is to not make any further changes to the Blazor Web App project template in .NET 9. The primary reasoning for this is:
I'll use this comment to summarize the internal discussion, including the tradeoffs and potential approaches we considered. We'd greatly appreciate feedback on what specific changes to the template you would like to see, so that the changes we do eventually make will be well-informed.
The original proposal was to make the following changes to variations of the Blazor Web template that use WebAssembly interactivity only:
RenderMode.InteractiveWebAssembly
with new InteractiveWebAssemblyRenderMode(prerender: false)
Weather
page:
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
HttpClient
and use it to fetch weather forecast data in OnInitializedAsync()
The following sections describe some tradeoffs we considered when evaluating these changes.
One fairly significant tradeoff is that the user gets a blank screen on the weather page during WebAssembly initialization. This is an especially poor UX on slower connections, where the WebAssembly runtime can several seconds to download and initialize.
A loading indicator (similar to the one in the WebAssembly standalone template) could provide some feedback to the user while the WebAssembly runtime is downloading and starting. We can do this by creating a WebAssemblyLoadingIndicator
component:
WebAssemblyLoadingIndicator.razor
@rendermode InteractiveWebAssembly
@if (!Platform.IsInteractive)
{
<svg class="loading-progress">
<circle r="40%" cx="50%" cy="50%" />
<circle r="40%" cx="50%" cy="50%" />
</svg>
<div class="loading-progress-text"></div>
}
This component works by:
When using global WebAssembly interactivity, we can simply plop the <LoadingIndicator>
in App.razor
. However, for the per-page interactivity case, things get a little more tricky; we only want to render the loading indicator on pages that utilize WebAssembly interactivity (with prerendering disabled).
One way to achieve this is to move MainLayout.razor
and NavMenu.razor
to the .Client
project and introduce a WebAssemblyLoadingLayout
component that renders a loading indicator alongside the body.
WebAssemblyLoadingIndicator.razor
@inherits LayoutComponentBase
@layout MainLayout
<LoadingIndicator />
@Body
Pages that want to display a loading indicator can do the following:
@page "/weather"
@layout WebAssemblyLoadingLayout
@rendermode @(new InteractiveWebAssemblyRenderMode(false))
This mostly results in a better UX, but:
WebAssemblyLoadingLayout
.Client
project (so that WebAssemblyLoadinglayout
can inherit from MainLayout
)There are other ways to add the loading indicator to the page that don't require an explicit gesture from the page component, but are a bit too "gross" to put in the template. For example, rather than introducing a new WebAssemblyLoadingLayout
, we could update the existing MainLayout
and keep it in the server project:
@inherits LayoutComponentBase
<div class="page">
...
<main>
+ @if (!Platform.IsInteractive && PageRenderMode is InteractiveWebAssemblyRenderMode { Prerender: false })
+ {
+ <LoadingIndicator />
+ }
<article class="content px-4">
@Body
</article>
</main>
</div>
...
@code {
+ private IComponentRenderMode? PageRenderMode
+ => (Body?.Target as RouteView)?.RouteData.PageType.GetCustomAttribute<RenderModeAttribute>()?.Mode;
}
The general internal consensus was that to address the UX regression, we're introducing another tradeoff adding complexity to the template.
We also briefly considered whether there was a way to cleanly make the Weather
page prerendered but still use HttpClient
to fetch data after interactivity starts. We even briefly considered whether a new framework feature could help with this. For example:
@page "/weather"
@rendermode InteractiveWebAssembly
@inject? HttpClient Client @* Some kind of new optional inject *@
@code {
protected override async Task OnInitializedAsync()
{
if (Client is not null)
{
forecasts = await Client.GetFromJsonAsync<WeatherForecast[]?>("/api/weather");
}
}
}
However, we quickly decided against this since it doesn't broadly solve the problem of being able to write components without having to worry about how they run on the server.
The weather page currently demonstrates stream rendering, which was one of the major features included in the .NET 8 release. We'd be swapping out that demonstration in favor of a different one that's arguably less suitable for the scenario, given the page is completely static after loading weather data.
We've received feedback that demonstrating use of HttpClient
would be helpful, but we don't have a clear sense of what the magnitude of the opposite feedback would be; how many developers would ask that we revert to using stream rendering in the template?
There's also the consideration that https://github.com/dotnet/aspnetcore/issues/51584 might get implemented in .NET 9. In many cases, that feature could serve as a better solution to the "double fetch" problem than disabling prerendering and using an HttpClient
.
To help us decide what changes to make to the template, we'd love to hear ideas from the community about what specific changes you'd like to see. Please bear in mind that one of our goals is to keep these templates as straightforward and approachable as possible. Thanks!
I'm 100% fine with the decision made to not change this in .NET 9. Template changes are something we don't want to churn on repeatedly, so doing it earlier in a cycle makes sense.
If we come back to this in .NET 10, what about the following ways to address the concerns above? Not sure if this approach has been considered but it seems simpler and more reliable to me:
MainLayout.razor
(just by inlining the <svg>
alongside the @Body
output), no matter the page render mode or global interactivity choice--blazor-load-percentage
, it also applies a new CSS class blazor-webassembly-loading
to the document element, and removes it once loading is completed. That is, <html>
will have this class if and only if loading is currently in progress.app.css
, add html:not(.blazor-webassembly-loading) .loading-progress { display: none }
I haven't tried it but would expect this to cause the loading indicator to not show up (even as a flicker) except when loading is actually happening, and would work the same whether it's global or per-page interactivity.
@SteveSandersonMS, that's also an approach I tried out, although I ended up with a slightly different version of it:
/* Hide the loading indicator if we haven't started loading WebAssembly resources */
html:not([style*="--blazor-load-percentage"]) > * .loading-indicator {
display: none;
}
/* Hide the loading indicator if the current page contains any content */
article > .loading-indicator:not(:only-child) {
display: none;
}
I found that relying on a CSS variable that gets removed after loading completes wasn't totally ideal because:
html
element, so we'd have to constantly reapply it (I guess it's not a huge deal, just requires some more code in the product).My solution was to add a second CSS rule to hide WebAssembly loading indicator if there's any content on the page. Unfortunately, this was still problematic:
Auto
or Server
interactivity without prerendering, which is odd.html
element until after blazor.web.js
loads and starts downloading resources, so the slowest connections still won't get the loading indicator right away.I can think of some other ways to overcome those problems, but this is where I stopped to try an approach that programmatically adds and removes the loading indicator. Maybe the CSS approach is worth revisiting again!
Seeing this postponed to Net 10, over a year and half from now, when the Net 9 roadmap is barely halfway, and the issues discussed here have been reported long before the Net 8 release is disappointing. I am not sure what additional feedback should be given about what we would like to see in templates: abundant feedback was given before the release of Net 8, and it was often ignored or dismissed by claiming the Web App already has equivalent functionality, even when implementing the feedback in question meant leaving the existing Net 7 templates available side-by-side to the new ones until the latter reached a higher level of maturity.
At any rate, my two (old) cents. Having a template where the developer can choose what render mode to use and generate an app that can be used as a starting point without major surgery just to achieve basic functionality, as needed now with the Web App, would be useful. For example:
1) App with local user accounts in the three render modes (already available), with a realistic example of an API call (currently missing: there is a database already using EF Core, why not make a trivial model/query/api call instead than hardcoding data etc.?).
2) App with Entra using the Microsoft Identity Web library (currently missing, some samples available using generic OIDC, which could also be an option for third-party OIDC providers) demonstrating the different render modes and API calls (already in the works - there is an issue open about it).
In these options, demonstrate persistence (it would be a start to have one page demoing pre-rendering persistence). It can be improved later for Net 10, if a better way is found. I think this would also reduce the effort to dig out the information from the documentation, which is sometimes spotty.
P.S. Templates have changed drastically in Net 8, significantly increasing the amount of work needed to get up to speed, and deeply changing the development pattern (yes, the previous functionality can be recovered, but also that takes time to figure out). They have also changed after the Net 8 release, to allow integration in Aspire. I don't see much concern with changing templates, so I would say pushing a partial fix to the issues discussed in this thread should have higher priority than not altering templates, which does not seem a concern in other circumstances.
To be honest, from the beginning I've found that "Blazor Unified", or Auto, is impressive technically but adds too much overthinking and complexity. As a developer you need to decide which render mode you want and answering that question requires a deep knowledge on what it takes.
By default VS suggests Auto mode, but when you want to add an API project (which is very common and was implemented before .Net 8) you'll realise that some tradeoffs are needed. Even the full (global) WASM template isn't a ready to go as mentioned earlier in this issue.
So the default templates are irrelevant in most use cases and you need to find tricks or disable some things. Back in .Net 7 the templates were ready to go.
Currently Auto, or trying to run a component in both Server and WASM is not a good choice IMO. There are a too many differences wether you're running on Server or on Client. Before .Net 8 that choice was made once for all at the start of the project and it was OK because those 2 modes are and will remain different no matter how hard you try to unify them.
Finally to me Auto should be a third mode for the adventurers / experts that exactly know what they are doing. But default templates should stay stupid simple, full Server or full WASM, and with WASM comes an API call so you are ready to start developing your solution.
.Net 7 WASM template was a real world sample. Now we miss it. When we choose (global) WASM by default we don't want components that are able tu run on the Server. And if it's needed it's up to the developer to do the necessary tricks.
To summarise, three templates would be great :
As of .NET 8, the Blazor Web template with WebAssembly interactivity does not contain any example of calling a server API. The "weather forecast" page simply generates data on the client and thus only demonstrates how to render a table, not how to fetch data.
Actually calling a server API in all generality, accounting for all the main ways of setting up a site, is complicated by several factors, as pointed out by @mrpmorris:
HttpClient
in DI with a base address on both server and client, and have the server call itself via HTTPWhile typical apps don't usually have to solve all these problems simultaneously, we need to be able to demonstrate convenient, flexible patterns that do compose well with all the features and are clean enough to put them in the project template.
Solving this nicely likely depends on implementing some other features like https://github.com/dotnet/aspnetcore/issues/51584
Note for community: Recommendation for .NET 8 developers
As mentioned above, typical apps don't normally have to solve all these problems. It's always been simple to load data from the server with Blazor WebAssembly and that is still the case in .NET 8.
The easiest path is simply to disable WebAssembly prerendering, because then just about all the complexity vanishes. Whenever you used rendermode
WebAssembly
(e.g., inApp.razor
), replace it withnew WebAssemblyRenderMode(prerender: false)
.At that point your UI is just running in the browser and you can make
HttpClient
calls to the server as normal.