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

[Blazor] Add an API to describe the render mode (if any) a component is running in #55577

Merged
merged 12 commits into from
May 14, 2024
22 changes: 22 additions & 0 deletions src/Components/Components/src/ComponentBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ namespace Microsoft.AspNetCore.Components;
public abstract class ComponentBase : IComponent, IHandleEvent, IHandleAfterRender
{
private readonly RenderFragment _renderFragment;
private (IComponentRenderMode? mode, bool cached) _renderMode;
private RenderHandle _renderHandle;
private bool _initialized;
private bool _hasNeverRendered = true;
Expand All @@ -41,6 +42,27 @@ public ComponentBase()
};
}

/// <summary>
/// Gets the <see cref="ComponentPlatform"/> the component is running on.
/// </summary>
protected ComponentPlatform Platform => _renderHandle.Platform;

/// <summary>
/// Gets the <see cref="IComponentRenderMode"/> assigned to this component.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is to avoid implying we are currently prerendering, or that prerendering is even applicable (since on WebView or standalone WebAssembly it wouldn't be applicable as a concept).

/// </summary>
protected IComponentRenderMode? AssignedRenderMode
{
get
{
if (!_renderMode.cached)
{
_renderMode = (_renderHandle.RenderMode, true);
}

return _renderMode.mode;
}
}

/// <summary>
/// Renders the component to the supplied <see cref="RenderTreeBuilder"/>.
/// </summary>
Expand Down
9 changes: 9 additions & 0 deletions src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
#nullable enable
Microsoft.AspNetCore.Components.ComponentBase.AssignedRenderMode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode?
Microsoft.AspNetCore.Components.ComponentBase.Platform.get -> Microsoft.AspNetCore.Components.ComponentPlatform!
Microsoft.AspNetCore.Components.ComponentPlatform
Microsoft.AspNetCore.Components.ComponentPlatform.ComponentPlatform(string! platformName, bool isInteractive) -> void
Microsoft.AspNetCore.Components.ComponentPlatform.IsInteractive.get -> bool
Microsoft.AspNetCore.Components.ComponentPlatform.Name.get -> string!
Microsoft.AspNetCore.Components.ExcludeFromInteractiveRoutingAttribute
Microsoft.AspNetCore.Components.ExcludeFromInteractiveRoutingAttribute.ExcludeFromInteractiveRoutingAttribute() -> void
Microsoft.AspNetCore.Components.RenderHandle.Platform.get -> Microsoft.AspNetCore.Components.ComponentPlatform!
Microsoft.AspNetCore.Components.RenderHandle.RenderMode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode?
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.ComponentPlatform.get -> Microsoft.AspNetCore.Components.ComponentPlatform!
22 changes: 22 additions & 0 deletions src/Components/Components/src/RenderHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,28 @@ public Dispatcher Dispatcher
internal bool IsRendererDisposed => _renderer?.Disposed
?? throw new InvalidOperationException("No renderer has been initialized.");

/// <summary>
/// Gets the <see cref="ComponentPlatform"/> the component is running on.
/// </summary>
public ComponentPlatform Platform => _renderer?.ComponentPlatform ?? throw new InvalidOperationException("No renderer has been initialized.");

/// <summary>
/// Retrieves the <see cref="IComponentRenderMode"/> assigned to the component.
/// </summary>
/// <returns>The <see cref="IComponentRenderMode"/> assigned to the component.</returns>
public IComponentRenderMode? RenderMode
{
get
{
if (_renderer == null)
{
throw new InvalidOperationException("No renderer has been initialized.");
}

return _renderer.GetComponentRenderMode(_componentId);
}
}

/// <summary>
/// Notifies the renderer that the component should be rendered.
/// </summary>
Expand Down
31 changes: 31 additions & 0 deletions src/Components/Components/src/RenderTree/ComponentPlatform.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Components;

