Description
There are multiple rendering platforms/environments:
- WebAssembly
- Server
- SSR/prerendering
- WebView
- Anything else (bUnit, custom renderers, etc.)
We've resisted having an API that literally just tells you which one is currently in effect, reasoning that components should be agnostic to this and only vary their behavior indirectly, e.g.:
- Is an HTTP context available? Find out by calling
serviceProvider.GetService<IHttpContextAccessor>()
- Is a JavaScript runtime reachable? Only do JS interop inside
OnAfterRenderAsync
, and then it won't execute if not.
What's changing?
With .NET 8, people will be writing a lot more code that's specialized to SSR. The actual UX of a component may frequently vary based on this. For example, you might choose to render some buttons in a greyed-out way for SSR/prerendering, and then light up if the component becomes interactive. Or you might render completely different UI given the restrictions of noninteractivity (reducing something to a basic HTML form that can be posted). There isn't currently a sensible way to vary UX based on whether interactivity is available; you just have to somehow know.
In the framework we also have reasons to distinguish. For example, InputBase
wants to emit field names in SSR cases and not otherwise. Currently we distinguish based on the presence of a FormMappingContext
, but that's a weak approximation and sometimes incorrect, since a FormMappingContext
could also be present in interactive components if the developer has put one there to make a form also work in SSR mode.
Proposal
I'd recommend something modelled on how .NET reports the current OS platform as a string (making it extensible) and has helpful extension methods to identify specific ones in a strongly-typed way. The main difference though is we can't have a static
API for this since it may vary within a single process. So:
- There would be a type like
class M.A.Components.ComponentPlatform(string PlatformName, bool Interactive)
Renderer
would haveComponentPlatform Platform { get; }
configured during the hosting environment startupRenderHandle
would haveComponentPlatform Platform => _renderer.Platform
ComponentBase
would haveComponentPlatform Platform => _renderHandle.Platform
- There would be extension methods like
Platform.IsStaticServer()
,Platform.IsInteractiveServer()
,Platform.IsWebAssembly()
,Platform.IsWebView()
,Platform.IsInteractive()
, ....- These could be defined within libraries that know about specific platform name strings (e.g., the WebView one can go in
M.A.C.WebView
, likewise for any custom hosting platforms).
- These could be defined within libraries that know about specific platform name strings (e.g., the WebView one can go in
I specifically do not propose we solve this with some kind of cascading parameter. We have to be careful about overusing them because they impose a cost of O(component depth)
to every [CascadingParameter]
declaration. In particular, adding these to InputBase
is not good given how apps may have UI with many hundreds of input fields at once (e.g., in an editable grid). We should try to reserve use of [CascadingParameter]
for application code and only use it in the framework in situations where we think only a small number of instances of the consumer will make sense to be used.
Likewise, I don't propose we solve this with a DI service. That again comes at a per-consumption cost. It's likely somewhat cheaper than CascadingParameter
(should be pretty much a single dictionary lookup) but still is a lot more machinery than we need for this.
Update: Also offer a way to find the rendermode
Besides knowing the current environment, it may be desirable to know the rendermode. That way even if you're currently prerendering (e.g., Platform.IsStaticServer()
), you can find out whether the component is scheduled to become interactive later, because it has a nonnull rendermode or is inheriting one from further up the hierarchy. You might want to change the actual shape of the render output based on whether you expect interactive buttons to light up in a moment.
Alternatives
Haven't really thought through the pros/cons, but instead of the platform being defined through a string name with extension methods to identify it, it could potentially be a type hierarchy. That would allow subclasses to provide extra custom information, while still adding extension methods on the root type that would try downcasting to the subclass they know about.
Not sure whether this is desirable or would even practically be useful. We don't consider "custom hosting models" to be a common use case, and the hosting model would be the thing constructing the instance, so it wouldn't really be very extensible anyway.
I was just trying to imagine scenarios like if M.A.C.WebView wants to be able to have platform.HasWebViewFlag(FlagName ...)
which it could do by subclassing ComponentPlatform
and then defining its own extension methods that read info from the subclass. Another possible approach would be putting a loosely-typed property bag on ComponentPlatform
.