Closed danroth27 closed 3 months ago
The error page, which uses HttpContext to get the request ID
We already accounted for that when implementing error support. No matter how your app is set up, the error page will render non-interactively automatically.
Scaffolded CRUD pages that interact with EF Core to handle data interactions
Can you clarify? Why wouldn't you want those to be interactive? Those sorts of pages are full of reasons to have interactive behaviors. Shouldn't it be up to the developer to choose whether CRUD pages use interactive rendering or not?
The default Identity UI, that uses server based functionality for user identity and account management
I agree about this, although (1) we already have an adequate workaround, and (2) it's only a point-in-time limitation that the Identity UI pages are not coded in such a way that they work nicely in interactive mode. Longer term it would be best if we did make them work properly in interactive mode (and yes they will have to do something nonobvious to set cookies).
Please don't interpret this as rejecting the feature request. I totally agree developers should have the flexibility to do this anywhere that they want. I'm just trying to sharpen up the definition of the scenarios to make sure we don't optimize the design for the wrong things. My justification for the feature would be something like "in per-page mode you can opt into interactivity, so in global mode you should be able to opt out of interactivity - in both cases so that architectural choices made for one part of your app don't limit your choices in other parts of your app"
We already accounted for that when implementing error support. No matter how your app is set up, the error page will render non-interactively automatically.
How is this accomplished?
Can you clarify? Why wouldn't you want those to be interactive? Those sorts of pages are full of reasons to have interactive behaviors. Shouldn't it be up to the developer to choose whether CRUD pages use interactive rendering or not?
You're correct that interactivity may be desired. I was focusing on static server rendering to simplify the scenario and to get to parity with the scaffolding support we have for MVC & Razor Pages. Interactive server rendering should be relatively straightforward to support since the code is still running from the server. Interactive WebAssembly rendering however adds a bunch of complexity because it requires a different data access strategy (API endpoints, HTTP requests, etc).
it's only a point-in-time limitation that the Identity UI pages are not coded in such a way that they work nicely in interactive mode. Longer term it would be best if we did make them work properly in interactive mode (and yes they will have to do something nonobvious to set cookies).
In addition to figuring out a way to set the cookies presumably we'll need a way to abstract access to the identity related services and data. For some components, it does seem like supporting all render modes introduces a significant amount of complexity currently.
My justification for the feature would be something like "in per-page mode you can opt into interactivity, so in global mode you should be able to opt out of interactivity - in both cases so that architectural choices made for one part of your app don't limit your choices in other parts of your app"
Agreed. I'm happy to update the original post description accordingly.
How is this accomplished?
Internal stuff, not exposed as public API. The renderer explicitly checks if the current HttpContext contains IErrorHandlingFeature
or whatever it's called - the thing that's the error handling middlware places there when re-running the pipeline.
For some components, it does seem like supporting all render modes introduces a significant amount of complexity currently.
Definitely. Just handling SSR + InteractiveServer is not so much complexity (except in some edge cases where you have to be able to set cookies), but I definitely agree that InteractiveWebAssembly adds a whole extra dimension. Limiting the scaffolding to SSR + InteractiveServer would be a pretty reasonable option.
Thanks for contacting us.
We're moving this issue to the .NET 9 Planning
milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s).
If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues.
To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.
This feature would be extremely useful for global InteractiveServer projects that want certain components/pages to render statically. In Blazor 8, it is currently tricky to do this.
I'm trying to convert a large(ish) Blazor Server 7 app to the new Blazor 8 web pattern and am having A LOT of issues unless I keep it globally InteractiveServer, especially with third party Blazor components.
This would allow me to carve out pages that can run statically. Currently, setting the rendermode on individual pages doesn't work, as it causes the circuit to drop and create a new one on each page (and the app relies on scoped services currently). :(
(I tried setting rendermode option in _Imports.Razor file, but as you know, it doesn't function there. Dan suggests I try using the Router to do this.)
My simple goal is to have a default global "InteractiveServer" render mode (primarily to keep a single circuit open across the app), but to be able to specify components as static when needed.
I understand this will be less useful for new apps, but it will really help with converting existing apps.
Good to see that my question from mid-August was reconsidered after all ^_^
Thanks for contacting us.
We're moving this issue to the .NET 9 Planning
milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s).
If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues.
To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.
I'll add some complexity to this:
I would like to be able to differentiate on other parameters than just the page or component. For instance, a page might need interactivity if the user is logged in, but not otherwise. Or it might need it of the user has a certain permission.
The ability to switch between prerendered and non-prerendered interactivity based on the same parameters. You may want to enable prerenderering, but reasonably be able to say that most pages will never be prerendered. The way it is now, if you enable prerendering for a WebAssembly application, every single service needs to work on the server as well as the client to avoid prerendering causing a crash. Even if you try to differentiate based on state, inject-direcrives on pages will crash the application during prerendering if the service is not available. Even if I know this service will never be used or required during static rendering, I need it to be injectable, or the app will crash..
It seems to me like there are a couple of possible solutions that could solve all of the above.
Suggestions:
Add an interface that one could register in DI that could perform a discriminating function, It would have information about the current request/navigation action, and be used to determine behaviour, such as the rendermode, whether or not to pretender, whether or not to require injected services to exist.
Add a cascading root component that holds information about whether it is prerendering or not.
Add a way for a component to retrieve the current rendermode.
Add a simple way to switch layouts based on the above, enabling the use of a simpler layout for prerendering scenarios
Enable conditional injects
Enable optional injects
Thanks @AbstractionsAs for the feedback!
Add a cascading root component that holds information about whether it is prerendering or not. Add a way for a component to retrieve the current rendermode.
I think these will be covered by #49401.
Enable optional injects
I believe in .NET 9 Preview 2 you can now do optional service dependencies in components using constructor injection.
I've been looking into and prototyping this, and have two main design possibilities: (1) simple but limited, and (2) powerful but confusing.
As you'll probably guess from my write-up, I'm currently siding with option 1 because, having prototyped option 2, I'm concerned that option 2:
@javiercn @MackinnonBuck @halter73 I know this is long, but if it makes sense and you have other proposals, please let me know. I am still open to things like option 2 (a new rendermode) but only if someone's inspired with a way to make it more intuitive and foolproof. Option 1 is limited but I don't think it will cost us much to support in the long run and meets the core needs.
This addresses the specific requirements of this issue, but does nothing more than that. There would be some way to indicate that a page component demands to be static. Note that this is not a new rendermode because it's not applicable in other places (e.g., on @rendermode=...
attributes, or on any components besides page components).
The syntax might be:
@page "/something"
@attribute [StaticPage]
or
@page "/something"
@staticpage
or some other variant like that. Once again, it's not a rendermode.
This would cause us to register some metadata on the page's endpoint setting this "static only" flag. Its effect would be:
EndpointHtmlRenderer
in a mode where it completely ignores all @rendermode
values, so everything is treated as static
<Routes @rendermode="InteractiveServer" />
etc) from actually starting up interactivity.As for handling outbound navigation from the static page:
So in summary this is simple, targeted, doesn't have any back-compat issues, and doesn't introduce any new pits of failure or likely points of confusion that I can think of. And it doesn't require any upgrade steps for existing apps.
This is where I started and have prototyped a fully-working solution. If you use it according to specific guidelines, it does work and covers all our scenarios, and is more powerful than option 1 above.
However, syntactically there are also many other ways to use it that seem valid but actually have mind-bending, unguessable behaviors, and have taken me a lot of debugging to even reason about what's going on.
Define a new rendermode like this:
@page "/something"
@rendermode StaticServer
Next, to escape from global interactivity, we need a way for the page's rendermode to override the rendermode set on the router. So we have the framework automatically pass the page's rendermode as a parameter to your root component (conditionally on you actually declaring this parameter property), and then you can use it to override the top-level rendermode:
@* App.razor *@
<html>
<head>
<HeadOutlet @rendermode="@(PageRenderMode ?? InteractiveServer)" />
</head>
<body>
<Routes @rendermode="@(PageRenderMode ?? InteractiveServer)" />
<script src="_framework/blazor.web.js"></script>
</body>
</html>
@code {
[Parameter]
public IComponentRenderMode? PageRenderMode { get; set; }
}
So now on the initial page load:
InteractiveServer
(i.e., we continue to support global interactivity)StaticServer
mode, you get a static page, satisfying the core requirement of this issueInteractiveServer
with InteractiveWebAssembly
if you want. That creates a new capability - global interactivity where you can still override the hosting platform on a per-page basis.Next we have to deal with what happens when you try to navigate across rendermodes.
@rendermode StaticServer
, we need to escape from interactive routing.
Router
so that when it builds its route table, it excludes any pages that specify a rendermode that's incompatible with the current renderer.RemoteRenderer
(Blazor Server), it does not register route table entries for any pages with rendermode StaticServer
or InteractiveWebAssembly
. And similar for WebAssemblyRenderer
and WebViewRenderer
.@rendermode StaticServer
will not hit anything in the interactive router, so it will fall back on a full page load and you get back to staticInteractiveServer
.@rendermode="@(PageRenderMode ?? InteractiveServer)"
is saying that InteractiveServer
is your default rendermode for pages that don't specify oneSo, if you do either of the following:
@* Existing apps that are SSR only, or per-page interactivity *@
<Routes />
... or ...
@* Global interactivity but allowing per-page overrides *@
<Routes @rendermode="@(PageRenderMode ?? InteractiveServer)" />
Then you get sensible, desirable behaviors. However, the problems start if you do any of the following:
@* Existing global-interactivity apps *@
<Routes @rendermode="InteractiveServer" />
... or ...
@* This looks like sensible code, but the behavior is subtle and strange - see below *@
<Routes @rendermode="@PageRenderMode" />
... or ...
@* This also looks sensible (i.e., define SSR as the default), but again the behavior is NOT what you think *@
<Routes @rendermode="@(PageRenderMode ?? StaticServer)" />
<Routes @rendermode="InteractiveServer" />
This is kind of obvious. On the initial page load it's going to ignore the per-page rendermode and just use InteractiveServer
regardless, so @rendermode StaticServer
simply doesn't work. This means people with existing global-interactivity sites have to do a nonobvious upgrade step and if they miss it, will struggle to understand why @rendermode StaticServer
is ignored.
TBH this is not so bad because at least it's understandable.
<Routes @rendermode="@PageRenderMode" />
Router
is somehow going to look at the @rendermode
expression and evaluate what it would be for a given destination, but that's not what happens at all. The @rendermode
expression is only evaluated during the initial page load and has nothing to do with building the route table.<Routes @rendermode="@(PageRenderMode ?? StaticServer)" />
People might do this if they want per-page interactivity, because it looks like it's saying to use the page's rendermode or fall back on SSR. You might think this allows you to do interactive routing among interactive pages (not enhanced nav), but still be able to escape from interactivity to SSR whenever a page doesn't specify a rendermode.
But it doesn't behave like that.
StaticServer
).
@rendermode="InteractiveServer"
would. The runtime has no way to know you're even trying to set some other behavior for pages without a rendermode.It's particularly aggravating because @rendermode="@(PageRenderMode ?? InteractiveServer) "
and @rendermode="@(PageRenderMode ?? StaticServer) "
feel near-equivalent so it's hard to justify why one of them happens to have correct behaviors and the other does weird stuff.
Also cc @danroth27 for opinions.
I think option 1 is the way to go. It's simple and satisfies the requirement nicely.
Blazor already has plenty of pitfalls to be aware of, let's not introduce more 😄
I prefer Option 1 and the @staticpage
syntax is spot on
I think Option 1 makes the most sense in this specific scenario.
I like the idea of Option 2 because it sounds like that could open up avenues for more extensibility on how developers choose to render components (we've been wanting more flexibility here for our app). However, there is already a ton of confusion in the community on how the existing rendering modes work and I fear this would become problematic, as you're already discovering yourself.
because it's not applicable in other places (e.g., on @rendermode=... attributes, or on any components besides page components).
How far of a stretch would it be for this attribute to be able to be used on individual components? If we could have static components inside of interactive components, that would be huge - though I understand this is completely outside the scope of this issue...just wondering if this could be more powerful than realized.
How far of a stretch would it be for this attribute to be able to be used on individual components?
That would be an incredible stretch, I'm afraid! We'd have to somehow call back to the server (e.g., from WebAssembly) to ask it to render fragments of pages that aren't even addressable via a URL. It's spectacularly out of scope 😄
I agree with others. It sounds like option 1 is the most straightforward. Being able to flag a page to be excluded from the global rendermode/router would achieve the goal. Also, using this attribute makes it clear that it is not a rendermode, but a flag.
In my application, my goal has always been to exclude certain pages (login, error, status, pages that need to be public non-interactive, etc.) from the global render mode.
P.S. Blazor a fantastic web framework (speaking as a web developer since 1997). I typically work on applications that have less than 1k simultaneous users, so my number one "feature" of a web framework is that it makes state management simple (or transparent). With Blazor, I don't have to manage a separate client and server state (other than DB). I can just have a single shared state. This is so productive and eliminates so many bugs. :) Blazor also has the flexibility to engineer your app for high performance/load (at the expense of more state complexity) and mix and match. You are the man Steve!
@SteveSandersonMS I'm ok with proposal 1, but I think there might be better ways of achieving proposal 2.
Instead of having to do all the "shennanigans" with the multiple render modes. We could provide the render mode from the host as a cascading value that gets computed by looking at the endpoint metadata.
The way we would compute this value would be by using a global default value (server, webassembly, auto) that can be overriden by the page if it provides a concrete @rendermode StaticServer
.
I feel a bit of impedance between having a different attribute as opposed to having a different render mode. If we proceed this way I would prefer if we name, the attribute differently without making any mention to the render mode. Like [DisableInteractivity]
or something that can't be associated with the render mode.
If on the contrary we do something like what I suggested, I think that can end up being something like:
@code
{
[CascadingValue] public RenderMode ResolvedRenderMode { get; set; }
}
<Router @rendermode="ResolvedRenderMode">
...
</Router>
Then the only thing to do remains defining the "default global render mode" which we could do in the call to MapRazorComponents();
as follows.
MapRazorComponents() -> Static|None
MapRazorComponents().AddServerComponents() -> Server
MapRazorComponents().AddWebAssemblyComponents() -> WebAssembly
MapRazorComponents().AddServerComponents().AddWebAssemblyComponents() -> Auto
MapRazorComponents(defaultRenderMode: RenderMode.Server).AddServerComponents().AddWebAssemblyComponents() -> Server
Problem with <Routes @rendermode="InteractiveServer" /> This is kind of obvious. On the initial page load it's going to ignore the per-page rendermode and just use InteractiveServer regardless, so @rendermode StaticServer simply doesn't work. This means people with existing global-interactivity sites have to do a nonobvious upgrade step and if they miss it, will struggle to understand why @rendermode StaticServer is ignored.
The router could get its render mode as a parameter and check the page its rendering to ensure it either doesn't define a render mode or defines a compatible one and throw an exception otherwise, couldn't it?
Problem with <Routes @rendermode="@PageRenderMode" />
If the render mode comes resolved from the host (and the SSR host just uses the global default + endpoint override) then this would work, because it would use the global default, isn't it?
The other aspect I think is that you would not get into a situation where you have <Router @rendermode="InteractiveServer" />
but your page doesn't render interactively without you knowing (think of a page coming from an RCL for example).
If we were to let the host resolve the render mode and have the server use the endpoint metadata to suggest a value, then at the point where you are passing the render mode to the router you can always put a breakpoint and know exactly what render mode is being used.
You also get reasonable behaviors in all combos (Server only, Wasm only, both enabled) and retain the ability to override on a per page basis (if the render mode is incompatible the entry doesn't get added to the client route table)
Finally, you get the benefit that you mentioned above where different groups of pages retain the ability to perform client navigations amongst themselves.
Conceptually, I think Option 2
is the most consistent and "simple" way; There are three ways of rendering Razor Components:
@rendermode InteractiveServer
@rendermode InteractiveWebAssembly
@rendermode
is set anywhere in the tree, or can be restored using... @staticpage
...wait what? Why did this suddenly completely change?@staticpage
directive feels like @technically-not-a-rendermode-because-we-were-unable-to-implement-it-as-such
That said, I definitely understand the problems faced with @rendermode StaticServer
, but the way it's shown tells me that all it requires is some (very) good documentation and some Roslyn Analyzers; Ensure that uses of @rendermode
is "sane" according to whatever rules are needed for it to work reliably.
Another thing that I realized now is that option 1 is all or nothing, meaning that no component is going to be able to render interactively (for example in your layout) if your page requires static rendering.
With option 2 that's not the case, and it also leaves the door open for interactive components on the page to render interactively if they need to.
Regarding option 2 and the navigation render mode "pitfalls", what if you added a new default RenderMode "Inherit"? This would actually be the default render mode if not specified. For pages that have not yet been navigated to and that do NOT have a RenderMode set, then Inherit would default to Static.
I believe this would be fairly backward compatible, but would require the addition of 2 new RenderModes (Static and Inherit).
Instead of having to do all the "shennanigans" with the multiple render modes. We could provide the render mode from the host as a cascading value that gets computed by looking at the endpoint metadata. The way we would compute this value would be by using a global default value (server, webassembly, auto) that can be overriden by the page if it provides a concrete @rendermode StaticServer.
What I'm taking from this is the notion that, instead of resolving the rendermode via arbitrary code inside App.razor
, we say "there is only one rule you can use, and it's: PageRenderMode ?? GlobalDefaultRenderMode
", where GlobalDefaultRenderMode
is another thing to configure in Program.cs
.
That does have the payoff that we can use the same rule when building the interactive route table (at the cost that we have to communicate GlobalDefaultRenderMode
into the circuit/wasm, via persistent component state or similar).
However, I'm concerned this still leaves us in the category of "it works if you use it like we say, but does weird stuff if you try to use it in ways we didn't plan for", which is one of the key challenges with rendermodes in general. For example, taking existing apps, if I'm interpreting correctly then I think:
App.razor
to put in the boilerplate logic, and move the place where they define the global rendermode from App.razor
into a new place in Program.cs
@rendermode StaticServer
as it will either be ignored or will be an error@rendermode="SomethingSpecific"
in App.razor
given that it wouldn't agree with the new rules around how we build the interactive route tableApp.razor
to pick the rendermode dynamically (e.g., based on URL, some HTTP header, or whatever), which is a genuinely useful feature today. However, that arbitrary logic will not compose properly with @rendermode StaticServer
or possibly other cross-mode navigations since the router will not be using that logic when it builds its route table.I'm keen that we bias towards simplicity and not increasing the range of weird scenarios that we didn't anticipate, since the community is already stretched to understand what you can do in .NET 8 (and of course there's the support load). So unless there's super high payoff and really low chance of problems, I'm reluctant to add new combinations and upgrade steps 😄
Similarly all the stuff about being able to do client-side navigations between Server page groups and WebAssembly page groups is conceptually interesting but creates new inconsistencies/problems if you're reliant on this but then try to add some interactive component to the layout (we either have to destroy and lose its state when transitioning across the rendermode groups, because it's now a full-page load, or we simply can't transition across the rendermode groups and it we either ignore the specified rendermode, or it becomes an error to attempt that navigation). So it's also expanding the surface area of "stuff people can do but leads into them into weirdness".
Regarding option 2 and the navigation render mode "pitfalls", what if you added a new default RenderMode "Inherit"?
TBH I'm not clear on how that differs from not setting a rendermode, or if the problems would still remain on components that don't set a rendermode.
Regarding option 2 and the navigation render mode "pitfalls", what if you added a new default RenderMode "Inherit"?
TBH I'm not clear on how that differs from not setting a rendermode, or if the problems would still remain on components that don't set a rendermode.
It changes nothing from a functionality standpoint, but makes it clear to developers what the rendering behavior will be (which was a valid concern you outlined regarding option 2).
As you outlined, I see having a page "inherit" the rendermode from the previous page an advantage and not necessarily a pitfall. For example, I could have some public "static" pages in the app that the user could navigate to. If they navigate to them after already entering the interactive part of the app, they will render as interactive and (in the case of InteractiveServer) the circuit would not be dropped. (I am making an assumption based on my understanding of your description of option 2).
Both options work for me and there seems to be a lot of pros to option 2, but they come with some cons regarding complexity, backward compatibility, and perhaps some unexpected behavior (as you said). I was thinking by making the behavior you outlined more implicate with a specific new RenderMode, it would help clarify the communities understanding of the behavior you outlined in Option 2. Just a thought if you go that route. ;)
P.S. Of course, now that I say this, you then have essentially have two RenderMode contexts you have to track. The page's "default" rendermode and then the rendermode the page/component is currently operating under.
I believe there is another issue requesting an API for code to be able to "get" the current rendermode of a component.
After recently going through the migration of a traditional Blazor application to the new Blazor approach in .NET 8 (2 months development effort, 500+ commits) I would like to request the following:
- please validate that this feature is actually needed before proceeding - it is already possible to configure an application for Static Rendering and then specify @rendermode = Interactive for the majority of your components - this is exactly what my application does as the home page is expected to be static but all of the administrative pages are expected to be interactive.
All good points. Regarding this last point, the issue I have run in to (with InteractiveServer) is that, when one configures the app this way (rendermode specified on each page instead of globally), when navigating between pages, the SignalR circuit is destroyed, and one loses any scoped services and cascading values. This is a problem for apps the use scoped services to store user state across pages (which was a recommended/common approach in Blazor 7-).
(I know one can externalize the session state stores in other ways (Distributed Cache))
@gabephudson it sounds like you are struggling with state management with mixed render modes. This is indeed a very big challenge in Blazor in .NET8 and is actually aligned with my first bullet point - “please make existing Blazor scenarios work 100%”. In Oqtane I am manually passing state as serializable parameters across the render mode boundary which is fairly simple to implement and works well. Maybe we just need some better guidance on this item as it seems to be the area where most developers are struggling… and it is potentially a better solution than introducing yet another render mode (with additional state management challenges)
please validate that this feature is actually needed before proceeding
I think the presence of the following in App.razor
in the .NET 8 Blazor project template if you have individual auth enabled is sufficient motivation for this feature:
<Routes @rendermode="RenderModeForPage" />
@* ... *@
@code {
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
private IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account")
? null
: InteractiveServer;
}
Having to check the HttpContext
to see if the request path starts with /Account
is very ugly. It's possible to have made this a little cleaner by checking the matched endpoint for a custom attribute or something, but that should be something that's built into the framework for a scenario as common as statically rendering a page on a mostly interactive site.
I think option 1 where we add a very limited @staticpage
directive that causes the EndpointHtmlRenderer
to ignore @rendermode
values fits the bill perfectly. It doesn't require any modifications to the code other than adding the directive to the page. I want to get out of the business of writing complicated expressions to cumpute a @rendermode
value.
Sure, it's less flexible than a fully blown render mode. @javiercn is right that it's all or nothing, but I think that's okay that no component including the layout is going to be able to render interactively if a page is explicitly labeled as a @staticpage
. The whole point is that I'm overriding the defaults of the layout and subcomponents, and it's very easy to explain. If you really want the main page to be rendered statically without preventing other components from rendering dynamically, you pass a computed @rendermode
to the router that's null for static pages like you do today.
I'm a fan of making it a directive rather than an attribute to highlight how special it is. We should also make sure we provide a clear error if someone tries to put @staticpage
on a non-routable component.
Also, this is a bit of a tangent, but would it be possible to delay all rendering until the Task
returned by OnInitializeAsync
completes for pages marked with the @staticpage
directive? Since it's a new directive, I would expect it to be pretty non-breaking. Sure, it might affect how existing components render, but so does ignoring @rendermode
values.
@gabephudson it sounds like you are struggling with state management with mixed render modes. This is indeed a very big challenge in Blazor in .NET8 and is actually aligned with my first bullet point - “please make existing Blazor scenarios work 100%”. In Oqtane I am manually passing state as serializable parameters across the render mode boundary which is fairly simple to implement and works well. Maybe we just need some better guidance on this item as it seems to be the area where most developers are struggling… and it is potentially a better solution than introducing yet another render mode (with additional state management challenges)
Agreed that state management across render modes is indeed a challenge and I wish there was a recommended or "in the box" way to do this in 8. When moving to 8, this was the main challenge, and I was surprised that the dotnet session state provider (the logical place to look to solve this) does not seem to support Interactve pages. (Would love to hear more about how you have solved this issue by using serialized parameters).
That said, this is a related but separate issue from this. Don't want to derail this thread more. I was going to comment on how even the templates have to use a "workaround" so that Login and Error pages are rendered statically in a global interactive app, but @halter73 beat me to it. Bottomline, this issue is here because the current workaround is unintuitive and verbose.
Bottom line, sounds like you are a vote for Option 1; and you make good arguments for this. 👍
This is great, @SteveSandersonMS!
There are some really interesting ideas in this proposal and I especially appreciate how clearly the limitations for option 2 are conveyed.
Of the two options, proposed as they are, I would definitely prefer the first. There were a lot of nuances introduced in .NET 8 that can already be hard to wrap one's head around, so it seems less than ideal to introduce additional mechanisms that appear to be flexible but are actually constrained a small number of well-known usage patterns.
That said, I think it's worth exploring the advantages that the second approach provides and determining if there's a way to minimize/eliminate the downsides.
However, the problems start if you do any of the following:
<Routes @rendermode="InteractiveServer" /> <Routes @rendermode="@PageRenderMode" /> <Routes @rendermode="@(PageRenderMode ?? StaticServer)" />
...
Correct me if I'm wrong, but I think even the expected usage...
<Routes @rendermode="@(PageRenderMode ?? InteractiveServer)" />
...is also susceptible to unintuitive behavior: If you navigate to a page with an explicit InteractiveWebAssembly
render mode, then navigate to a page with no explicit render mode, then WebAssembly interactivity will continue to be used. This does not reflect what's specified in the expression used to compute the <Routes>
render mode, which suggests that InteractiveServer
should be used on any page without an explicit render mode.
It seems to me like the general problem is that the approach requires application code (the render mode expression in App.razor
) to exactly match the logic that the interactive router uses when determining whether to reload the page. Having a framework feature that makes such a strong assumption about application logic seems problematic to me.
I believe the functionality that option 2 provides can be summarized simply: Prefer the page's explicit render mode (including a new StaticServer
render mode), with a default render mode for pages that don't specify one.
If that's the case, something like @javiercn's proposal has some great ideas. But I think we could probably make some subtle changes to reduce the likelihood of misusing the feature.
First, there wouldn't be any behavioral changes to how client-side routing works by default. Everything would be opt-in so all existing patterns (like the one Identity UI uses) are still valid.
If a customer wanted to use this feature, they would make the following changes to their app:
Program.cs
app.MapRazorComponents()
.AddServerComponents()
.AddWebAssemblyComponents()
+ .AddCascadingPageRenderMode(defaultRenderMode: InteractiveServer);
App.razor
-<Routes @rendermode="InteractiveServer" />
+<Routes @rendermode="PageRenderMode" />
...
@code {
+ [CascadingParameter]
+ public IComponentRenderMode PageRenderMode { get; set; }
}
If a defaultRenderMode
does not get supplied to AddCascadingPageRenderMode()
, then the default defaultRenderMode
is always StaticServer
. I think it would be too spooky to automatically select a default interactive render mode based on which interactive render modes are enabled.
Then the one additional bit of information the interactive router needs is whether the default page render mode is compatible with the current interactive render mode. If it is compatible, then allow interactive navigation to that page. If not, then reload when navigating to that page.
I believe this approach addresses most of the concerning scenarios presented so far:
1.
@* This looks like sensible code, but the behavior is subtle and strange - see below *@
<Routes @rendermode="@PageRenderMode" />
The original problem with this was that interactively navigating to a page with no explicit render mode results in staying on the current render mode. This doesn't happen with the proposed alternative, because the interactive router is aware of whether the default render mode is compatible with its current render mode. If it's not compatible, a refresh will occur and the render mode will be re-evaluated on the server.
2.
@* This also looks sensible (i.e., define SSR as the default), but again the behavior is NOT what you think *@
<Routes @rendermode="@(PageRenderMode ?? StaticServer)" />
The proposed alternative doesn't have this issue because the default gets expressed not through the @rendermode
expression, but instead via the call to AddCascadingPageRenderMode()
. As mentioned above, the client-side router knows when the specified default render mode is not compatible with the current one.
3.
@* Existing global-interactivity apps *@
<Routes @rendermode="InteractiveServer" />
The concern with this was that the StaticServer
render mode wouldn't work in this scenario without requiring changes to application code. This would also be true with the proposed alternative, but we could probably catch this case at runtime and throw a detailed error that suggests what the problem is.
From some of @SteveSandersonMS's recently-expressed concerns:
It's going to be really weird that you even can have
@rendermode="SomethingSpecific"
inApp.razor
given that it wouldn't agree with the new rules around how we build the interactive route table. It continues to look like you can put arbitrary logic inApp.razor
to pick the rendermode dynamically (e.g., based on URL, some HTTP header, or whatever), which is a genuinely useful feature today. However, that arbitrary logic will not compose properly with@rendermode StaticServer
or possibly other cross-mode navigations since the router will not be using that logic when it builds its route table.
I agree with all of this, although it's not a problem by default - only if you use .AddCascadingPageRenderMode()
. We might be able to create an analyzer that warns if it detects that you pass anything other than the cascading PageRenderMode
to a component rendering a <Router>
(but there's still a possibility that the analyzer misses a more sophisticated scenario, and we do have to pay the cost of maintaining the analyzer).
While I think it's still worth discussing various directions to take option 2, I'm leaning toward option 1. That said, option 1 still has a potentially confusing bit of behavior, which is that @rendermode
gets ignored/disabled. But if the naming/syntax made it clear enough that this was a switch completely disabling interactivity for the page, then this might be fine.
In addition, AFAICT, taking option 1 now doesn't necessarily prevent us from taking option 2 in the future if there's enough demand for it. Even if we add some variation of option 2 later, it might still be useful to have option 1 as a global override that completely disables render modes.
If we do option 1, would it be sensible to throw if any part of the layout or any subcomponent specifies a non-null @rendermode
for a page with the @staticpage
directive rather than ignore it? It's a bit more restrictive, but makes it clear what's going on.
@halter73 I actually do not agree that the default Blazor template with Auth enabled is validation that this feature is needed.
If you are developing globally interactive applications then it can be assumed all components are using Blazor interactivity… and you would actually prefer to use interactive Auth components as well… however you cannot because Microsoft only provided static versions of these components in .NET 8. If Microsoft had provided interactive Auth components there would be no need for ugly hacks in App.razor and there would be no need for the enhancement being debated in this issue.
I actually do not understand why the Auth components were developed as static components… as the type of functionality they represent is a perfect example of the scenario where you would choose to use an interactive component within a statically rendered application. The Oqtane Framework (https://www.oqtane.org) has interactive components for all Auth scenarios (ie, login, register, etc…) which work perfectly whether you choose to use global interactivity or static rendering in your app.
@gabephudson To clarify, I am not voting for option 1 or 2… I am suggesting that this feature should not be implemented at all.
Thanks everyone for the additional thoughts! Especially @MackinnonBuck for the very detailed analysis.
The "option 2" refinements that @javiercn and @MackinnonBuck are describing do indeed address some of the more severe weirdnesses of my original option 2. My sense is that we could make something along those lines work if we considered it important enough, albeit with remaining costs/challenges:
StaticServer
rendermode won't work)StaticServer
rendermode (e.g., can't use it on @rendermode=...
attributes)However some of the following feedback backs up my instinct that it would be a wrong direction right now:
After hearing all this, I'm sold on the idea that "requiring upgrade steps" on its own is enough to invalidate a design (I don't mean that universally, but for this feature). It would be OK if the upgrade steps were innately obvious (e.g., "to use a new DI service, first register it in your DI container"), but it's definitely not obvious that using @rendermode StaticServer
on one place also requires arbitrary-looking boilerplate changes in App.razor
. The ecosystem does not have an appetite for learning about more combinations that do and don't work together, or for debugging problems that happen if you didn't upgrade your app properly.
[javiercn] Another thing that I realized now is that option 1 is all or nothing, meaning that no component is going to be able to render interactively (for example in your layout) if your page requires static rendering. [mackinnonbuck] option 1 still has a potentially confusing bit of behavior, which is that @rendermode gets ignored/disabled
This is an excellent point - thanks for bringing it up, @javiercn! I thought about this a bunch more and think it can be mitigated in a quite satisfying way. That is, we would define the meaning of @attribute [StaticPage]
or @staticpage
as follows:
"Suppresses any @rendermode
directives in your root component"
Now I haven't actually prototyped this so maybe I'm missing something, but this small tweak to the definition seems to evade the entire limitation. I know at first glance it feels a bit arbitrary, but there's nothing hard to understand about it, and AFAICT gives exactly the behaviors everyone would want:
I know in theory people could structure their site in more unexpected ways, for example having some combination of other components wrapped around their router, with @rendermode
in one of them. In my opinion the high benefit of fixing the "all or nothing" problem is worth that, since there's still a simple and clear definition of "static page" that should be understandable.
If we were hyper-determined to give even more options, we could have some further enum parameter like StaticPageMode.SuppressRootRenderModes
(default) or StaticPageMode.SuppressAllRenderModes
, but that's obviously something we could layer on in the future if enough demand emerged.
Here's what I now propose to do:
@attribute [StaticPage]
@staticpage
because the whole point of supporting @attribute
is to avoid incurring the costs of new syntax for every feature. We could later add an equivalent shorthand syntax, but right now I don't see a reason to think this is more syntax-worthy than @attribute [StreamRendering]
If we were going further, the next highest-priority thing would be something like an analyzer that fails if you try to use this on a non-page component, or if you try to use it on a page component that itself has @rendermode
. However on a cost-benefit basis I don't expect to implement such an analyzer, but it's something we could come back to late in the product cycle if by that stage we have evidence that it's top of our priority queue at that point.
I actually do not understand why the Auth components were developed as static components…
I was wondering this at well when first exposed to the templates. I have come to the following conclusions (total assumptions by me)...
If you are developing globally interactive applications then it can be assumed all components are using Blazor interactivity… and you would actually prefer to use interactive Auth components as well… however you cannot because Microsoft only provided static versions of these components in .NET 8. If Microsoft had provided interactive Auth components there would be no need for ugly hacks in App.razor and there would be no need for the enhancement being debated in this issue.
I actually do not understand why the Auth components were developed as static components… as the type of functionality they represent is a perfect example of the scenario where you would choose to use an interactive component within a statically rendered application. The Oqtane Framework (https://www.oqtane.org) has interactive components for all Auth scenarios (ie, login, register, etc…) which work perfectly whether you choose to use global interactivity or static rendering in your app.
You need to set a cookie and that's done via a traditional form post. These components need to work without JavaScript enabled, which might not be a constraint that you have, but most other people do.
@javiercn you are correct that Oqtane does not have the requirement to support non-JavaScript clients (its interactive login component uses JS Interop to call a JS method which posts values to a razor page endpoint which uses standard Identity to set the cookie and redirect). However, I would still suggest that the majority of the Identity Auth components could have been offered as interactive components. The exception is Login which could have been implemented as a page component which does not specify @rendermode so that it inherits the render mode from the project (either static or interactive)... and the Login component could use the static form onsubmit approach which works in all render modes. However I am sure there would then be complaints from the purists who want a pure static solution - unfortunately you can never satisfy everyone.
Thanks for the writeup, @SteveSandersonMS.
I'm trying to understand the experience in a situation, where a component has defined a render mode, but it's being use from a page, that is defined as static
. Here is a diagram to visualize the scenario:
flowchart TD
P[Static Page] --> C1[Component 1 - no rendermode defined]
P --> C2[Component 2 - InteractiveWebAssembly]
C1 --> C3[Component 3 - InteractiveServer]
I assume the following is going to be the case, but I would like to get confirmation:
rendermode
from the page, so it will be rendered staticallyCan you please confirm my understanding?
Assuming this is the case, I think we should produce some warnings
at runtime indicating, that the developer is not going to get the experience they actually intend to and that the rendermode defined for Component 2 and 3 will be ignored.
Sharing my feedback on introducing a new rendering mode from the position of a Blazor Component vendor who speaks daily with lots of Blazor developers.
I am backing @sbwalker on all his points. I too think that this feature shouldn't be implemented.
Some Blazor developers (especially the new ones) are having a hard time understanding the existing rendering modes. I can only imagine what a new rendering mode would do to them. As a Blazor UI component vendor we are still seeing the question "why aren't your components firing any events". Of course that's because static rendering mode doesn't support interactivity. Some recent examples: https://forum.radzen.com/t/radzen-profile-menu-not-showing-up/16893 https://forum.radzen.com/t/radzen-blazor-button-click-not-firing/3853/5 https://forum.radzen.com/t/layout-icon-sidebar-example-not-reacting-to-user-input/16649
To this day I don't know why static rendering mode was picked as the default. A comparison with React SSR was made but it isn't 100% equivalent - React SSR still hydrates to client-side and supports events (interactivity) just fine. Also I don't think React SSR is the default React mode and the getting started tutorial about events doesn't mention anything about rendering modes as they are advanced and opt-in concept.
Another major point where I back @sbwalker is that authentication could have been implemented with interactive components and without HttpContext access. Our tool Radzen has been doing this since Blazor 1. Yes it needs a cookie to be set - we set it in a controller action method which the login form posts to. It doesn't even require JavaScript. I am starting to think that static rendering mode was implemented to support HttpContext access and the new authentication components.
As a Blazor developer that has been working with this tech since its beta releases, I will add my two cents to this discussion because this was quite frustrating issue recently to me and why the first option is more important than you guys imagine.
Imagine the scenario, you are developing a consumer facing website, with lots of users daily, so, your first thought would be, lets use SSR as the main rendering mode, but than you want to add more complex UI functionality on some 'lnternal/logged in' area only, so you switch to auto with per page/component, you develop the internal area with a lot of functionality and decide that those internal pages should render as interactiveWebassembly only, but than, every single route on the 'internal' area always hit the server before loading the page,even after the wasm was download, and you think 'this is uggly, if its wasm already, why not only load the page without going to the server', but thats by design, without the global render being InteractiveWebassembly every route will go to the server.
With proposal 1, I could for example use a global 'InteraciveWebassembly' render mode, and only on the most consumed pages (home and others not internal) use the attribute [StaticPage], literally saving me from turning the entire 'internal pages' in a single page with lots of components, because i don't want my wasm routes on internal pages to hit the server every single time the user clicks some link on the wasm.
And this my guys, is what blazor devs are after, some dumb pages with SSR, and some smart ones in interactive wasm, but without the routes hitting the server after is loaded. That simple, and should actually be available as soon as possible because devs are going to do things the wrong way to overcome those limitations/issues and when you release this, they will be pissed to have to rework everything again and say 'why was not like this from the start?'.
So that is my 2 cents, hope it help you guys decide in the best approach for this.
We provide a way to opt into interactivity on a per page or component level. When interactivity is enabled globally, there should similarly be a way to opt out of interactivity so that architectural choices made for one part of your app don't limit your choices in other parts of your app. For example, you might have an app that is setup to use interactive WebAssembly rendering globally, but you want to add a page that only renders statically from the server.