Prerender select elements with value; move HtmlRenderer into Mvc.ViewFeatures (#12996)

This commit is contained in:
Steve Sanderson 2019-08-13 09:34:28 +01:00 committed by GitHub
parent 826ed7504b
commit 6b2d9f23f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 240 additions and 194 deletions

View File

@ -363,30 +363,12 @@ namespace Microsoft.AspNetCore.Components.CompilerServices
} }
namespace Microsoft.AspNetCore.Components.Rendering 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<string> Tokens { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
}
public partial class EventFieldInfo public partial class EventFieldInfo
{ {
public EventFieldInfo() { } public EventFieldInfo() { }
public int ComponentId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } 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 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<string, string> 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<Microsoft.AspNetCore.Components.Rendering.ComponentRenderedText> RenderComponentAsync(System.Type componentType, Microsoft.AspNetCore.Components.ParameterView initialParameters) { throw null; }
public System.Threading.Tasks.Task<Microsoft.AspNetCore.Components.Rendering.ComponentRenderedText> RenderComponentAsync<TComponent>(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)] [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
public readonly partial struct RenderBatch 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 virtual System.Threading.Tasks.Task DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.Rendering.EventFieldInfo fieldInfo, System.EventArgs eventArgs) { throw null; }
public void Dispose() { } public void Dispose() { }
protected virtual void Dispose(bool disposing) { } protected virtual void Dispose(bool disposing) { }
protected Microsoft.AspNetCore.Components.RenderTree.ArrayRange<Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame> GetCurrentRenderTreeFrames(int componentId) { throw null; }
protected abstract void HandleException(System.Exception exception); protected abstract void HandleException(System.Exception exception);
protected Microsoft.AspNetCore.Components.IComponent InstantiateComponent(System.Type componentType) { throw null; } protected Microsoft.AspNetCore.Components.IComponent InstantiateComponent(System.Type componentType) { throw null; }
protected virtual void ProcessPendingRender() { } protected virtual void ProcessPendingRender() { }

View File

@ -363,30 +363,12 @@ namespace Microsoft.AspNetCore.Components.CompilerServices
} }
namespace Microsoft.AspNetCore.Components.Rendering 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<string> Tokens { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
}
public partial class EventFieldInfo public partial class EventFieldInfo
{ {
public EventFieldInfo() { } public EventFieldInfo() { }
public int ComponentId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } 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 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<string, string> 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<Microsoft.AspNetCore.Components.Rendering.ComponentRenderedText> RenderComponentAsync(System.Type componentType, Microsoft.AspNetCore.Components.ParameterView initialParameters) { throw null; }
public System.Threading.Tasks.Task<Microsoft.AspNetCore.Components.Rendering.ComponentRenderedText> RenderComponentAsync<TComponent>(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)] [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
public readonly partial struct RenderBatch 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 virtual System.Threading.Tasks.Task DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.Rendering.EventFieldInfo fieldInfo, System.EventArgs eventArgs) { throw null; }
public void Dispose() { } public void Dispose() { }
protected virtual void Dispose(bool disposing) { } protected virtual void Dispose(bool disposing) { }
protected Microsoft.AspNetCore.Components.RenderTree.ArrayRange<Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame> GetCurrentRenderTreeFrames(int componentId) { throw null; }
protected abstract void HandleException(System.Exception exception); protected abstract void HandleException(System.Exception exception);
protected Microsoft.AspNetCore.Components.IComponent InstantiateComponent(System.Type componentType) { throw null; } protected Microsoft.AspNetCore.Components.IComponent InstantiateComponent(System.Type componentType) { throw null; }
protected virtual void ProcessPendingRender() { } protected virtual void ProcessPendingRender() { }

View File

@ -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
{
/// <summary>
/// Represents the result of rendering a component into static html.
/// </summary>
public readonly struct ComponentRenderedText
{
internal ComponentRenderedText(int componentId, IEnumerable<string> tokens)
{
ComponentId = componentId;
Tokens = tokens;
}
/// <summary>
/// Gets the id associated with the component.
/// </summary>
public int ComponentId { get; }
/// <summary>
/// Gets the sequence of tokens that when concatenated represent the html for the rendered component.
/// </summary>
public IEnumerable<string> Tokens { get; }
}
}

View File

@ -93,7 +93,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// </summary> /// </summary>
/// <param name="componentId">The id for the component.</param> /// <param name="componentId">The id for the component.</param>
/// <returns>The <see cref="RenderTreeBuilder"/> representing the current render tree.</returns> /// <returns>The <see cref="RenderTreeBuilder"/> representing the current render tree.</returns>
private protected ArrayRange<RenderTreeFrame> GetCurrentRenderTreeFrames(int componentId) => GetRequiredComponentState(componentId).CurrentRenderTree.GetFrames(); protected ArrayRange<RenderTreeFrame> GetCurrentRenderTreeFrames(int componentId) => GetRequiredComponentState(componentId).CurrentRenderTree.GetFrames();
/// <summary> /// <summary>
/// Performs the first render for a root component, waiting for this component and all /// Performs the first render for a root component, waiting for this component and all

View File

@ -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);
}
}
}

View File

