Skip to content

Commit

Permalink
[Blazor] Add an API to describe the render mode (if any) a component …
Browse files Browse the repository at this point in the history
…is running in (#55577)
  • Loading branch information
javiercn committed May 14, 2024
1 parent 3307bf6 commit 1b454f5
Show file tree
Hide file tree
Showing 16 changed files with 252 additions and 11 deletions.
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.
/// </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 @@ internal partial class RemoteRenderer : WebRenderer

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 @@ internal sealed class WebViewRenderer : WebRenderer

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;

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
57 changes: 57 additions & 0 deletions src/Components/test/E2ETest/Tests/InteractiveHostRendermodeTest.cs
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}");
}
}
}

0 comments on commit 1b454f5

Please sign in to comment.