Skip to content
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

Add an API to tell you the current render mode #49401

Closed
SteveSandersonMS opened this issue Jul 14, 2023 · 16 comments · Fixed by #55577
Closed

Add an API to tell you the current render mode #49401

SteveSandersonMS opened this issue Jul 14, 2023 · 16 comments · Fixed by #55577
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-blazor Includes: Blazor, Razor Components enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-full-stack-web-ui Full stack web UI with Blazor Pillar: Complete Blazor Web Priority:0 Work that we can't release without
Milestone

Comments

@SteveSandersonMS
Copy link
Member

SteveSandersonMS commented Jul 14, 2023

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 have ComponentPlatform Platform { get; } configured during the hosting environment startup
  • RenderHandle would have ComponentPlatform Platform => _renderer.Platform
  • ComponentBase would have ComponentPlatform 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).

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.

@SteveSandersonMS SteveSandersonMS added the area-blazor Includes: Blazor, Razor Components label Jul 14, 2023
@danroth27 danroth27 added this to the Backlog milestone Jul 17, 2023
@ghost
Copy link

ghost commented Jul 17, 2023

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

@marinasundstrom
Copy link

There should be APIs to determine at least what context a component is rendering in.

I have created my own RenderingContext class with properties IsServer, IsClient, and IsPrerendering. That serves the purpose well for me since I'm not running anywhere else than in the browsers.

https://github.com/marinasundstrom/Blazor8Test/blob/main/src/Shared/RenderingContext.cs

Besides this, I'm also looking for a way to determine whether or not an interactive component has been mounted and activated in the browser so that I know when to execute the Javascript.

#49497

@SteveSandersonMS
Copy link
Member Author

SteveSandersonMS commented Jul 20, 2023

I have created my own RenderingContext class with properties IsServer, IsClient, and IsPrerendering. That serves the purpose well for me since I'm not running anywhere else than in the browsers.

Sounds good! Glad to hear this is a generally useful concept.

Besides this, I'm also looking for a way to determine whether or not an interactive component has been mounted and activated in the browser so that I know when to execute the Javascript.

Hopefully OnAfterRenderAsync is suitable for that requirement.

@ladeak
Copy link
Contributor

ladeak commented Sep 13, 2023

Another approach could be something like RenderModeSelector provided:

<RenderModeSelector>
    <PreRender>
          <MyPlaceholderComponentA>
   </PreRender>
    <WebAssembly>
          <MyComponentA>
   </WebAssembly>
</RenderModeSelector>

where PreRender == SSR.

@marinasundstrom
Copy link

marinasundstrom commented Sep 13, 2023

@ladeak In my own experience, I don't see a use for this type of component. To me, it just makes the markup harder to read when in for instance a page component. It is just a glorified switch statement.

This kind of component pattern is OK for the router, and perhaps ErrorBoundary, but I wouldn't like people to abuse this component.

I'd rather see that people learn about how pre-rendering works, than for Blazor to to hide the details for them in some React-like pattern from the start.

I just think we should wait and see whether this is a good idea or not.

@ladeak
Copy link
Contributor

ladeak commented Sep 13, 2023

Sure, although I agree @marinasundstrom I wanted to offer an alternative, where the custom components can remain agnostic to render mode.

I think the original proposal is very useful: with ASP.NET Core 8 when I have a component tree in SSR or server rendered and one component with rendermode: webassembly, I get a refresh in the browser that moves components around as soon as that component gets rendered eventually on the browser. It looks very undesired.

@capdiem
Copy link

capdiem commented Sep 14, 2023

This seems to work.

public enum RenderingEnvironment
{
    Unknown,
    Server,
    WebAssembly,
    SSRServer,
    SSRWebAssembly,
    WebView
}
RenderingEnvironment renderingEnvironment;
var servicesProvider = builder.Services.BuildServiceProvider();
var isSSR = servicesProvider.GetService<IComponentPrerenderer>() is not null;
var jsRuntime = servicesProvider.GetRequiredService<IJSRuntime>();
if (isSSR)
{
    renderingEnvironment = jsRuntime is IJSInProcessRuntime 
        ? RenderingEnvironment.SSRWebAssembly 
        : RenderingEnvironment.SSRServer;
}
else if (jsRuntime is JSInProcessRuntime)
{
    renderingEnvironment = RenderingEnvironment.WebAssembly;
}
else if (jsRuntime.GetType().Name == "RemoteJSRuntime")
{
    renderingEnvironment = RenderingEnvironment.Server;
}
else if (jsRuntime.GetType().Name == "WebViewJSRuntime")
{
    renderingEnvironment = RenderingEnvironment.WebView;
}
else
{
    renderingEnvironment = RenderingEnvironment.Unknown;
}

@mkArtakMSFT mkArtakMSFT modified the milestones: Backlog, BlazorPlanning Nov 5, 2023
@danroth27 danroth27 changed the title Consider adding an API to tell you the current rendering platform Consider adding an API to tell you the current render mode Nov 17, 2023
@ladeak
Copy link
Contributor

ladeak commented Nov 18, 2023

RenderingEnvironment renderingEnvironment;
var servicesProvider = builder.Services.BuildServiceProvider();
var isSSR = servicesProvider.GetService<IComponentPrerenderer>() is not null;
var jsRuntime = servicesProvider.GetRequiredService<IJSRuntime>();
...

The above suggested solution does not seem to work when I navigate with links on the page.

@danielgreen
Copy link

danielgreen commented Dec 1, 2023

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.

Until this ability is available, prerendering offers a poor user experience. Even in the Counter.razor component that ships with the .NET 8 Blazor Web App template, the "Click me" button does nothing when initially prerendered, for a second or two until Web Assembly interactivity kicks in.

