Prerender select elements with value; move HtmlRenderer into Mvc.ViewFeatures (#12996)
This commit is contained in:
parent
826ed7504b
commit
6b2d9f23f8
|
|
@ -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() { }
|
||||
|
|
|
|||
|
|
@ -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() { }
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
{
|
||||
Loading…
Reference in New Issue