/// <summary>
/// Provides information about the platform that the component is running on.
/// </summary>
public sealed class ComponentPlatform
{
/// <summary>
/// Constructs a new instance of <see cref="ComponentPlatform"/>.
/// </summary>
/// <param name="platformName">The name of the platform.</param>
/// <param name="isInteractive">A flag to indicate if the platform is interactive.</param>
public ComponentPlatform(string platformName, bool isInteractive)
{
Name = platformName;
IsInteractive = isInteractive;
}

/// <summary>
/// Gets the name of the platform.
/// </summary>
public string Name { get; }

/// <summary>
/// Gets a flag to indicate if the platform is interactive.
/// </summary>
public bool IsInteractive { get; }
}
8 changes: 8 additions & 0 deletions src/Components/Components/src/RenderTree/Renderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ protected ComponentState GetComponentState(int componentId)
protected internal virtual IComponentRenderMode? GetComponentRenderMode(IComponent component)
=> null;

internal IComponentRenderMode? GetComponentRenderMode(int componentId)
=> GetComponentRenderMode(GetRequiredComponentState(componentId).Component);

/// <summary>
/// Resolves the component state for a given <see cref="IComponent"/> instance.
/// </summary>
Expand All @@ -150,6 +153,11 @@ protected ComponentState GetComponentState(int componentId)
protected internal ComponentState GetComponentState(IComponent component)
=> _componentStateByComponent.GetValueOrDefault(component);

/// <summary>
/// Gets the <see cref="ComponentPlatform"/> associated with this <see cref="Renderer"/>.
/// </summary>
protected internal virtual ComponentPlatform ComponentPlatform { get; }

private async void RenderRootComponentsOnHotReload()
{
// Before re-rendering the root component, also clear any well-known caches in the framework
Expand Down
5 changes: 5 additions & 0 deletions src/Components/Server/src/Circuits/RemoteRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal partial class RemoteRenderer : WebRenderer
#pragma warning restore CA1852 // Seal internal types
{
private static readonly Task CanceledTask = Task.FromCanceled(new CancellationToken(canceled: true));
private static readonly ComponentPlatform _componentPlatform = new("Server", isInteractive: true);

private readonly CircuitClientProxy _client;
private readonly CircuitOptions _options;
Expand Down Expand Up @@ -56,6 +57,10 @@ public RemoteRenderer(

public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();

protected override ComponentPlatform ComponentPlatform => _componentPlatform;

protected override IComponentRenderMode? GetComponentRenderMode(IComponent component) => RenderMode.InteractiveServer;

public Task AddComponentAsync(Type componentType, ParameterView parameters, string domElementSelector)
{
var componentId = AddRootComponent(componentType, domElementSelector);
Expand Down
5 changes: 5 additions & 0 deletions src/Components/Web/src/HtmlRendering/StaticHtmlRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ namespace Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure;
/// </summary>
public partial class StaticHtmlRenderer : Renderer
{
private static readonly ComponentPlatform _componentPlatform = new ComponentPlatform("Static", isInteractive: false);

private static readonly Task CanceledRenderTask = Task.FromCanceled(new CancellationToken(canceled: true));
private readonly NavigationManager? _navigationManager;

Expand All @@ -38,6 +40,9 @@ public StaticHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory logge
/// <inheritdoc/>
public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();

/// <inheritdoc/>
protected internal override ComponentPlatform ComponentPlatform => _componentPlatform;

/// <summary>
/// Adds a root component of the specified type and begins rendering it.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/Components/Web/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ Microsoft.AspNetCore.Components.Web.Internal.IInternalWebJSInProcessRuntime
Microsoft.AspNetCore.Components.Web.Internal.IInternalWebJSInProcessRuntime.InvokeJS(string! identifier, string? argsJson, Microsoft.JSInterop.JSCallResultType resultType, long targetInstanceId) -> string!
Microsoft.AspNetCore.Components.Web.KeyboardEventArgs.IsComposing.get -> bool
Microsoft.AspNetCore.Components.Web.KeyboardEventArgs.IsComposing.set -> void
override Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.ComponentPlatform.get -> Microsoft.AspNetCore.Components.ComponentPlatform!
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ internal sealed partial class WebAssemblyRenderer : WebRenderer
private readonly ILogger _logger;
private readonly Dispatcher _dispatcher;
private readonly IInternalJSImportMethods _jsMethods;
private static readonly ComponentPlatform _componentPlatform = new("WebAssembly", isInteractive: true);

public WebAssemblyRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, JSComponentInterop jsComponentInterop)
: base(serviceProvider, loggerFactory, DefaultWebAssemblyJSRuntime.Instance.ReadJsonSerializerOptions(), jsComponentInterop)
Expand Down Expand Up @@ -72,11 +73,15 @@ private void OnUpdateRootComponents(RootComponentOperationBatch batch)
NotifyEndUpdateRootComponents(batch.BatchId);
}

protected override IComponentRenderMode? GetComponentRenderMode(IComponent component) => RenderMode.InteractiveWebAssembly;

public void NotifyEndUpdateRootComponents(long batchId)
{
_jsMethods.EndUpdateRootComponents(batchId);
}

protected override ComponentPlatform ComponentPlatform => _componentPlatform;

public override Dispatcher Dispatcher => _dispatcher;

public Task AddComponentAsync([DynamicallyAccessedMembers(Component)] Type componentType, ParameterView parameters, string domElementSelector)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Microsoft.AspNetCore.Components.WebView.Services;

internal sealed class WebViewRenderer : WebRenderer
{
private static readonly ComponentPlatform _componentPlatform = new("WebView", isInteractive: true);
private readonly Queue<UnacknowledgedRenderBatch> _unacknowledgedRenderBatches = new();
private readonly Dispatcher _dispatcher;
private readonly IpcSender _ipcSender;
Expand All @@ -31,6 +32,8 @@ public WebViewRenderer(

public override Dispatcher Dispatcher => _dispatcher;

protected override ComponentPlatform ComponentPlatform => _componentPlatform;

protected override int GetWebRendererId() => (int)WebRendererId.WebView;

protected override void HandleException(Exception exception)
Expand Down Expand Up @@ -81,7 +84,7 @@ public void NotifyRenderCompleted(long batchId)
private sealed class UnacknowledgedRenderBatch
{
public long BatchId { get; init; }

public TaskCompletionSource CompletionSource { get; init; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ public GridScenario() : base("grid")
protected override async Task ExecuteAsync(ConsoleHostRenderer renderer, int numCycles)
{
var gridType = _gridTypeOption.HasValue()
? (GridRendering.RenderMode)Enum.Parse(typeof(GridRendering.RenderMode), _gridTypeOption.Value(), true)
: GridRendering.RenderMode.FastGrid;
? (GridRendering.GridRenderMode)Enum.Parse(typeof(GridRendering.GridRenderMode), _gridTypeOption.Value(), true)
: GridRendering.GridRenderMode.FastGrid;
Copy link
Member

@SteveSandersonMS SteveSandersonMS May 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose this shows it's a breaking change in a super niche sense, but we have to accept that otherwise we couldn't extend the base class at all. If there's a lot of pushback in previews we can consider options.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we decided to go with AssignedRenderMode instead of just RenderMode for the property, hopefully it will be less breaking.


for (var i = 0; i < numCycles; i++)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

<fieldset>
<select id="render-mode" @bind="SelectedRenderMode">
<option>@RenderMode.FastGrid</option>
<option>@RenderMode.PlainTable</option>
<option>@RenderMode.ComplexTable</option>
<option>@GridRenderMode.FastGrid</option>
<option>@GridRenderMode.PlainTable</option>
<option>@GridRenderMode.ComplexTable</option>
</select>

<button id="show" @onclick="Show">Show</button>
Expand All @@ -23,7 +23,7 @@
{
<p><em>(No data assigned)</em></p>
}
else if (SelectedRenderMode == RenderMode.FastGrid)
else if (SelectedRenderMode == GridRenderMode.FastGrid)
{
<p>FastGrid represents a minimal, optimized implementation of a grid.</p>

Expand All @@ -50,23 +50,23 @@ else if (SelectedRenderMode == RenderMode.FastGrid)
<GridColumn TRowData="WeatherForecast" Title="Summary">@context.Summary</GridColumn>
</Grid>
}
else if (SelectedRenderMode == RenderMode.PlainTable)
else if (SelectedRenderMode == GridRenderMode.PlainTable)
{
<p>PlainTable represents a minimal but not optimized implementation of a grid.</p>

<Wasm.Performance.TestApp.Shared.PlainTable.TableComponent Data="@forecasts" Columns="@Columns" />
}
else if (SelectedRenderMode == RenderMode.ComplexTable)
else if (SelectedRenderMode == GridRenderMode.ComplexTable)
{
<p>ComplexTable represents a maximal, not optimized implementation of a grid, using a wide range of Blazor features at once.</p>

<Wasm.Performance.TestApp.Shared.ComplexTable.TableComponent Data="@forecasts" Columns="@Columns" />
}

@code {
public enum RenderMode { PlainTable, ComplexTable, FastGrid }
public enum GridRenderMode { PlainTable, ComplexTable, FastGrid }

public RenderMode SelectedRenderMode { get; set; } = RenderMode.FastGrid;
public GridRenderMode SelectedRenderMode { get; set; } = GridRenderMode.FastGrid;

private WeatherForecast[] forecasts;
public List<string> Columns { get; set; } = new List<string>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Components.TestServer.RazorComponents;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using TestServer;
using Xunit.Abstractions;

namespace Microsoft.AspNetCore.Components.E2ETests.Tests;

public class InteractiveHostRendermodeTest : ServerTestBase<BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>>>
{
public InteractiveHostRendermodeTest(
BrowserFixture browserFixture,
BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
}

[Theory]
[InlineData("server")]
[InlineData("webassembly")]
[InlineData("auto")]
[InlineData("static")]
public void EmbeddingServerAppInsideIframe_Works(string renderMode)
{
Navigate($"/subdir/ComponentPlatform?suppress-autostart&ComponentRenderMode={renderMode}");

Browser.Equal(renderMode, () => Browser.Exists(By.Id("host-render-mode")).Text);
Browser.Equal("False", () => Browser.Exists(By.Id("platform-is-interactive")).Text);

Browser.Click(By.Id("call-blazor-start"));

if (renderMode == "static")
{
Browser.Equal("False", () => Browser.Exists(By.Id("platform-is-interactive")).Text);
}
else
{
Browser.Equal("True", () => Browser.Exists(By.Id("platform-is-interactive")).Text);
}

if (renderMode != "auto")
{
Browser.Equal(renderMode, () => Browser.Exists(By.Id("host-render-mode")).Text);
}
else
{
Browser.True(() => Browser.Exists(By.Id("host-render-mode")).Text is "server" or "webassembly");
}
}
}

1 change: 1 addition & 0 deletions src/Components/test/testassets/BasicTestApp/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
<option value="BasicTestApp.GracefulTermination">Graceful Termination</option>
<option value="BasicTestApp.HeadModification">Head Modification</option>
<option value="BasicTestApp.HierarchicalImportsTest.Subdir.ComponentUsingImports">Imports statement</option>
<option value="BasicTestApp.HostRenderMode">Host render mode</option>
<option value="BasicTestApp.HtmlBlockChildContent">ChildContent HTML Block</option>
<option value="BasicTestApp.HtmlEncodedChildContent">ChildContent HTML Encoded Block</option>
<option value="BasicTestApp.HtmlMixedChildContent">ChildContent Mixed Block</option>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
@page "/componentplatform"
@using TestContentPackage

<h3>Component platform tests</h3>

<p>
Defines a component and applies the render mode in the query string value for ComponentRenderMode.
The component prints the render mode and whether its interactive.
</p>

<ComponentPlatformDetails @rendermode="_renderMode" />

@code {
[SupplyParameterFromQuery] public string ComponentRenderMode { get; set; }

IComponentRenderMode _renderMode;

protected override void OnInitialized()
{
switch (ComponentRenderMode)
{
case "server":
_renderMode = RenderMode.InteractiveServer;
break;
case "webassembly":
_renderMode = RenderMode.InteractiveWebAssembly;
break;
case "auto":
_renderMode = RenderMode.InteractiveAuto;
break;
case "static":
_renderMode = null;
break;
default:
throw new InvalidOperationException($"Unknown component render mode: {ComponentRenderMode}");
}
}
}
Loading
Loading