From 6b2d9f23f86b221d7c54181ecf65f70a6a6cda2b Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 13 Aug 2019 09:34:28 +0100 Subject: [PATCH] Prerender select elements with value; move HtmlRenderer into Mvc.ViewFeatures (#12996) --- ...oft.AspNetCore.Components.netcoreapp3.0.cs | 19 +-- ...ft.AspNetCore.Components.netstandard2.0.cs | 19 +-- .../src/Rendering/ComponentRenderedText.cs | 29 ---- .../Components/src/Rendering/Renderer.cs | 2 +- .../test/Rendering/HtmlRendererTests.cs | 16 --- .../src/Circuits/DefaultCircuitFactory.cs | 2 - .../Server/src/Circuits/RemoteRenderer.cs | 6 +- .../Server/test/Circuits/CircuitHostTest.cs | 4 +- .../test/Circuits/RemoteRendererTest.cs | 67 ++++----- .../Server/test/Circuits/TestCircuitHost.cs | 2 - ....AspNetCore.Components.Server.Tests.csproj | 3 +- .../RazorComponents/ComponentRenderedText.cs | 20 +++ .../src/RazorComponents}/HtmlRenderer.cs | 113 ++++++++------- .../test/RazorComponents/HtmlRendererTest.cs} | 132 ++++++++++++++++-- 14 files changed, 240 insertions(+), 194 deletions(-) delete mode 100644 src/Components/Components/src/Rendering/ComponentRenderedText.cs delete mode 100644 src/Components/Components/test/Rendering/HtmlRendererTests.cs create mode 100644 src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderedText.cs rename src/{Components/Components/src/Rendering => Mvc/Mvc.ViewFeatures/src/RazorComponents}/HtmlRenderer.cs (69%) rename src/{Components/Components/test/Rendering/HtmlRendererTestBase.cs => Mvc/Mvc.ViewFeatures/test/RazorComponents/HtmlRendererTest.cs} (80%) diff --git a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netcoreapp3.0.cs b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netcoreapp3.0.cs index 468bea98e9..9448a45cb4 100644 --- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netcoreapp3.0.cs +++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netcoreapp3.0.cs @@ -363,30 +363,12 @@ namespace Microsoft.AspNetCore.Components.CompilerServices } namespace Microsoft.AspNetCore.Components.Rendering { - [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] - public readonly partial struct ComponentRenderedText - { - private readonly object _dummy; - private readonly int _dummyPrimitive; - public int ComponentId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - public System.Collections.Generic.IEnumerable Tokens { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - } public partial class EventFieldInfo { public EventFieldInfo() { } public int ComponentId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public object FieldValue { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } } - public partial class HtmlRenderer : Microsoft.AspNetCore.Components.Rendering.Renderer - { - public HtmlRenderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, System.Func htmlEncoder) : base (default(System.IServiceProvider), default(Microsoft.Extensions.Logging.ILoggerFactory)) { } - public override Microsoft.AspNetCore.Components.Dispatcher Dispatcher { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - protected override void HandleException(System.Exception exception) { } - [System.Diagnostics.DebuggerStepThroughAttribute] - public System.Threading.Tasks.Task RenderComponentAsync(System.Type componentType, Microsoft.AspNetCore.Components.ParameterView initialParameters) { throw null; } - public System.Threading.Tasks.Task RenderComponentAsync(Microsoft.AspNetCore.Components.ParameterView initialParameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; } - protected override System.Threading.Tasks.Task UpdateDisplayAsync(in Microsoft.AspNetCore.Components.Rendering.RenderBatch renderBatch) { throw null; } - } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] public readonly partial struct RenderBatch { @@ -405,6 +387,7 @@ namespace Microsoft.AspNetCore.Components.Rendering public virtual System.Threading.Tasks.Task DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.Rendering.EventFieldInfo fieldInfo, System.EventArgs eventArgs) { throw null; } public void Dispose() { } protected virtual void Dispose(bool disposing) { } + protected Microsoft.AspNetCore.Components.RenderTree.ArrayRange GetCurrentRenderTreeFrames(int componentId) { throw null; } protected abstract void HandleException(System.Exception exception); protected Microsoft.AspNetCore.Components.IComponent InstantiateComponent(System.Type componentType) { throw null; } protected virtual void ProcessPendingRender() { } diff --git a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs index 468bea98e9..9448a45cb4 100644 --- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs +++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs @@ -363,30 +363,12 @@ namespace Microsoft.AspNetCore.Components.CompilerServices } namespace Microsoft.AspNetCore.Components.Rendering { - [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] - public readonly partial struct ComponentRenderedText - { - private readonly object _dummy; - private readonly int _dummyPrimitive; - public int ComponentId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - public System.Collections.Generic.IEnumerable Tokens { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - } public partial class EventFieldInfo { public EventFieldInfo() { } public int ComponentId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public object FieldValue { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } } - public partial class HtmlRenderer : Microsoft.AspNetCore.Components.Rendering.Renderer - { - public HtmlRenderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, System.Func htmlEncoder) : base (default(System.IServiceProvider), default(Microsoft.Extensions.Logging.ILoggerFactory)) { } - public override Microsoft.AspNetCore.Components.Dispatcher Dispatcher { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - protected override void HandleException(System.Exception exception) { } - [System.Diagnostics.DebuggerStepThroughAttribute] - public System.Threading.Tasks.Task RenderComponentAsync(System.Type componentType, Microsoft.AspNetCore.Components.ParameterView initialParameters) { throw null; } - public System.Threading.Tasks.Task RenderComponentAsync(Microsoft.AspNetCore.Components.ParameterView initialParameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; } - protected override System.Threading.Tasks.Task UpdateDisplayAsync(in Microsoft.AspNetCore.Components.Rendering.RenderBatch renderBatch) { throw null; } - } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] public readonly partial struct RenderBatch { @@ -405,6 +387,7 @@ namespace Microsoft.AspNetCore.Components.Rendering public virtual System.Threading.Tasks.Task DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.Rendering.EventFieldInfo fieldInfo, System.EventArgs eventArgs) { throw null; } public void Dispose() { } protected virtual void Dispose(bool disposing) { } + protected Microsoft.AspNetCore.Components.RenderTree.ArrayRange GetCurrentRenderTreeFrames(int componentId) { throw null; } protected abstract void HandleException(System.Exception exception); protected Microsoft.AspNetCore.Components.IComponent InstantiateComponent(System.Type componentType) { throw null; } protected virtual void ProcessPendingRender() { } diff --git a/src/Components/Components/src/Rendering/ComponentRenderedText.cs b/src/Components/Components/src/Rendering/ComponentRenderedText.cs deleted file mode 100644 index 400fda7643..0000000000 --- a/src/Components/Components/src/Rendering/ComponentRenderedText.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; - -namespace Microsoft.AspNetCore.Components.Rendering -{ - /// - /// Represents the result of rendering a component into static html. - /// - public readonly struct ComponentRenderedText - { - internal ComponentRenderedText(int componentId, IEnumerable tokens) - { - ComponentId = componentId; - Tokens = tokens; - } - - /// - /// Gets the id associated with the component. - /// - public int ComponentId { get; } - - /// - /// Gets the sequence of tokens that when concatenated represent the html for the rendered component. - /// - public IEnumerable Tokens { get; } - } -} diff --git a/src/Components/Components/src/Rendering/Renderer.cs b/src/Components/Components/src/Rendering/Renderer.cs index 1ca8636192..d1fdfa35ff 100644 --- a/src/Components/Components/src/Rendering/Renderer.cs +++ b/src/Components/Components/src/Rendering/Renderer.cs @@ -93,7 +93,7 @@ namespace Microsoft.AspNetCore.Components.Rendering /// /// The id for the component. /// The representing the current render tree. - private protected ArrayRange GetCurrentRenderTreeFrames(int componentId) => GetRequiredComponentState(componentId).CurrentRenderTree.GetFrames(); + protected ArrayRange GetCurrentRenderTreeFrames(int componentId) => GetRequiredComponentState(componentId).CurrentRenderTree.GetFrames(); /// /// Performs the first render for a root component, waiting for this component and all diff --git a/src/Components/Components/test/Rendering/HtmlRendererTests.cs b/src/Components/Components/test/Rendering/HtmlRendererTests.cs deleted file mode 100644 index c02e835751..0000000000 --- a/src/Components/Components/test/Rendering/HtmlRendererTests.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Microsoft.AspNetCore.Components.Rendering -{ - public class HtmlRendererTests : HtmlRendererTestBase - { - protected override HtmlRenderer GetHtmlRenderer(IServiceProvider serviceProvider) - { - return new HtmlRenderer(serviceProvider, NullLoggerFactory.Instance, _encoder); - } - } -} diff --git a/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs b/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs index b5c14b0911..b51f122278 100644 --- a/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs +++ b/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs @@ -50,7 +50,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits var components = ResolveComponentMetadata(httpContext); var scope = _scopeFactory.CreateScope(); - var encoder = scope.ServiceProvider.GetRequiredService(); var jsRuntime = (RemoteJSRuntime)scope.ServiceProvider.GetRequiredService(); var componentContext = (RemoteComponentContext)scope.ServiceProvider.GetRequiredService(); jsRuntime.Initialize(client); @@ -76,7 +75,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits _options, jsRuntime, client, - encoder, _loggerFactory.CreateLogger()); var circuitHandlers = scope.ServiceProvider.GetServices() diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs index f2dae7dd0d..4c77968d42 100644 --- a/src/Components/Server/src/Circuits/RemoteRenderer.cs +++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Concurrent; using System.Linq; -using System.Text.Encodings.Web; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Rendering; @@ -17,7 +16,7 @@ using Microsoft.JSInterop; namespace Microsoft.AspNetCore.Components.Web.Rendering { - internal class RemoteRenderer : HtmlRenderer + internal class RemoteRenderer : Renderer { private static readonly Task CanceledTask = Task.FromCanceled(new CancellationToken(canceled: true)); @@ -43,9 +42,8 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering CircuitOptions options, IJSRuntime jsRuntime, CircuitClientProxy client, - HtmlEncoder encoder, ILogger logger) - : base(serviceProvider, loggerFactory, encoder.Encode) + : base(serviceProvider, loggerFactory) { _jsRuntime = jsRuntime; _client = client; diff --git a/src/Components/Server/test/Circuits/CircuitHostTest.cs b/src/Components/Server/test/Circuits/CircuitHostTest.cs index 9a1973e2f4..f85a408803 100644 --- a/src/Components/Server/test/Circuits/CircuitHostTest.cs +++ b/src/Components/Server/test/Circuits/CircuitHostTest.cs @@ -5,10 +5,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Text.Encodings.Web; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web.Rendering; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; @@ -238,7 +236,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits private class TestRemoteRenderer : RemoteRenderer { public TestRemoteRenderer(IServiceProvider serviceProvider, IJSRuntime jsRuntime, IClientProxy client) - : base(serviceProvider, NullLoggerFactory.Instance, new CircuitOptions(), jsRuntime, new CircuitClientProxy(client, "connection"), HtmlEncoder.Default, NullLogger.Instance) + : base(serviceProvider, NullLoggerFactory.Instance, new CircuitOptions(), jsRuntime, new CircuitClientProxy(client, "connection"), NullLogger.Instance) { } diff --git a/src/Components/Server/test/Circuits/RemoteRendererTest.cs b/src/Components/Server/test/Circuits/RemoteRendererTest.cs index d50e5fa5e6..386ea99f28 100644 --- a/src/Components/Server/test/Circuits/RemoteRendererTest.cs +++ b/src/Components/Server/test/Circuits/RemoteRendererTest.cs @@ -4,10 +4,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Text.Encodings.Web; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.AspNetCore.SignalR; @@ -20,23 +18,18 @@ using Xunit; namespace Microsoft.AspNetCore.Components.Web.Rendering { - public class RemoteRendererTest : HtmlRendererTestBase + public class RemoteRendererTest { // Nothing should exceed the timeout in a successful run of the the tests, this is just here to catch // failures. private static readonly TimeSpan Timeout = Debugger.IsAttached ? System.Threading.Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10); - protected override HtmlRenderer GetHtmlRenderer(IServiceProvider serviceProvider) - { - return GetRemoteRenderer(serviceProvider, new CircuitClientProxy()); - } - [Fact] public void WritesAreBufferedWhenTheClientIsOffline() { // Arrange var serviceProvider = new ServiceCollection().BuildServiceProvider(); - var renderer = (RemoteRenderer)GetHtmlRenderer(serviceProvider); + var renderer = GetRemoteRenderer(serviceProvider); var component = new TestComponent(builder => { builder.OpenElement(0, "my element"); @@ -57,7 +50,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering public void NotAcknowledgingRenders_ProducesBatches_UpToTheLimit() { var serviceProvider = new ServiceCollection().BuildServiceProvider(); - var renderer = (RemoteRenderer)GetHtmlRenderer(serviceProvider); + var renderer = GetRemoteRenderer(serviceProvider); var component = new TestComponent(builder => { builder.OpenElement(0, "my element"); @@ -81,7 +74,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering public async Task NoNewBatchesAreCreated_WhenThereAreNoPendingRenderRequestsFromComponents() { var serviceProvider = new ServiceCollection().BuildServiceProvider(); - var renderer = (RemoteRenderer)GetHtmlRenderer(serviceProvider); + var renderer = GetRemoteRenderer(serviceProvider); var component = new TestComponent(builder => { builder.OpenElement(0, "my element"); @@ -107,7 +100,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering public async Task ProducesNewBatch_WhenABatchGetsAcknowledged() { var serviceProvider = new ServiceCollection().BuildServiceProvider(); - var renderer = (RemoteRenderer)GetHtmlRenderer(serviceProvider); + var renderer = GetRemoteRenderer(serviceProvider); var i = 0; var component = new TestComponent(builder => { @@ -215,7 +208,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering .Returns((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task); // This produces the initial batch (id = 2) - var result = await renderer.RenderComponentAsync( + await renderer.RenderComponentAsync( ParameterView.FromDictionary(new Dictionary { [nameof(AutoParameterTestComponent.Content)] = initialContent, @@ -278,7 +271,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering .Returns((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task); // This produces the initial batch (id = 2) - var result = await renderer.RenderComponentAsync( + await renderer.RenderComponentAsync( ParameterView.FromDictionary(new Dictionary { [nameof(AutoParameterTestComponent.Content)] = initialContent, @@ -341,7 +334,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering var trigger = new Trigger(); // This produces the initial batch (id = 2) - var result = await renderer.RenderComponentAsync( + await renderer.RenderComponentAsync( ParameterView.FromDictionary(new Dictionary { [nameof(AutoParameterTestComponent.Content)] = initialContent, @@ -398,7 +391,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering var trigger = new Trigger(); // This produces the initial batch (id = 2) - var result = await renderer.RenderComponentAsync( + await renderer.RenderComponentAsync( ParameterView.FromDictionary(new Dictionary { [nameof(AutoParameterTestComponent.Content)] = initialContent, @@ -432,27 +425,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering exception.Message); } - [Fact] - public async Task PrerendersMultipleComponentsSuccessfully() - { - // Arrange - var serviceProvider = new ServiceCollection().BuildServiceProvider(); - - var renderer = GetRemoteRenderer( - serviceProvider, - new CircuitClientProxy()); - - // Act - var first = await renderer.RenderComponentAsync(ParameterView.Empty); - var second = await renderer.RenderComponentAsync(ParameterView.Empty); - - // Assert - Assert.Equal(0, first.ComponentId); - Assert.Equal(1, second.ComponentId); - Assert.Equal(2, renderer._unacknowledgedRenderBatches.Count); - } - - private RemoteRenderer GetRemoteRenderer(IServiceProvider serviceProvider, CircuitClientProxy circuitClientProxy) + private TestRemoteRenderer GetRemoteRenderer(IServiceProvider serviceProvider, CircuitClientProxy circuitClient = null) { var jsRuntime = new Mock(); jsRuntime.Setup(r => r.InvokeAsync( @@ -462,16 +435,30 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering It.IsAny())) .ReturnsAsync(Task.FromResult(null)); - return new RemoteRenderer( + return new TestRemoteRenderer( serviceProvider, NullLoggerFactory.Instance, new CircuitOptions(), jsRuntime.Object, - circuitClientProxy, - HtmlEncoder.Default, + circuitClient ?? new CircuitClientProxy(), NullLogger.Instance); } + private class TestRemoteRenderer : RemoteRenderer + { + public TestRemoteRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, CircuitOptions options, IJSRuntime jsRuntime, CircuitClientProxy client, ILogger logger) + : base(serviceProvider, loggerFactory, options, jsRuntime, client, logger) + { + } + + public async Task RenderComponentAsync(ParameterView initialParameters) + { + var component = InstantiateComponent(typeof(TComponent)); + var componentId = AssignRootComponentId(component); + await RenderRootComponentAsync(componentId, initialParameters); + } + } + private class TestComponent : IComponent, IHandleAfterRender { private RenderHandle _renderHandle; diff --git a/src/Components/Server/test/Circuits/TestCircuitHost.cs b/src/Components/Server/test/Circuits/TestCircuitHost.cs index 8e04f6ca2d..e0a2e0bd54 100644 --- a/src/Components/Server/test/Circuits/TestCircuitHost.cs +++ b/src/Components/Server/test/Circuits/TestCircuitHost.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Text.Encodings.Web; using Microsoft.AspNetCore.Components.Web.Rendering; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; @@ -40,7 +39,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits new CircuitOptions(), jsRuntime, clientProxy, - HtmlEncoder.Default, NullLogger.Instance); } diff --git a/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj b/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj index bf4040312e..c80f6fc2fb 100644 --- a/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj +++ b/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -14,7 +14,6 @@ - diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderedText.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderedText.cs new file mode 100644 index 0000000000..92274e38c3 --- /dev/null +++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderedText.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Components.Rendering +{ + internal readonly struct ComponentRenderedText + { + public ComponentRenderedText(int componentId, IEnumerable tokens) + { + ComponentId = componentId; + Tokens = tokens; + } + + public int ComponentId { get; } + + public IEnumerable Tokens { get; } + } +} diff --git a/src/Components/Components/src/Rendering/HtmlRenderer.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/HtmlRenderer.cs similarity index 69% rename from src/Components/Components/src/Rendering/HtmlRenderer.cs rename to src/Mvc/Mvc.ViewFeatures/src/RazorComponents/HtmlRenderer.cs index 5eb2e71095..5075e8a893 100644 --- a/src/Components/Components/src/Rendering/HtmlRenderer.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/HtmlRenderer.cs @@ -12,10 +12,7 @@ using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Components.Rendering { - /// - /// A that produces HTML. - /// - public class HtmlRenderer : Renderer + internal class HtmlRenderer : Renderer { private static readonly HashSet SelfClosingElements = new HashSet(StringComparer.OrdinalIgnoreCase) { @@ -26,12 +23,6 @@ namespace Microsoft.AspNetCore.Components.Rendering private readonly Func _htmlEncoder; - /// - /// Initializes a new instance of . - /// - /// The to use to instantiate components. - /// The . - /// A that will HTML encode the given string. public HtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, Func htmlEncoder) : base(serviceProvider, loggerFactory) { @@ -58,30 +49,16 @@ namespace Microsoft.AspNetCore.Components.Rendering return CanceledRenderTask; } - /// - /// Renders a component into a sequence of fragments that represent the textual representation - /// of the HTML produced by the component. - /// - /// The type of the . - /// A with the initial parameters to render the component. - /// A that on completion returns a sequence of fragments that represent the HTML text of the component. public async Task RenderComponentAsync(Type componentType, ParameterView initialParameters) { var (componentId, frames) = await CreateInitialRenderAsync(componentType, initialParameters); - var result = new List(); - var newPosition = RenderFrames(result, frames, 0, frames.Count); + var context = new HtmlRenderingContext(); + var newPosition = RenderFrames(context, frames, 0, frames.Count); Debug.Assert(newPosition == frames.Count); - return new ComponentRenderedText(componentId, result); + return new ComponentRenderedText(componentId, context.Result); } - /// - /// Renders a component into a sequence of fragments that represent the textual representation - /// of the HTML produced by the component. - /// - /// The type of the . - /// A with the initial parameters to render the component. - /// A that on completion returns a sequence of fragments that represent the HTML text of the component. public Task RenderComponentAsync(ParameterView initialParameters) where TComponent : IComponent { return RenderComponentAsync(typeof(TComponent), initialParameters); @@ -91,13 +68,13 @@ namespace Microsoft.AspNetCore.Components.Rendering protected override void HandleException(Exception exception) => ExceptionDispatchInfo.Capture(exception).Throw(); - private int RenderFrames(List result, ArrayRange frames, int position, int maxElements) + private int RenderFrames(HtmlRenderingContext context, ArrayRange frames, int position, int maxElements) { var nextPosition = position; var endPosition = position + maxElements; while (position < endPosition) { - nextPosition = RenderCore(result, frames, position, maxElements); + nextPosition = RenderCore(context, frames, position); if (position == nextPosition) { throw new InvalidOperationException("We didn't consume any input."); @@ -109,28 +86,27 @@ namespace Microsoft.AspNetCore.Components.Rendering } private int RenderCore( - List result, + HtmlRenderingContext context, ArrayRange frames, - int position, - int length) + int position) { ref var frame = ref frames.Array[position]; switch (frame.FrameType) { case RenderTreeFrameType.Element: - return RenderElement(result, frames, position); + return RenderElement(context, frames, position); case RenderTreeFrameType.Attribute: - return RenderAttributes(result, frames, position, 1); + throw new InvalidOperationException($"Attributes should only be encountered within {nameof(RenderElement)}"); case RenderTreeFrameType.Text: - result.Add(_htmlEncoder(frame.TextContent)); + context.Result.Add(_htmlEncoder(frame.TextContent)); return ++position; case RenderTreeFrameType.Markup: - result.Add(frame.MarkupContent); + context.Result.Add(frame.MarkupContent); return ++position; case RenderTreeFrameType.Component: - return RenderChildComponent(result, frames, position); + return RenderChildComponent(context, frames, position); case RenderTreeFrameType.Region: - return RenderFrames(result, frames, position + 1, frame.RegionSubtreeLength - 1); + return RenderFrames(context, frames, position + 1, frame.RegionSubtreeLength - 1); case RenderTreeFrameType.ElementReferenceCapture: case RenderTreeFrameType.ComponentReferenceCapture: return ++position; @@ -140,30 +116,57 @@ namespace Microsoft.AspNetCore.Components.Rendering } private int RenderChildComponent( - List result, + HtmlRenderingContext context, ArrayRange frames, int position) { ref var frame = ref frames.Array[position]; var childFrames = GetCurrentRenderTreeFrames(frame.ComponentId); - RenderFrames(result, childFrames, 0, childFrames.Count); + RenderFrames(context, childFrames, 0, childFrames.Count); return position + frame.ComponentSubtreeLength; } private int RenderElement( - List result, + HtmlRenderingContext context, ArrayRange frames, int position) { ref var frame = ref frames.Array[position]; + var result = context.Result; result.Add("<"); result.Add(frame.ElementName); - var afterAttributes = RenderAttributes(result, frames, position + 1, frame.ElementSubtreeLength - 1); + var afterAttributes = RenderAttributes(context, frames, position + 1, frame.ElementSubtreeLength - 1, out var capturedValueAttribute); + + // When we see an " + + @"" + + @"" + + "" + + @"" + + "

"; + var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => + { + rtb.OpenElement(0, "p"); + rtb.OpenElement(1, "select"); + rtb.AddAttribute(2, "unrelated-attribute-before", "a"); + rtb.AddAttribute(3, "value", "b"); + rtb.AddAttribute(4, "unrelated-attribute-after", "c"); + + foreach (var optionValue in new[] { "a", "b", "c"}) + { + rtb.OpenElement(5, "option"); + rtb.AddAttribute(6, "unrelated-attribute", "a"); + rtb.AddAttribute(7, "value", optionValue); + rtb.AddContent(8, $"Pick value {optionValue}"); + rtb.CloseElement(); // option + } + + rtb.CloseElement(); // select + + rtb.OpenElement(9, "option"); // To show other value-matching options don't get marked as selected + rtb.AddAttribute(10, "value", "b"); + rtb.AddContent(11, "unrelated option"); + rtb.CloseElement(); // option + + rtb.CloseElement(); // p + })).BuildServiceProvider(); + + var htmlRenderer = GetHtmlRenderer(serviceProvider); + + // Act + var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); + + // Assert + Assert.Equal(expectedHtml, string.Concat(result)); + } + + [Fact] + public void RenderComponentAsync_MarksSelectedOptionsAsSelected_WithOptGroups() + { + // Arrange + var expectedHtml = + @""; + var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => + { + rtb.OpenElement(0, "select"); + rtb.AddAttribute(1, "value", "beta"); + + foreach (var optionValue in new[] { "alpha", "beta", "gamma" }) + { + rtb.OpenElement(2, "optgroup"); + rtb.OpenElement(3, "option"); + rtb.AddAttribute(4, "value", optionValue); + rtb.AddContent(5, optionValue); + rtb.CloseElement(); // option + rtb.CloseElement(); // optgroup + } + + rtb.CloseElement(); // select + })).BuildServiceProvider(); + + var htmlRenderer = GetHtmlRenderer(serviceProvider); + + // Act + var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); + + // Assert + Assert.Equal(expectedHtml, string.Concat(result)); + } [Fact] public void RenderComponentAsync_CanRenderComponentAsyncWithChildrenComponents() @@ -358,11 +441,11 @@ namespace Microsoft.AspNetCore.Components.Rendering // Act var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync( - new ParameterView(new[] { - RenderTreeFrame.Element(0,string.Empty), - RenderTreeFrame.Attribute(1,"update",change), - RenderTreeFrame.Attribute(2,"value",5) - }, 0)))); + ParameterView.FromDictionary(new Dictionary + { + { "update", change }, + { "value", 5 } + })))); // Assert Assert.Equal(expectedHtml, result); @@ -499,6 +582,31 @@ namespace Microsoft.AspNetCore.Components.Rendering Assert.Equal(expectedHtml, result.Tokens); } + [Fact] + public async Task PrerendersMultipleComponentsSuccessfully() + { + // Arrange + var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => + { + rtb.OpenElement(0, "p"); + rtb.AddMarkupContent(1, "Hello world!"); + rtb.CloseElement(); + })).BuildServiceProvider(); + var renderer = GetHtmlRenderer(serviceProvider); + + // Act + var first = await renderer.Dispatcher.InvokeAsync(() => renderer.RenderComponentAsync(ParameterView.Empty)); + var second = await renderer.Dispatcher.InvokeAsync(() => renderer.RenderComponentAsync(ParameterView.Empty)); + + // Assert + Assert.Equal(0, first.ComponentId); + Assert.Equal(1, second.ComponentId); + } + + private HtmlRenderer GetHtmlRenderer(IServiceProvider serviceProvider) + { + return new HtmlRenderer(serviceProvider, NullLoggerFactory.Instance, _encoder); + } private class NestedAsyncComponent : ComponentBase {