-
Notifications
You must be signed in to change notification settings - Fork 10k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Provide a mechanism for rendering Blazor pages statically even when the app is set up for interactive rendering #51046
Comments
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.
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?
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" |
How is this accomplished?
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).
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.
Agreed. I'm happy to update the original post description accordingly. |
Internal stuff, not exposed as public API. The renderer explicitly checks if the current HttpContext contains
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 |
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 |
I'll add some complexity to this:
It seems to me like there are a couple of possible solutions that could solve all of the above. Suggestions:
|
Thanks @AbstractionsAs for the feedback!
I think these will be covered by #49401.
I believe in .NET 9 Preview 2 you can now do optional service dependencies in components using constructor injection. |
@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. 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 If on the contrary we do something like what I suggested, I think that can end up being something like:
Then the only thing to do remains defining the "default global render mode" which we could do in the call to
|
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?
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 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
That said, I definitely understand the problems faced with |
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). |
What I'm taking from this is the notion that, instead of resolving the rendermode via arbitrary code inside 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 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:
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".
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:
|
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) |
I think the presence of the following in <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 I think option 1 where we add a very limited 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 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 Also, this is a bit of a tangent, but would it be possible to delay all rendering until the |
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! High-level thoughtsThere 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. Additional option 2 considerations
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 It seems to me like the general problem is that the approach requires application code (the render mode expression in Option 2 alternativeI believe the functionality that option 2 provides can be summarized simply: Prefer the page's explicit render mode (including a new 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 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: @* 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. @* 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 @* Existing global-interactivity apps *@
<Routes @rendermode="InteractiveServer" /> The concern with this was that the From some of @SteveSandersonMS's recently-expressed concerns:
I agree with all of this, although it's not a problem by default - only if you use Revisiting option 1While 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 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 |
@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:
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 The all-or-nothing limitation
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 "Suppresses any 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 If we were hyper-determined to give even more options, we could have some further enum parameter like ProposalHere's what I now propose to do:
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 |
I was wondering this at well when first exposed to the templates. I have come to the following conclusions (total assumptions by me)...
|
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 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:
Can you please confirm my understanding? Assuming this is the case, I think we should produce some |
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: 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. |
Done in #55157 |
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.
The text was updated successfully, but these errors were encountered: