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
{
[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 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<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)]
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<Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame> 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() { }

View File

@ -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<string> 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<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)]
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<Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame> 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() { }

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>
/// <param name="componentId">The id for the component.</param>
/// <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>
/// 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 scope = _scopeFactory.CreateScope();
var encoder = scope.ServiceProvider.GetRequiredService<HtmlEncoder>();
var jsRuntime = (RemoteJSRuntime)scope.ServiceProvider.GetRequiredService<IJSRuntime>();
var componentContext = (RemoteComponentContext)scope.ServiceProvider.GetRequiredService<IComponentContext>();
jsRuntime.Initialize(client);
@ -76,7 +75,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
_options,
jsRuntime,
client,
encoder,
_loggerFactory.CreateLogger<RemoteRenderer>());
var circuitHandlers = scope.ServiceProvider.GetServices<CircuitHandler>()

View File

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

View File

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

View File

@ -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<string, object[], CancellationToken>((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task);
// This produces the initial batch (id = 2)
var result = await renderer.RenderComponentAsync<AutoParameterTestComponent>(
await renderer.RenderComponentAsync<AutoParameterTestComponent>(
ParameterView.FromDictionary(new Dictionary<string, object>
{
[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);
// This produces the initial batch (id = 2)
var result = await renderer.RenderComponentAsync<AutoParameterTestComponent>(
await renderer.RenderComponentAsync<AutoParameterTestComponent>(
ParameterView.FromDictionary(new Dictionary<string, object>
{
[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<AutoParameterTestComponent>(
await renderer.RenderComponentAsync<AutoParameterTestComponent>(
ParameterView.FromDictionary(new Dictionary<string, object>
{
[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<AutoParameterTestComponent>(
await renderer.RenderComponentAsync<AutoParameterTestComponent>(
ParameterView.FromDictionary(new Dictionary<string, object>
{
[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<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)
private TestRemoteRenderer GetRemoteRenderer(IServiceProvider serviceProvider, CircuitClientProxy circuitClient = null)
{
var jsRuntime = new Mock<IJSRuntime>();
jsRuntime.Setup(r => r.InvokeAsync<object>(
@ -462,16 +435,30 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
It.IsAny<int>()))
.ReturnsAsync(Task.FromResult<object>(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<TComponent>(ParameterView initialParameters)
{
var component = InstantiateComponent(typeof(TComponent));
var componentId = AssignRootComponentId(component);
await RenderRootComponentAsync(componentId, initialParameters);
}
}
private class TestComponent : IComponent, IHandleAfterRender
{
private RenderHandle _renderHandle;

View File

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

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
@ -14,7 +14,6 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\Components\test\Rendering\HtmlRendererTestBase.cs" />
<Compile Include="$(SignalRTestBase)HubMessageHelpers.cs" LinkBase="BlazorPack" />
<Compile Include="$(SignalRTestBase)MessagePackHubProtocolTestBase.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
{
/// <summary>
/// A <see cref="Renderer"/> that produces HTML.
/// </summary>
public class HtmlRenderer : Renderer
internal class HtmlRenderer : Renderer
{
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;
/// <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)
: base(serviceProvider, loggerFactory)
{
@ -58,30 +49,16 @@ namespace Microsoft.AspNetCore.Components.Rendering
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)
{
var (componentId, frames) = await CreateInitialRenderAsync(componentType, initialParameters);
var result = new List<string>();
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);
}
/// <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
{
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<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 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<string> result,
HtmlRenderingContext context,
ArrayRange<RenderTreeFrame> 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<string> result,
HtmlRenderingContext context,
ArrayRange<RenderTreeFrame> 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<string> result,
HtmlRenderingContext context,
ArrayRange<RenderTreeFrame> 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 <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;
if (remainingElements > 0)
{
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(frame.ElementName);
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)
{
return position;
}
return RenderFrames(result, frames, position, maxElements);
return RenderFrames(context, frames, position, maxElements);
}
private int RenderAttributes(
List<string> result,
ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
HtmlRenderingContext context,
ArrayRange<RenderTreeFrame> frames, int position, int maxElements, out string capturedValueAttribute)
{
capturedValueAttribute = null;
if (maxElements == 0)
{
return position;
}
var result = context.Result;
for (var i = 0; i < maxElements; i++)
{
var candidateIndex = position + i;
@ -216,6 +223,11 @@ namespace Microsoft.AspNetCore.Components.Rendering
return candidateIndex;
}
if (frame.AttributeName.Equals("value", StringComparison.OrdinalIgnoreCase))
{
capturedValueAttribute = frame.AttributeValue as string;
}
switch (frame.AttributeValue)
{
case bool flag when flag:
@ -247,6 +259,13 @@ namespace Microsoft.AspNetCore.Components.Rendering
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.Text.Encodings.Web;
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.Logging.Abstractions;
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 abstract HtmlRenderer GetHtmlRenderer(IServiceProvider serviceProvider);
[Fact]
public void RenderComponentAsync_CanRenderEmptyElement()
{
// Arrange
var expectedHtml = new[] { "<", "p", ">", "</", "p", ">" };
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
{
@ -99,7 +98,6 @@ namespace Microsoft.AspNetCore.Components.Rendering
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponentAsync_CanRenderWithAttributes()
{
@ -272,6 +270,91 @@ namespace Microsoft.AspNetCore.Components.Rendering
// Assert
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]
public void RenderComponentAsync_CanRenderComponentAsyncWithChildrenComponents()
@ -358,11 +441,11 @@ namespace Microsoft.AspNetCore.Components.Rendering
// Act
var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<ComponentWithParameters>(
new ParameterView(new[] {
RenderTreeFrame.Element(0,string.Empty),
RenderTreeFrame.Attribute(1,"update",change),
RenderTreeFrame.Attribute(2,"value",5)
}, 0))));
ParameterView.FromDictionary(new Dictionary<string, object>
{
{ "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, "<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
{