[Components] Create a renderer to render components into HTML (#4463)

* Adds an HtmlRenderer to Microsoft.AspNetCore.Components
 * It renders the component into a list of strings.
 * It only handles synchronous rendering.
This commit is contained in:
Javier Calvarro Nelson 2018-12-14 21:28:05 +01:00 committed by GitHub
parent a7f6154520
commit 436b5461ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 709 additions and 3 deletions

View File

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27130.2010
# Visual Studio Version 16
VisualStudioVersion = 16.0.28315.86
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{F5FDD4E5-6A52-4A86-BE5E-5E42CB1DC8DA}"
EndProject

View File

@ -9,7 +9,9 @@
<InternalAspNetCoreSdkPackageVersion>3.0.0-alpha1-20181011.3</InternalAspNetCoreSdkPackageVersion>
<MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>2.2.0-preview1-34576</MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>
<MicrosoftAspNetCoreAppPackageVersion>3.0.0-alpha1-10605</MicrosoftAspNetCoreAppPackageVersion>
<MicrosoftAspNetCoreHtmlAbstractionsPackageVersion>3.0.0-alpha1-10605</MicrosoftAspNetCoreHtmlAbstractionsPackageVersion>
<MicrosoftAspNetCoreRazorDesignPackageVersion>3.0.0-alpha1-10605</MicrosoftAspNetCoreRazorDesignPackageVersion>
<MicrosoftExtensionsDependencyInjectionPackageVersion>3.0.0-alpha1-10605</MicrosoftExtensionsDependencyInjectionPackageVersion>
<MicrosoftNETCoreAppPackageVersion>3.0.0-preview1-26907-05</MicrosoftNETCoreAppPackageVersion>
<SignalRPackageVersion>3.0.0-alpha1-10605</SignalRPackageVersion>
<TemplateBlazorPackageVersion>0.8.0-preview1-20181122.3</TemplateBlazorPackageVersion>

View File

@ -29,6 +29,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
public int ComponentId => _componentId;
public IComponent Component => _component;
public ComponentState ParentComponentState => _parentComponentState;
public RenderTreeBuilder CurrrentRenderTree => _renderTreeBuilderCurrent;
/// <summary>
/// Constructs an instance of <see cref="ComponentState"/>.

View File

@ -0,0 +1,234 @@
// 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 System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.RenderTree;
namespace Microsoft.AspNetCore.Components.Rendering
{
/// <summary>
/// A <see cref="Renderer"/> that produces HTML.
/// </summary>
public class HtmlRenderer : Renderer
{
private static readonly HashSet<string> SelfClosingElements = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"
};
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="htmlEncoder">A <see cref="Func{T, TResult}"/> that will HTML encode the given string.</param>
public HtmlRenderer(IServiceProvider serviceProvider, Func<string, string> htmlEncoder) : base(serviceProvider)
{
_htmlEncoder = htmlEncoder;
}
/// <inheritdoc />
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
{
return Task.CompletedTask;
}
/// <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="T">The type of the <see cref="IComponent"/>.</typeparam>
/// <param name="initialParameters">A <see cref="ParameterCollection"/> with the initial parameters to render the component.</param>
/// <returns>A sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
public IEnumerable<string> RenderComponent<T>(ParameterCollection initialParameters) where T : IComponent
{
return RenderComponent(typeof(T), initialParameters);
}
/// <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="ParameterCollection"/> with the initial parameters to render the component.</param>
/// <returns>A sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
private IEnumerable<string> RenderComponent(Type componentType, ParameterCollection initialParameters)
{
var frames = CreateInitialRender(componentType, initialParameters);
if (frames.Count == 0)
{
return Array.Empty<string>();
}
else
{
var result = new List<string>();
var newPosition = RenderFrames(result, frames, 0, frames.Count);
Debug.Assert(newPosition == frames.Count);
return result;
}
}
private int RenderFrames(List<string> result, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
{
var nextPosition = position;
var endPosition = position + maxElements;
while (position < endPosition)
{
nextPosition = RenderCore(result, frames, position, maxElements);
if (position == nextPosition)
{
throw new InvalidOperationException("We didn't consume any input.");
}
position = nextPosition;
}
return nextPosition;
}
private int RenderCore(
List<string> result,
ArrayRange<RenderTreeFrame> frames,
int position,
int length)
{
ref var frame = ref frames.Array[position];
switch (frame.FrameType)
{
case RenderTreeFrameType.Element:
return RenderElement(result, frames, position);
case RenderTreeFrameType.Attribute:
return RenderAttributes(result, frames, position, 1);
case RenderTreeFrameType.Text:
result.Add(_htmlEncoder(frame.TextContent));
return ++position;
case RenderTreeFrameType.Markup:
result.Add(frame.MarkupContent);
return ++position;
case RenderTreeFrameType.Component:
return RenderChildComponent(result, frames, position);
case RenderTreeFrameType.Region:
return RenderFrames(result, frames, position + 1, frame.RegionSubtreeLength - 1);
case RenderTreeFrameType.ElementReferenceCapture:
case RenderTreeFrameType.ComponentReferenceCapture:
return ++position;
default:
throw new InvalidOperationException($"Invalid element frame type '{frame.FrameType}'.");
}
}
private int RenderChildComponent(
List<string> result,
ArrayRange<RenderTreeFrame> frames,
int position)
{
ref var frame = ref frames.Array[position];
var childFrames = GetCurrentRenderTreeFrames(frame.ComponentId);
RenderFrames(result, childFrames, 0, childFrames.Count);
return position + frame.ComponentSubtreeLength;
}
private int RenderElement(
List<string> result,
ArrayRange<RenderTreeFrame> frames,
int position)
{
ref var frame = ref frames.Array[position];
result.Add("<");
result.Add(frame.ElementName);
var afterAttributes = RenderAttributes(result, frames, position + 1, frame.ElementSubtreeLength - 1);
var remainingElements = frame.ElementSubtreeLength + position - afterAttributes;
if (remainingElements > 0)
{
result.Add(">");
var afterElement = RenderChildren(result, frames, afterAttributes, remainingElements);
result.Add("</");
result.Add(frame.ElementName);
result.Add(">");
Debug.Assert(afterElement == position + frame.ElementSubtreeLength);
return afterElement;
}
else
{
if (SelfClosingElements.Contains(frame.ElementName))
{
result.Add(" />");
}
else
{
result.Add(">");
result.Add("</");
result.Add(frame.ElementName);
result.Add(">");
}
Debug.Assert(afterAttributes == position + frame.ElementSubtreeLength);
return afterAttributes;
}
}
private int RenderChildren(List<string> result, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
{
if (maxElements == 0)
{
return position;
}
return RenderFrames(result, frames, position, maxElements);
}
private int RenderAttributes(
List<string> result,
ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
{
if (maxElements == 0)
{
return position;
}
for (var i = 0; i < maxElements; i++)
{
var candidateIndex = position + i;
ref var frame = ref frames.Array[candidateIndex];
if (frame.FrameType != RenderTreeFrameType.Attribute)
{
return candidateIndex;
}
switch (frame.AttributeValue)
{
case bool flag when flag:
result.Add(" ");
result.Add(frame.AttributeName);
break;
case string value:
result.Add(" ");
result.Add(frame.AttributeName);
result.Add("=");
result.Add("\"");
result.Add(_htmlEncoder(value));
result.Add("\"");
break;
default:
break;
}
}
return position + maxElements;
}
private ArrayRange<RenderTreeFrame> CreateInitialRender(Type componentType, ParameterCollection initialParameters)
{
var component = InstantiateComponent(componentType);
var componentId = AssignRootComponentId(component);
RenderRootComponent(componentId, initialParameters);
return GetCurrentRenderTreeFrames(componentId);
}
}
}

View File

@ -53,6 +53,13 @@ namespace Microsoft.AspNetCore.Components.Rendering
protected int AssignRootComponentId(IComponent component)
=> AttachAndInitComponent(component, -1).ComponentId;
/// <summary>
/// Gets the current render tree for a given component.
/// </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).CurrrentRenderTree.GetFrames();
/// <summary>
/// Performs the first render for a root component. After this, the root component
/// makes its own decisions about when to re-render, so there is no need to call
@ -65,6 +72,19 @@ namespace Microsoft.AspNetCore.Components.Rendering
.SetDirectParameters(ParameterCollection.Empty);
}
/// <summary>
/// Performs the first render for a root component. After this, the root component
/// makes its own decisions about when to re-render, so there is no need to call
/// this more than once.
/// </summary>
/// <param name="componentId">The ID returned by <see cref="AssignRootComponentId(IComponent)"/>.</param>
/// <param name="initialParameters">The <see cref="ParameterCollection"/>with the initial parameters to use for rendering.</param>
protected void RenderRootComponent(int componentId, ParameterCollection initialParameters)
{
GetRequiredComponentState(componentId)
.SetDirectParameters(initialParameters);
}
private ComponentState AttachAndInitComponent(IComponent component, int parentComponentId)
{
var componentId = _nextComponentId++;

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
@ -11,6 +11,8 @@
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Html.Abstractions" Version="$(MicrosoftAspNetCoreHtmlAbstractionsPackageVersion)" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,447 @@
// 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 System.Text.Encodings.Web;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Microsoft.AspNetCore.Components.Rendering
{
public class HtmlRendererTests
{
private static readonly Func<string, string> _encoder = (string t) => HtmlEncoder.Default.Encode(t);
[Fact]
public void RenderComponent_CanRenderEmptyElement()
{
// Arrange
var expectedHtml = new[] { "<", "p", ">", "</", "p", ">" };
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
{
rtb.OpenElement(0, "p");
rtb.CloseElement();
})).BuildServiceProvider();
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
// Act
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_CanRenderSimpleComponent()
{
// Arrange
var expectedHtml = new[] { "<", "p", ">", "Hello world!", "</", "p", ">" };
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
{
rtb.OpenElement(0, "p");
rtb.AddContent(1, "Hello world!");
rtb.CloseElement();
})).BuildServiceProvider();
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
// Act
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_HtmlEncodesContent()
{
// Arrange
var expectedHtml = new[] { "<", "p", ">", "&lt;Hello world!&gt;", "</", "p", ">" };
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
{
rtb.OpenElement(0, "p");
rtb.AddContent(1, "<Hello world!>");
rtb.CloseElement();
})).BuildServiceProvider();
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
// Act
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_DoesNotEncodeMarkup()
{
// Arrange
var expectedHtml = new[] { "<", "p", ">", "<span>Hello world!</span>", "</", "p", ">" };
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
{
rtb.OpenElement(0, "p");
rtb.AddMarkupContent(1, "<span>Hello world!</span>");
rtb.CloseElement();
})).BuildServiceProvider();
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
// Act
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_CanRenderWithAttributes()
{
// Arrange
var expectedHtml = new[] { "<", "p", " ", "class", "=", "\"", "lead", "\"", ">", "Hello world!", "</", "p", ">" };
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
{
rtb.OpenElement(0, "p");
rtb.AddAttribute(1, "class", "lead");
rtb.AddContent(2, "Hello world!");
rtb.CloseElement();
})).BuildServiceProvider();
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
// Act
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_HtmlEncodesAttributeValues()
{
// Arrange
var expectedHtml = new[] { "<", "p", " ", "class", "=", "\"", "&lt;lead", "\"", ">", "Hello world!", "</", "p", ">" };
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
{
rtb.OpenElement(0, "p");
rtb.AddAttribute(1, "class", "<lead");
rtb.AddContent(2, "Hello world!");
rtb.CloseElement();
})).BuildServiceProvider();
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
// Act
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_CanRenderBooleanAttributes()
{
// Arrange
var expectedHtml = new[] { "<", "input", " ", "disabled", " />" };
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
{
rtb.OpenElement(0, "input");
rtb.AddAttribute(1, "disabled", true);
rtb.CloseElement();
})).BuildServiceProvider();
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
// Act
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_DoesNotRenderBooleanAttributesWhenValueIsFalse()
{
// Arrange
var expectedHtml = new[] { "<", "input", " />" };
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
{
rtb.OpenElement(0, "input");
rtb.AddAttribute(1, "disabled", false);
rtb.CloseElement();
})).BuildServiceProvider();
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
// Act
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_CanRenderWithChildren()
{
// Arrange
var expectedHtml = new[] { "<", "p", ">", "<", "span", ">", "Hello world!", "</", "span", ">", "</", "p", ">" };
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
{
rtb.OpenElement(0, "p");
rtb.OpenElement(1, "span");
rtb.AddContent(2, "Hello world!");
rtb.CloseElement();
rtb.CloseElement();
})).BuildServiceProvider();
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
// Act
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_CanRenderWithMultipleChildren()
{
// Arrange
var expectedHtml = new[] { "<", "p", ">",
"<", "span", ">", "Hello world!", "</", "span", ">",
"<", "span", ">", "Bye Bye world!", "</", "span", ">",
"</", "p", ">"
};
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
{
rtb.OpenElement(0, "p");
rtb.OpenElement(1, "span");
rtb.AddContent(2, "Hello world!");
rtb.CloseElement();
rtb.OpenElement(3, "span");
rtb.AddContent(4, "Bye Bye world!");
rtb.CloseElement();
rtb.CloseElement();
})).BuildServiceProvider();
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
// Act
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_CanRenderComponentWithChildrenComponents()
{
// Arrange
var expectedHtml = new[] {
"<", "p", ">", "<", "span", ">", "Hello world!", "</", "span", ">", "</", "p", ">",
"<", "span", ">", "Child content!", "</", "span", ">"
};
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
{
rtb.OpenElement(0, "p");
rtb.OpenElement(1, "span");
rtb.AddContent(2, "Hello world!");
rtb.CloseElement();
rtb.CloseElement();
rtb.OpenComponent(3, typeof(ChildComponent));
rtb.AddAttribute(4, "Value", "Child content!");
rtb.CloseComponent();
})).BuildServiceProvider();
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
// Act
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_ComponentReferenceNoops()
{
// Arrange
var expectedHtml = new[] {
"<", "p", ">", "<", "span", ">", "Hello world!", "</", "span", ">", "</", "p", ">",
"<", "span", ">", "Child content!", "</", "span", ">"
};
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
{
rtb.OpenElement(0, "p");
rtb.OpenElement(1, "span");
rtb.AddContent(2, "Hello world!");
rtb.CloseElement();
rtb.CloseElement();
rtb.OpenComponent(3, typeof(ChildComponent));
rtb.AddAttribute(4, "Value", "Child content!");
rtb.AddComponentReferenceCapture(5, cr => { });
rtb.CloseComponent();
})).BuildServiceProvider();
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
// Act
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_CanPassParameters()
{
// Arrange
var expectedHtml = new[] {
"<", "p", ">", "<", "input", " ", "value", "=", "\"", "5", "\"", " />", "</", "p", ">" };
RenderFragment Content(ParameterCollection pc) => new RenderFragment((RenderTreeBuilder rtb) =>
{
rtb.OpenElement(0, "p");
rtb.OpenElement(1, "input");
rtb.AddAttribute(2, "change", pc.GetValueOrDefault<Action<UIChangeEventArgs>>("update"));
rtb.AddAttribute(3, "value", pc.GetValueOrDefault<int>("value"));
rtb.CloseElement();
rtb.CloseElement();
});
var serviceProvider = new ServiceCollection()
.AddSingleton(new Func<ParameterCollection, RenderFragment>(Content))
.BuildServiceProvider();
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
Action<UIChangeEventArgs> change = (UIChangeEventArgs changeArgs) => throw new InvalidOperationException();
// Act
var result = htmlRenderer.RenderComponent<ComponentWithParameters>(
new ParameterCollection(new[] {
RenderTreeFrame.Element(0,string.Empty),
RenderTreeFrame.Attribute(1,"update",change),
RenderTreeFrame.Attribute(2,"value",5)
}, 0));
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_CanRenderComponentWithRenderFragmentContent()
{
// Arrange
var expectedHtml = new[] {
"<", "p", ">", "<", "span", ">", "Hello world!", "</", "span", ">", "</", "p", ">" };
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
{
rtb.OpenElement(0, "p");
rtb.OpenElement(1, "span");
rtb.AddContent(2,
// This internally creates a region frame.
rf => rf.AddContent(0, "Hello world!"));
rtb.CloseElement();
rtb.CloseElement();
})).BuildServiceProvider();
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
// Act
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_ElementRefsNoops()
{
// Arrange
var expectedHtml = new[] {
"<", "p", ">", "<", "span", ">", "Hello world!", "</", "span", ">", "</", "p", ">" };
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
{
rtb.OpenElement(0, "p");
rtb.AddElementReferenceCapture(1, er => { });
rtb.OpenElement(2, "span");
rtb.AddContent(3,
// This internally creates a region frame.
rf => rf.AddContent(0, "Hello world!"));
rtb.CloseElement();
rtb.CloseElement();
})).BuildServiceProvider();
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
// Act
var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
// Assert
Assert.Equal(expectedHtml, result);
}
private class ComponentWithParameters : IComponent
{
public RenderHandle RenderHandle { get; private set; }
public void Init(RenderHandle renderHandle)
{
RenderHandle = renderHandle;
}
[Inject]
Func<ParameterCollection, RenderFragment> CreateRenderFragment { get; set; }
public void SetParameters(ParameterCollection parameters)
{
RenderHandle.Render(CreateRenderFragment(parameters));
}
}
private class ChildComponent : IComponent
{
private RenderHandle _renderHandle;
public void Init(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}
public void SetParameters(ParameterCollection parameters)
{
_renderHandle.Render(CreateRenderFragment(parameters));
}
private RenderFragment CreateRenderFragment(ParameterCollection parameters)
{
return RenderFragment;
void RenderFragment(RenderTreeBuilder rtb)
{
rtb.OpenElement(1, "span");
rtb.AddContent(2, parameters.GetValueOrDefault<string>("Value"));
rtb.CloseElement();
}
}
}
private class TestComponent : IComponent
{
private RenderHandle _renderHandle;
[Inject]
public RenderFragment Fragment { get; set; }
public void Init(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}
public void SetParameters(ParameterCollection parameters)
{
_renderHandle.Render(Fragment);
}
}
}
}