From 1b454f57c664c1d43348c6c74cde7a2119813c8a Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Tue, 14 May 2024 18:19:20 +0200 Subject: [PATCH] [Blazor] Add an API to describe the render mode (if any) a component is running in (#55577) --- .../Components/src/ComponentBase.cs | 22 +++++++ .../Components/src/PublicAPI.Unshipped.txt | 9 +++ src/Components/Components/src/RenderHandle.cs | 22 +++++++ .../src/RenderTree/ComponentPlatform.cs | 31 ++++++++++ .../Components/src/RenderTree/Renderer.cs | 8 +++ .../Server/src/Circuits/RemoteRenderer.cs | 5 ++ .../src/HtmlRendering/StaticHtmlRenderer.cs | 5 ++ .../Web/src/PublicAPI.Unshipped.txt | 1 + .../src/Rendering/WebAssemblyRenderer.cs | 5 ++ .../WebView/src/Services/WebViewRenderer.cs | 5 +- .../ConsoleHost/Scenarios/GridScenario.cs | 4 +- .../TestApp/Pages/GridRendering.razor | 16 +++--- .../Tests/InteractiveHostRendermodeTest.cs | 57 +++++++++++++++++++ .../test/testassets/BasicTestApp/Index.razor | 1 + .../Pages/ComponentPlatform.razor | 38 +++++++++++++ .../ComponentPlatformDetails.razor | 34 +++++++++++ 16 files changed, 252 insertions(+), 11 deletions(-) create mode 100644 src/Components/Components/src/RenderTree/ComponentPlatform.cs create mode 100644 src/Components/test/E2ETest/Tests/InteractiveHostRendermodeTest.cs create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/ComponentPlatform.razor create mode 100644 src/Components/test/testassets/TestContentPackage/ComponentPlatformDetails.razor diff --git a/src/Components/Components/src/ComponentBase.cs b/src/Components/Components/src/ComponentBase.cs index 1b3896a77c9c..86b5c32fcea8 100644 --- a/src/Components/Components/src/ComponentBase.cs +++ b/src/Components/Components/src/ComponentBase.cs @@ -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; @@ -41,6 +42,27 @@ public ComponentBase() }; } + /// + /// Gets the the component is running on. + /// + protected ComponentPlatform Platform => _renderHandle.Platform; + + /// + /// Gets the assigned to this component. + /// + protected IComponentRenderMode? AssignedRenderMode + { + get + { + if (!_renderMode.cached) + { + _renderMode = (_renderHandle.RenderMode, true); + } + + return _renderMode.mode; + } + } + /// /// Renders the component to the supplied . /// diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 3f7272402f77..aa9e6551abe9 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -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! diff --git a/src/Components/Components/src/RenderHandle.cs b/src/Components/Components/src/RenderHandle.cs index fdd61f32ef3a..e26639e83b02 100644 --- a/src/Components/Components/src/RenderHandle.cs +++ b/src/Components/Components/src/RenderHandle.cs @@ -51,6 +51,28 @@ public Dispatcher Dispatcher internal bool IsRendererDisposed => _renderer?.Disposed ?? throw new InvalidOperationException("No renderer has been initialized."); + /// + /// Gets the the component is running on. + /// + public ComponentPlatform Platform => _renderer?.ComponentPlatform ?? throw new InvalidOperationException("No renderer has been initialized."); + + /// + /// Retrieves the assigned to the component. + /// + /// The assigned to the component. + public IComponentRenderMode? RenderMode + { + get + { + if (_renderer == null) + { + throw new InvalidOperationException("No renderer has been initialized."); + } + + return _renderer.GetComponentRenderMode(_componentId); + } + } + /// /// Notifies the renderer that the component should be rendered. /// diff --git a/src/Components/Components/src/RenderTree/ComponentPlatform.cs b/src/Components/Components/src/RenderTree/ComponentPlatform.cs new file mode 100644 index 000000000000..40a15d4f6081 --- /dev/null +++ b/src/Components/Components/src/RenderTree/ComponentPlatform.cs @@ -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; + +/// +/// Provides information about the platform that the component is running on. +/// +public sealed class ComponentPlatform +{ + /// + /// Constructs a new instance of . + /// + /// The name of the platform. + /// A flag to indicate if the platform is interactive. + public ComponentPlatform(string platformName, bool isInteractive) + { + Name = platformName; + IsInteractive = isInteractive; + } + + /// + /// Gets the name of the platform. + /// + public string Name { get; } + + /// + /// Gets a flag to indicate if the platform is interactive. + /// + public bool IsInteractive { get; } +} diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 29c6d7ef00c8..1e4ef05d30b4 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -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); + /// /// Resolves the component state for a given instance. /// @@ -150,6 +153,11 @@ protected ComponentState GetComponentState(int componentId) protected internal ComponentState GetComponentState(IComponent component) => _componentStateByComponent.GetValueOrDefault(component); + /// + /// Gets the associated with this . + /// + 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 diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs index 6a57a2eccfa5..370c80d1edbd 100644 --- a/src/Components/Server/src/Circuits/RemoteRenderer.cs +++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs @@ -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; @@ -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); diff --git a/src/Components/Web/src/HtmlRendering/StaticHtmlRenderer.cs b/src/Components/Web/src/HtmlRendering/StaticHtmlRenderer.cs index 28cc2d3e9e69..1a1c3425c840 100644 --- a/src/Components/Web/src/HtmlRendering/StaticHtmlRenderer.cs +++ b/src/Components/Web/src/HtmlRendering/StaticHtmlRenderer.cs @@ -19,6 +19,8 @@ namespace Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure; /// 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; @@ -38,6 +40,9 @@ public StaticHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory logge /// public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault(); + /// + protected internal override ComponentPlatform ComponentPlatform => _componentPlatform; + /// /// Adds a root component of the specified type and begins rendering it. /// diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index 1a79e385d309..d3c627905559 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -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! diff --git a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs index a2297cb2f8b7..96174ead5c30 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs @@ -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) @@ -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) diff --git a/src/Components/WebView/WebView/src/Services/WebViewRenderer.cs b/src/Components/WebView/WebView/src/Services/WebViewRenderer.cs index b323ed1f39b6..538750239477 100644 --- a/src/Components/WebView/WebView/src/Services/WebViewRenderer.cs +++ b/src/Components/WebView/WebView/src/Services/WebViewRenderer.cs @@ -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 _unacknowledgedRenderBatches = new(); private readonly Dispatcher _dispatcher; private readonly IpcSender _ipcSender; @@ -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) @@ -81,7 +84,7 @@ public void NotifyRenderCompleted(long batchId) private sealed class UnacknowledgedRenderBatch { public long BatchId { get; init; } - + public TaskCompletionSource CompletionSource { get; init; } } } diff --git a/src/Components/benchmarkapps/Wasm.Performance/ConsoleHost/Scenarios/GridScenario.cs b/src/Components/benchmarkapps/Wasm.Performance/ConsoleHost/Scenarios/GridScenario.cs index b1c7567d2270..c05dc0dd27ef 100644 --- a/src/Components/benchmarkapps/Wasm.Performance/ConsoleHost/Scenarios/GridScenario.cs +++ b/src/Components/benchmarkapps/Wasm.Performance/ConsoleHost/Scenarios/GridScenario.cs @@ -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++) { diff --git a/src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/GridRendering.razor b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/GridRendering.razor index c7dc8137081e..e033948e8393 100644 --- a/src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/GridRendering.razor +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/GridRendering.razor @@ -6,9 +6,9 @@
@@ -23,7 +23,7 @@ {

(No data assigned)

} -else if (SelectedRenderMode == RenderMode.FastGrid) +else if (SelectedRenderMode == GridRenderMode.FastGrid) {

FastGrid represents a minimal, optimized implementation of a grid.

@@ -50,13 +50,13 @@ else if (SelectedRenderMode == RenderMode.FastGrid) @context.Summary } -else if (SelectedRenderMode == RenderMode.PlainTable) +else if (SelectedRenderMode == GridRenderMode.PlainTable) {

PlainTable represents a minimal but not optimized implementation of a grid.

} -else if (SelectedRenderMode == RenderMode.ComplexTable) +else if (SelectedRenderMode == GridRenderMode.ComplexTable) {

ComplexTable represents a maximal, not optimized implementation of a grid, using a wide range of Blazor features at once.

@@ -64,9 +64,9 @@ else if (SelectedRenderMode == RenderMode.ComplexTable) } @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 Columns { get; set; } = new List diff --git a/src/Components/test/E2ETest/Tests/InteractiveHostRendermodeTest.cs b/src/Components/test/E2ETest/Tests/InteractiveHostRendermodeTest.cs new file mode 100644 index 000000000000..08fd739ab81f --- /dev/null +++ b/src/Components/test/E2ETest/Tests/InteractiveHostRendermodeTest.cs @@ -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>> +{ + public InteractiveHostRendermodeTest( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> 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"); + } + } +} + diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor index d0382fbdc547..e437aa9811d2 100644 --- a/src/Components/test/testassets/BasicTestApp/Index.razor +++ b/src/Components/test/testassets/BasicTestApp/Index.razor @@ -55,6 +55,7 @@ + diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/ComponentPlatform.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/ComponentPlatform.razor new file mode 100644 index 000000000000..406c29fe7b42 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/ComponentPlatform.razor @@ -0,0 +1,38 @@ +@page "/componentplatform" +@using TestContentPackage + +

Component platform tests

+ +

+ 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. +

+ + + +@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}"); + } + } +} diff --git a/src/Components/test/testassets/TestContentPackage/ComponentPlatformDetails.razor b/src/Components/test/testassets/TestContentPackage/ComponentPlatformDetails.razor new file mode 100644 index 000000000000..76763edadb6b --- /dev/null +++ b/src/Components/test/testassets/TestContentPackage/ComponentPlatformDetails.razor @@ -0,0 +1,34 @@ + +

@_renderMode

+

@_isInteractive

+ +@code { + + private string _renderMode = ""; + private bool _isInteractive = false; + + protected override void OnInitialized() + { + if (AssignedRenderMode is InteractiveServerRenderMode) + { + _renderMode = $"server"; + } + else if (AssignedRenderMode is InteractiveWebAssemblyRenderMode) + { + _renderMode = $"webassembly"; + } + else if (AssignedRenderMode is InteractiveAutoRenderMode) + { + _renderMode = $"auto"; + } + else + { + _renderMode = "static"; + } + + if (Platform.IsInteractive) + { + _isInteractive = true; + } + } +}