Is there no recommended way to mitigate it for the time being, other than disabling prerendering on the component?

By using reflection on the IJSRuntime, it seems to be possible to detect prerendering.
But YMMV.

public class RenderContext(IJSRuntime jsRuntime)
{
    private const string RemoteJSRuntime = nameof(RemoteJSRuntime);
    private const string IsInitialized = nameof(IsInitialized);
    private readonly Type JSRuntimeType = jsRuntime.GetType();
    private readonly PropertyInfo? IsInitializedProperty = jsRuntime.GetType().GetProperty(IsInitialized);

    public bool IsWebAssembly() => OperatingSystem.IsBrowser();

    public bool IsServer() => !IsWebAssembly();

    public bool IsPreRendering() => IsServer() && 
                                    JSRuntimeType.Name == RemoteJSRuntime &&
                                    IsInitializedProperty?.GetValue(jsRuntime) is not true;
}

EDIT - please check out this post on issue #51468 for a cleaner RenderContext implementation by @bcheung4589.

Having separate RenderContext implementations for client and server, and detecting prerendering using HttpContext.Response avoids any need for reflection.

@bcheung4589
Copy link

Just wanna say my RenderContext was heavily inspired by marinasundstrom. Some months ago I went through multiple public repo's to look for inspiration on how people work and to learn about Blazor 8. So credit where credit is due, @marinasundstrom <3

@mkArtakMSFT mkArtakMSFT added the enhancement This issue represents an ask for new feature or an enhancement to an existing one label Dec 19, 2023
@ghost
Copy link

ghost commented Dec 19, 2023

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.

@mkArtakMSFT mkArtakMSFT added the feature-full-stack-web-ui Full stack web UI with Blazor label Dec 19, 2023
@AmerBakeer
Copy link

We're moving this issue to the .NET 9 Planning milestone for future evaluation / consideration.

Do we really have to wait a whole year just to determine the RenderMode that we are using in our code?

@javiercn
Copy link
Member

javiercn commented May 13, 2024

Sample

    protected override void OnInitialized()
    {
        if (GetRenderMode() is InteractiveServerRenderMode)
        {
            _message = $"Host Render Mode: Interactive Server";
        }
else if (GetRenderMode() is InteractiveWebAssemblyRenderMode)
        {
            _message = $"Host Render Mode: Interactive Webassembly";
        }
        else
        {
            _message = "Host Render Mode: Static";
        }
    }
}

APIs

namespace Microsoft.AspNetCore.Components;

public class ComponentBase
{
+  public IComponentRenderMode GetRenderMode();
+  public ComponentPlatform Platform { get; }
}

public struct RenderHandle
{
+  public IComponentRenderMode GetRenderMode();
+  public ComponentPlatform Platform { get; }
}

+public class ComponentPlatform(string platformName, bool isInteractive, IComponentRenderMode? renderMode)
+{
+   public string PlatformName { get; }
+   public bool IsInteractive { get; }
+}

@javiercn javiercn added the api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews label May 13, 2024
Copy link
Contributor

Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:

  • The PR contains changes to the reference-assembly that describe the API change. Or, you have included a snippet of reference-assembly-style code that illustrates the API change.
  • The PR describes the impact to users, both positive (useful new APIs) and negative (breaking changes).
  • Someone is assigned to "champion" this change in the meeting, and they understand the impact and design of the change.

@amcasey
Copy link
Member

amcasey commented May 13, 2024

[API Review]

  • Why isn't GetRenderMode a property?
    • We think it would create ambiguity with the RenderMode type, but we should confirm
    • In particular, static imports can make property names conflict with type names
    • It does do more work than one might expect a property to do
    • Could change the name
      • CurrentRenderMode (misleading)
      • TargetRenderMode (since it may not be in effect yet)
      • ComponentRenderMode
      • AssignedRenderMode (since it may not be in effect yet)
    • Resolved: make it a property - RenderMode if possible; AssignedRenderMode otherwise
  • Why are we using runtime type checks, rather than an enum?
    • The RenderMode object is userful in its own right
  • ComponentPlatform should be sealed
  • We're not convinced the render mode has to be passed to the ComponentPlatform ctor
  • RenderMode and platform are exposed separately since render mode is per-component and platform is shared
    • if (Platform.IsInteractive) { do JS interop }
  • PlatformName seems redundant - could just call it Name
  • Could the platform be an enum, rather than a string
    • We don't allow user extensibility
    • We do have some internal extensibility (bUnit, custom renderers, etc)
    • We could add extension methods (in the future)

@amcasey
Copy link
Member

amcasey commented May 13, 2024

namespace Microsoft.AspNetCore.Components;

public class ComponentBase
{
+  public IComponentRenderMode RenderMode { get; } // Or AssignedRenderMode
+  public ComponentPlatform Platform { get; }
}

public struct RenderHandle
{
+  public IComponentRenderMode RenderMode { readonly get; } // Or AssignedRenderMode
+  public ComponentPlatform Platform { readonly get; }
}

+public sealed class ComponentPlatform(string name, bool isInteractive)
+{
+   public string Name { get; }
+   public bool IsInteractive { get; }
+}

(Feel free to drop the readonlys if the compiler is unhappy.)

API approved!

@amcasey amcasey added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews labels May 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-approved API was approved in API review, it can be implemented area-blazor Includes: Blazor, Razor Components enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-full-stack-web-ui Full stack web UI with Blazor Pillar: Complete Blazor Web Priority:0 Work that we can't release without
Projects
None yet
Development

Successfully merging a pull request may close this issue.

13 participants