@ -50,7 +50,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
var components = ResolveComponentMetadata(httpContext); var components = ResolveComponentMetadata(httpContext);
var scope = _scopeFactory.CreateScope(); var scope = _scopeFactory.CreateScope();
var encoder = scope.ServiceProvider.GetRequiredService<HtmlEncoder>();
var jsRuntime = (RemoteJSRuntime)scope.ServiceProvider.GetRequiredService<IJSRuntime>(); var jsRuntime = (RemoteJSRuntime)scope.ServiceProvider.GetRequiredService<IJSRuntime>();
var componentContext = (RemoteComponentContext)scope.ServiceProvider.GetRequiredService<IComponentContext>(); var componentContext = (RemoteComponentContext)scope.ServiceProvider.GetRequiredService<IComponentContext>();
jsRuntime.Initialize(client); jsRuntime.Initialize(client);
@ -76,7 +75,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
_options, _options,
jsRuntime, jsRuntime,
client, client,
encoder,
_loggerFactory.CreateLogger<RemoteRenderer>()); _loggerFactory.CreateLogger<RemoteRenderer>());
var circuitHandlers = scope.ServiceProvider.GetServices<CircuitHandler>() var circuitHandlers = scope.ServiceProvider.GetServices<CircuitHandler>()

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Linq; using System.Linq;
using System.Text.Encodings.Web;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Rendering;
@ -17,7 +16,7 @@ using Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Components.Web.Rendering 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)); private static readonly Task CanceledTask = Task.FromCanceled(new CancellationToken(canceled: true));
@ -43,9 +42,8 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
CircuitOptions options, CircuitOptions options,
IJSRuntime jsRuntime, IJSRuntime jsRuntime,
CircuitClientProxy client, CircuitClientProxy client,
HtmlEncoder encoder,
ILogger logger) ILogger logger)
: base(serviceProvider, loggerFactory, encoder.Encode) : base(serviceProvider, loggerFactory)
{ {
_jsRuntime = jsRuntime; _jsRuntime = jsRuntime;
_client = client; _client = client;

View File

@ -5,10 +5,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text.Encodings.Web;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.Rendering; using Microsoft.AspNetCore.Components.Web.Rendering;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -238,7 +236,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
private class TestRemoteRenderer : RemoteRenderer private class TestRemoteRenderer : RemoteRenderer
{ {
public TestRemoteRenderer(IServiceProvider serviceProvider, IJSRuntime jsRuntime, IClientProxy client) 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)
{ {
} }

View File

@ -4,10 +4,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Text.Encodings.Web;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
@ -20,23 +18,18 @@ using Xunit;
namespace Microsoft.AspNetCore.Components.Web.Rendering 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 // Nothing should exceed the timeout in a successful run of the the tests, this is just here to catch
// failures. // failures.
private static readonly TimeSpan Timeout = Debugger.IsAttached ? System.Threading.Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10); 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] [Fact]
public void WritesAreBufferedWhenTheClientIsOffline() public void WritesAreBufferedWhenTheClientIsOffline()
{ {
// Arrange // Arrange
var serviceProvider = new ServiceCollection().BuildServiceProvider(); var serviceProvider = new ServiceCollection().BuildServiceProvider();
var renderer = (RemoteRenderer)GetHtmlRenderer(serviceProvider); var renderer = GetRemoteRenderer(serviceProvider);
var component = new TestComponent(builder => var component = new TestComponent(builder =>
{ {
builder.OpenElement(0, "my element"); builder.OpenElement(0, "my element");
@ -57,7 +50,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
public void NotAcknowledgingRenders_ProducesBatches_UpToTheLimit() public void NotAcknowledgingRenders_ProducesBatches_UpToTheLimit()
{ {
var serviceProvider = new ServiceCollection().BuildServiceProvider(); var serviceProvider = new ServiceCollection().BuildServiceProvider();
var renderer = (RemoteRenderer)GetHtmlRenderer(serviceProvider); var renderer = GetRemoteRenderer(serviceProvider);
var component = new TestComponent(builder => var component = new TestComponent(builder =>
{ {
builder.OpenElement(0, "my element"); builder.OpenElement(0, "my element");
@ -81,7 +74,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
public async Task NoNewBatchesAreCreated_WhenThereAreNoPendingRenderRequestsFromComponents() public async Task NoNewBatchesAreCreated_WhenThereAreNoPendingRenderRequestsFromComponents()
{ {
var serviceProvider = new ServiceCollection().BuildServiceProvider(); var serviceProvider = new ServiceCollection().BuildServiceProvider();
var renderer = (RemoteRenderer)GetHtmlRenderer(serviceProvider); var renderer = GetRemoteRenderer(serviceProvider);
var component = new TestComponent(builder => var component = new TestComponent(builder =>
{ {
builder.OpenElement(0, "my element"); builder.OpenElement(0, "my element");
@ -107,7 +100,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
public async Task ProducesNewBatch_WhenABatchGetsAcknowledged() public async Task ProducesNewBatch_WhenABatchGetsAcknowledged()
{ {
var serviceProvider = new ServiceCollection().BuildServiceProvider(); var serviceProvider = new ServiceCollection().BuildServiceProvider();
var renderer = (RemoteRenderer)GetHtmlRenderer(serviceProvider); var renderer = GetRemoteRenderer(serviceProvider);
var i = 0; var i = 0;
var component = new TestComponent(builder => var component = new TestComponent(builder =>
{ {
@ -215,7 +208,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
.Returns<string, object[], CancellationToken>((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task); .Returns<string, object[], CancellationToken>((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task);
// This produces the initial batch (id = 2) // This produces the initial batch (id = 2)
var result = await renderer.RenderComponentAsync<AutoParameterTestComponent>( await renderer.RenderComponentAsync<AutoParameterTestComponent>(
ParameterView.FromDictionary(new Dictionary<string, object> ParameterView.FromDictionary(new Dictionary<string, object>
{ {
[nameof(AutoParameterTestComponent.Content)] = initialContent, [nameof(AutoParameterTestComponent.Content)] = initialContent,
@ -278,7 +271,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
.Returns<string, object[], CancellationToken>((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task); .Returns<string, object[], CancellationToken>((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task);
// This produces the initial batch (id = 2) // This produces the initial batch (id = 2)
var result = await renderer.RenderComponentAsync<AutoParameterTestComponent>( await renderer.RenderComponentAsync<AutoParameterTestComponent>(
ParameterView.FromDictionary(new Dictionary<string, object> ParameterView.FromDictionary(new Dictionary<string, object>
{ {
[nameof(AutoParameterTestComponent.Content)] = initialContent, [nameof(AutoParameterTestComponent.Content)] = initialContent,
@ -341,7 +334,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
var trigger = new Trigger(); var trigger = new Trigger();
// This produces the initial batch (id = 2) // This produces the initial batch (id = 2)
var result = await renderer.RenderComponentAsync<AutoParameterTestComponent>( await renderer.RenderComponentAsync<AutoParameterTestComponent>(
ParameterView.FromDictionary(new Dictionary<string, object> ParameterView.FromDictionary(new Dictionary<string, object>
{ {
[nameof(AutoParameterTestComponent.Content)] = initialContent, [nameof(AutoParameterTestComponent.Content)] = initialContent,
@ -398,7 +391,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
var trigger = new Trigger(); var trigger = new Trigger();
// This produces the initial batch (id = 2) // This produces the initial batch (id = 2)
var result = await renderer.RenderComponentAsync<AutoParameterTestComponent>( await renderer.RenderComponentAsync<AutoParameterTestComponent>(
ParameterView.FromDictionary(new Dictionary<string, object> ParameterView.FromDictionary(new Dictionary<string, object>
{ {
[nameof(AutoParameterTestComponent.Content)] = initialContent, [nameof(AutoParameterTestComponent.Content)] = initialContent,
@ -432,27 +425,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
exception.Message); exception.Message);
} }
[Fact] private TestRemoteRenderer GetRemoteRenderer(IServiceProvider serviceProvider, CircuitClientProxy circuitClient = null)
public async Task PrerendersMultipleComponentsSuccessfully()
{
// Arrange
var serviceProvider = new ServiceCollection().BuildServiceProvider();
var renderer = GetRemoteRenderer(
serviceProvider,
new CircuitClientProxy());
// Act
var first = await renderer.RenderComponentAsync<TestComponent>(ParameterView.Empty);
var second = await renderer.RenderComponentAsync<TestComponent>(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)
{ {
var jsRuntime = new Mock<IJSRuntime>(); var jsRuntime = new Mock<IJSRuntime>();
jsRuntime.Setup(r => r.InvokeAsync<object>( jsRuntime.Setup(r => r.InvokeAsync<object>(
@ -462,16 +435,30 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
It.IsAny<int>())) It.IsAny<int>()))
.ReturnsAsync(Task.FromResult<object>(null)); .ReturnsAsync(Task.FromResult<object>(null));
return new RemoteRenderer( return new TestRemoteRenderer(
serviceProvider, serviceProvider,
NullLoggerFactory.Instance, NullLoggerFactory.Instance,
new CircuitOptions(), new CircuitOptions(),
jsRuntime.Object, jsRuntime.Object,
circuitClientProxy, circuitClient ?? new CircuitClientProxy(),
HtmlEncoder.Default,
NullLogger.Instance); 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<TComponent>(ParameterView initialParameters)
{
var component = InstantiateComponent(typeof(TComponent));
var componentId = AssignRootComponentId(component);
await RenderRootComponentAsync(componentId, initialParameters);
}
}
private class TestComponent : IComponent, IHandleAfterRender private class TestComponent : IComponent, IHandleAfterRender
{ {
private RenderHandle _renderHandle; private RenderHandle _renderHandle;

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Components.Web.Rendering; using Microsoft.AspNetCore.Components.Web.Rendering;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -40,7 +39,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
new CircuitOptions(), new CircuitOptions(),
jsRuntime, jsRuntime,
clientProxy, clientProxy,
HtmlEncoder.Default,
NullLogger.Instance); NullLogger.Instance);
} }

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework> <TargetFramework>netcoreapp3.0</TargetFramework>
@ -14,7 +14,6 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\..\Components\test\Rendering\HtmlRendererTestBase.cs" />
<Compile Include="$(SignalRTestBase)HubMessageHelpers.cs" LinkBase="BlazorPack" /> <Compile Include="$(SignalRTestBase)HubMessageHelpers.cs" LinkBase="BlazorPack" />
<Compile Include="$(SignalRTestBase)MessagePackHubProtocolTestBase.cs" LinkBase="BlazorPack" /> <Compile Include="$(SignalRTestBase)MessagePackHubProtocolTestBase.cs" LinkBase="BlazorPack" />
<Compile Include="$(SignalRTestBase)TestBinder.cs" LinkBase="BlazorPack" /> <Compile Include="$(SignalRTestBase)TestBinder.cs" LinkBase="BlazorPack" />

View File

@ -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<string> tokens)
{
ComponentId = componentId;
Tokens = tokens;
}
public int ComponentId { get; }
public IEnumerable<string> Tokens { get; }
}
}

View File

@ -12,10 +12,7 @@ using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Components.Rendering namespace Microsoft.AspNetCore.Components.Rendering
{ {
/// <summary> internal class HtmlRenderer : Renderer
/// A <see cref="Renderer"/> that produces HTML.
/// </summary>
public class HtmlRenderer : Renderer
{ {
private static readonly HashSet<string> SelfClosingElements = new HashSet<string>(StringComparer.OrdinalIgnoreCase) private static readonly HashSet<string> SelfClosingElements = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{ {
@ -26,12 +23,6 @@ namespace Microsoft.AspNetCore.Components.Rendering
private readonly Func<string, string> _htmlEncoder; private readonly Func<string, string> _htmlEncoder;
/// <summary>
/// Initializes a new instance of <see cref="HtmlRenderer"/>.
/// </summary>
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> to use to instantiate components.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="htmlEncoder">A <see cref="Func{T, TResult}"/> that will HTML encode the given string.</param>
public HtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, Func<string, string> htmlEncoder) public HtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, Func<string, string> htmlEncoder)
: base(serviceProvider, loggerFactory) : base(serviceProvider, loggerFactory)
{ {
@ -58,30 +49,16 @@ namespace Microsoft.AspNetCore.Components.Rendering
return CanceledRenderTask; return CanceledRenderTask;
} }
/// <summary>
/// Renders a component into a sequence of <see cref="string"/> fragments that represent the textual representation
/// of the HTML produced by the component.
/// </summary>
/// <param name="componentType">The type of the <see cref="IComponent"/>.</param>
/// <param name="initialParameters">A <see cref="ParameterView"/> with the initial parameters to render the component.</param>
/// <returns>A <see cref="Task"/> that on completion returns a sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
public async Task<ComponentRenderedText> RenderComponentAsync(Type componentType, ParameterView initialParameters) public async Task<ComponentRenderedText> RenderComponentAsync(Type componentType, ParameterView initialParameters)
{ {
var (componentId, frames) = await CreateInitialRenderAsync(componentType, initialParameters); var (componentId, frames) = await CreateInitialRenderAsync(componentType, initialParameters);
var result = new List<string>(); var context = new HtmlRenderingContext();
var newPosition = RenderFrames(result, frames, 0, frames.Count); var newPosition = RenderFrames(context, frames, 0, frames.Count);
Debug.Assert(newPosition == frames.Count); Debug.Assert(newPosition == frames.Count);
return new ComponentRenderedText(componentId, result); return new ComponentRenderedText(componentId, context.Result);
} }
/// <summary>
/// Renders a component into a sequence of <see cref="string"/> fragments that represent the textual representation
/// of the HTML produced by the component.
/// </summary>
/// <typeparam name="TComponent">The type of the <see cref="IComponent"/>.</typeparam>
/// <param name="initialParameters">A <see cref="ParameterView"/> with the initial parameters to render the component.</param>
/// <returns>A <see cref="Task"/> that on completion returns a sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
public Task<ComponentRenderedText> RenderComponentAsync<TComponent>(ParameterView initialParameters) where TComponent : IComponent public Task<ComponentRenderedText> RenderComponentAsync<TComponent>(ParameterView initialParameters) where TComponent : IComponent
{ {
return RenderComponentAsync(typeof(TComponent), initialParameters); return RenderComponentAsync(typeof(TComponent), initialParameters);
@ -91,13 +68,13 @@ namespace Microsoft.AspNetCore.Components.Rendering
protected override void HandleException(Exception exception) protected override void HandleException(Exception exception)
=> ExceptionDispatchInfo.Capture(exception).Throw(); => ExceptionDispatchInfo.Capture(exception).Throw();
private int RenderFrames(List<string> result, ArrayRange<RenderTreeFrame> frames, int position, int maxElements) private int RenderFrames(HtmlRenderingContext context, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
{ {
var nextPosition = position; var nextPosition = position;
var endPosition = position + maxElements; var endPosition = position + maxElements;
while (position < endPosition) while (position < endPosition)
{ {
nextPosition = RenderCore(result, frames, position, maxElements); nextPosition = RenderCore(context, frames, position);
if (position == nextPosition) if (position == nextPosition)
{ {
throw new InvalidOperationException("We didn't consume any input."); throw new InvalidOperationException("We didn't consume any input.");
@ -109,28 +86,27 @@ namespace Microsoft.AspNetCore.Components.Rendering
} }
private int RenderCore( private int RenderCore(
List<string> result, HtmlRenderingContext context,
ArrayRange<RenderTreeFrame> frames, ArrayRange<RenderTreeFrame> frames,
int position, int position)
int length)
{ {
ref var frame = ref frames.Array[position]; ref var frame = ref frames.Array[position];
switch (frame.FrameType) switch (frame.FrameType)
{ {
case RenderTreeFrameType.Element: case RenderTreeFrameType.Element:
return RenderElement(result, frames, position); return RenderElement(context, frames, position);
case RenderTreeFrameType.Attribute: case RenderTreeFrameType.Attribute:
return RenderAttributes(result, frames, position, 1); throw new InvalidOperationException($"Attributes should only be encountered within {nameof(RenderElement)}");
case RenderTreeFrameType.Text: case RenderTreeFrameType.Text:
result.Add(_htmlEncoder(frame.TextContent)); context.Result.Add(_htmlEncoder(frame.TextContent));
return ++position; return ++position;
case RenderTreeFrameType.Markup: case RenderTreeFrameType.Markup:
result.Add(frame.MarkupContent); context.Result.Add(frame.MarkupContent);
return ++position; return ++position;
case RenderTreeFrameType.Component: case RenderTreeFrameType.Component:
return RenderChildComponent(result, frames, position); return RenderChildComponent(context, frames, position);
case RenderTreeFrameType.Region: 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.ElementReferenceCapture:
case RenderTreeFrameType.ComponentReferenceCapture: case RenderTreeFrameType.ComponentReferenceCapture:
return ++position; return ++position;
@ -140,30 +116,57 @@ namespace Microsoft.AspNetCore.Components.Rendering
} }
private int RenderChildComponent( private int RenderChildComponent(
List<string> result, HtmlRenderingContext context,
ArrayRange<RenderTreeFrame> frames, ArrayRange<RenderTreeFrame> frames,
int position) int position)
{ {
ref var frame = ref frames.Array[position]; ref var frame = ref frames.Array[position];
var childFrames = GetCurrentRenderTreeFrames(frame.ComponentId); var childFrames = GetCurrentRenderTreeFrames(frame.ComponentId);
RenderFrames(result, childFrames, 0, childFrames.Count); RenderFrames(context, childFrames, 0, childFrames.Count);
return position + frame.ComponentSubtreeLength; return position + frame.ComponentSubtreeLength;
} }
private int RenderElement( private int RenderElement(
List<string> result, HtmlRenderingContext context,
ArrayRange<RenderTreeFrame> frames, ArrayRange<RenderTreeFrame> frames,
int position) int position)
{ {
ref var frame = ref frames.Array[position]; ref var frame = ref frames.Array[position];
var result = context.Result;
result.Add("<"); result.Add("<");
result.Add(frame.ElementName); 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 <option> as a descendant of a <select>, and the option's "value" attribute matches the
// "value" attribute on the <select>, then we auto-add the "selected" attribute to that option. This is
// a way of converting Blazor's select binding feature to regular static HTML.
if (context.ClosestSelectValueAsString != null
&& string.Equals(frame.ElementName, "option", StringComparison.OrdinalIgnoreCase)
&& string.Equals(capturedValueAttribute, context.ClosestSelectValueAsString, StringComparison.Ordinal))
{
result.Add(" selected");
}
var remainingElements = frame.ElementSubtreeLength + position - afterAttributes; var remainingElements = frame.ElementSubtreeLength + position - afterAttributes;
if (remainingElements > 0) if (remainingElements > 0)
{ {
result.Add(">"); result.Add(">");
var afterElement = RenderChildren(result, frames, afterAttributes, remainingElements);
var isSelect = string.Equals(frame.ElementName, "select", StringComparison.OrdinalIgnoreCase);
if (isSelect)
{
context.ClosestSelectValueAsString = capturedValueAttribute;
}
var afterElement = RenderChildren(context, frames, afterAttributes, remainingElements);
if (isSelect)
{
// There's no concept of nested <select> elements, so as soon as we're exiting one of them,
// we can safely say there is no longer any value for this
context.ClosestSelectValueAsString = null;
}
result.Add("</"); result.Add("</");
result.Add(frame.ElementName); result.Add(frame.ElementName);
result.Add(">"); result.Add(">");
@ -188,25 +191,29 @@ namespace Microsoft.AspNetCore.Components.Rendering
} }
} }
private int RenderChildren(List<string> result, ArrayRange<RenderTreeFrame> frames, int position, int maxElements) private int RenderChildren(HtmlRenderingContext context, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
{ {
if (maxElements == 0) if (maxElements == 0)
{ {
return position; return position;
} }
return RenderFrames(result, frames, position, maxElements); return RenderFrames(context, frames, position, maxElements);
} }
private int RenderAttributes( private int RenderAttributes(
List<string> result, HtmlRenderingContext context,
ArrayRange<RenderTreeFrame> frames, int position, int maxElements) ArrayRange<RenderTreeFrame> frames, int position, int maxElements, out string capturedValueAttribute)
{ {
capturedValueAttribute = null;
if (maxElements == 0) if (maxElements == 0)
{ {
return position; return position;
} }
var result = context.Result;
for (var i = 0; i < maxElements; i++) for (var i = 0; i < maxElements; i++)
{ {
var candidateIndex = position + i; var candidateIndex = position + i;
@ -216,6 +223,11 @@ namespace Microsoft.AspNetCore.Components.Rendering
return candidateIndex; return candidateIndex;
} }
if (frame.AttributeName.Equals("value", StringComparison.OrdinalIgnoreCase))
{
capturedValueAttribute = frame.AttributeValue as string;
}
switch (frame.AttributeValue) switch (frame.AttributeValue)
{ {
case bool flag when flag: case bool flag when flag:
@ -247,6 +259,13 @@ namespace Microsoft.AspNetCore.Components.Rendering
return (componentId, GetCurrentRenderTreeFrames(componentId)); return (componentId, GetCurrentRenderTreeFrames(componentId));
} }
private class HtmlRenderingContext
{
public List<string> Result { get; } = new List<string>();
public string ClosestSelectValueAsString { get; set; }
}
} }
} }

View File

@ -6,23 +6,22 @@ using System.Collections.Generic;
using System.Runtime.ExceptionServices; using System.Runtime.ExceptionServices;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Components.Rendering namespace Microsoft.AspNetCore.Mvc.RazorComponents
{ {
public abstract class HtmlRendererTestBase public class HtmlRendererTest
{ {
protected readonly Func<string, string> _encoder = (string t) => HtmlEncoder.Default.Encode(t); protected readonly Func<string, string> _encoder = (string t) => HtmlEncoder.Default.Encode(t);
protected abstract HtmlRenderer GetHtmlRenderer(IServiceProvider serviceProvider);
[Fact] [Fact]
public void RenderComponentAsync_CanRenderEmptyElement() public void RenderComponentAsync_CanRenderEmptyElement()
{ {
// Arrange // Arrange
var expectedHtml = new[] { "<", "p", ">", "</", "p", ">" }; var expectedHtml = new[] { "<", "p", ">", "</", "p", ">" };
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
{ {
@ -99,7 +98,6 @@ namespace Microsoft.AspNetCore.Components.Rendering
Assert.Equal(expectedHtml, result); Assert.Equal(expectedHtml, result);
} }
[Fact] [Fact]
public void RenderComponentAsync_CanRenderWithAttributes() public void RenderComponentAsync_CanRenderWithAttributes()
{ {
@ -272,6 +270,91 @@ namespace Microsoft.AspNetCore.Components.Rendering
// Assert // Assert
Assert.Equal(expectedHtml, result); Assert.Equal(expectedHtml, result);
} }
[Fact]
public void RenderComponentAsync_MarksSelectedOptionsAsSelected()
{
// Arrange
var expectedHtml = "<p>" +
@"<select unrelated-attribute-before=""a"" value=""b"" unrelated-attribute-after=""c"">" +
@"<option unrelated-attribute=""a"" value=""a"">Pick value a</option>" +
@"<option unrelated-attribute=""a"" value=""b"" selected>Pick value b</option>" +
@"<option unrelated-attribute=""a"" value=""c"">Pick value c</option>" +
"</select>" +
@"<option value=""b"">unrelated option</option>" +
"</p>";
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<TestComponent>(ParameterView.Empty)));
// Assert
Assert.Equal(expectedHtml, string.Concat(result));
}
[Fact]
public void RenderComponentAsync_MarksSelectedOptionsAsSelected_WithOptGroups()
{
// Arrange
var expectedHtml =
@"<select value=""beta"">" +
@"<optgroup><option value=""alpha"">alpha</option></optgroup>" +
@"<optgroup><option value=""beta"" selected>beta</option></optgroup>" +
@"<optgroup><option value=""gamma"">gamma</option></optgroup>" +
"</select>";
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<TestComponent>(ParameterView.Empty)));
// Assert
Assert.Equal(expectedHtml, string.Concat(result));
}
[Fact] [Fact]
public void RenderComponentAsync_CanRenderComponentAsyncWithChildrenComponents() public void RenderComponentAsync_CanRenderComponentAsyncWithChildrenComponents()
@ -358,11 +441,11 @@ namespace Microsoft.AspNetCore.Components.Rendering
// Act // Act
var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<ComponentWithParameters>( var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<ComponentWithParameters>(
new ParameterView(new[] { ParameterView.FromDictionary(new Dictionary<string, object>
RenderTreeFrame.Element(0,string.Empty), {
RenderTreeFrame.Attribute(1,"update",change), { "update", change },
RenderTreeFrame.Attribute(2,"value",5) { "value", 5 }
}, 0)))); }))));
// Assert // Assert
Assert.Equal(expectedHtml, result); Assert.Equal(expectedHtml, result);
@ -499,6 +582,31 @@ namespace Microsoft.AspNetCore.Components.Rendering
Assert.Equal(expectedHtml, result.Tokens); 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, "<span>Hello world!</span>");
rtb.CloseElement();
})).BuildServiceProvider();
var renderer = GetHtmlRenderer(serviceProvider);
// Act
var first = await renderer.Dispatcher.InvokeAsync(() => renderer.RenderComponentAsync<TestComponent>(ParameterView.Empty));
var second = await renderer.Dispatcher.InvokeAsync(() => renderer.RenderComponentAsync<TestComponent>(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 private class NestedAsyncComponent : ComponentBase
{ {