From 436b5461ad0337aac90089aa7ccb61dd875808e4 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Fri, 14 Dec 2018 21:28:05 +0100 Subject: [PATCH] [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. --- src/Components/Components.sln | 4 +- src/Components/build/dependencies.props | 2 + .../Rendering/ComponentState.cs | 1 + .../Rendering/HtmlRenderer.cs | 234 +++++++++ .../Rendering/Renderer.cs | 20 + ...icrosoft.AspNetCore.Components.Test.csproj | 4 +- .../Rendering/HtmlRendererTests.cs | 447 ++++++++++++++++++ 7 files changed, 709 insertions(+), 3 deletions(-) create mode 100644 src/Components/src/Microsoft.AspNetCore.Components/Rendering/HtmlRenderer.cs create mode 100644 src/Components/test/Microsoft.AspNetCore.Components.Test/Rendering/HtmlRendererTests.cs diff --git a/src/Components/Components.sln b/src/Components/Components.sln index 0a909ab6e3..6d1065b2b6 100644 --- a/src/Components/Components.sln +++ b/src/Components/Components.sln @@ -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 diff --git a/src/Components/build/dependencies.props b/src/Components/build/dependencies.props index d7586f1dda..8cf890eb21 100644 --- a/src/Components/build/dependencies.props +++ b/src/Components/build/dependencies.props @@ -9,7 +9,9 @@ 3.0.0-alpha1-20181011.3 2.2.0-preview1-34576 3.0.0-alpha1-10605 + 3.0.0-alpha1-10605 3.0.0-alpha1-10605 + 3.0.0-alpha1-10605 3.0.0-preview1-26907-05 3.0.0-alpha1-10605 0.8.0-preview1-20181122.3 diff --git a/src/Components/src/Microsoft.AspNetCore.Components/Rendering/ComponentState.cs b/src/Components/src/Microsoft.AspNetCore.Components/Rendering/ComponentState.cs index a0ecb672a2..b7d38c6cd4 100644 --- a/src/Components/src/Microsoft.AspNetCore.Components/Rendering/ComponentState.cs +++ b/src/Components/src/Microsoft.AspNetCore.Components/Rendering/ComponentState.cs @@ -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; /// /// Constructs an instance of . diff --git a/src/Components/src/Microsoft.AspNetCore.Components/Rendering/HtmlRenderer.cs b/src/Components/src/Microsoft.AspNetCore.Components/Rendering/HtmlRenderer.cs new file mode 100644 index 0000000000..1918e85b44 --- /dev/null +++ b/src/Components/src/Microsoft.AspNetCore.Components/Rendering/HtmlRenderer.cs @@ -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 +{ + /// + /// A that produces HTML. + /// + public class HtmlRenderer : Renderer + { + private static readonly HashSet SelfClosingElements = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr" + }; + + private readonly Func _htmlEncoder; + + /// + /// Initializes a new instance of . + /// + /// The to use to instantiate components. + /// A that will HTML encode the given string. + public HtmlRenderer(IServiceProvider serviceProvider, Func htmlEncoder) : base(serviceProvider) + { + _htmlEncoder = htmlEncoder; + } + + /// + protected override Task UpdateDisplayAsync(in RenderBatch renderBatch) + { + return Task.CompletedTask; + } + + /// + /// Renders a component into a sequence of fragments that represent the textual representation + /// of the HTML produced by the component. + /// + /// The type of the . + /// A with the initial parameters to render the component. + /// A sequence of fragments that represent the HTML text of the component. + public IEnumerable RenderComponent(ParameterCollection initialParameters) where T : IComponent + { + return RenderComponent(typeof(T), initialParameters); + } + + /// + /// Renders a component into a sequence of fragments that represent the textual representation + /// of the HTML produced by the component. + /// + /// The type of the . + /// A with the initial parameters to render the component. + /// A sequence of fragments that represent the HTML text of the component. + private IEnumerable RenderComponent(Type componentType, ParameterCollection initialParameters) + { + var frames = CreateInitialRender(componentType, initialParameters); + + if (frames.Count == 0) + { + return Array.Empty(); + } + else + { + var result = new List(); + var newPosition = RenderFrames(result, frames, 0, frames.Count); + Debug.Assert(newPosition == frames.Count); + return result; + } + } + + private int RenderFrames(List result, ArrayRange 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 result, + ArrayRange 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 result, + ArrayRange 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 result, + ArrayRange 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(""); + Debug.Assert(afterElement == position + frame.ElementSubtreeLength); + return afterElement; + } + else + { + if (SelfClosingElements.Contains(frame.ElementName)) + { + result.Add(" />"); + } + else + { + result.Add(">"); + result.Add(""); + } + Debug.Assert(afterAttributes == position + frame.ElementSubtreeLength); + return afterAttributes; + } + } + + private int RenderChildren(List result, ArrayRange frames, int position, int maxElements) + { + if (maxElements == 0) + { + return position; + } + + return RenderFrames(result, frames, position, maxElements); + } + + private int RenderAttributes( + List result, + ArrayRange 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 CreateInitialRender(Type componentType, ParameterCollection initialParameters) + { + var component = InstantiateComponent(componentType); + var componentId = AssignRootComponentId(component); + + RenderRootComponent(componentId, initialParameters); + + return GetCurrentRenderTreeFrames(componentId); + } + } +} + diff --git a/src/Components/src/Microsoft.AspNetCore.Components/Rendering/Renderer.cs b/src/Components/src/Microsoft.AspNetCore.Components/Rendering/Renderer.cs index 8498611925..cbdca363e7 100644 --- a/src/Components/src/Microsoft.AspNetCore.Components/Rendering/Renderer.cs +++ b/src/Components/src/Microsoft.AspNetCore.Components/Rendering/Renderer.cs @@ -53,6 +53,13 @@ namespace Microsoft.AspNetCore.Components.Rendering protected int AssignRootComponentId(IComponent component) => AttachAndInitComponent(component, -1).ComponentId; + /// + /// Gets the current render tree for a given component. + /// + /// The id for the component. + /// The representing the current render tree. + private protected ArrayRange GetCurrentRenderTreeFrames(int componentId) => GetRequiredComponentState(componentId).CurrrentRenderTree.GetFrames(); + /// /// 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); } + /// + /// 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. + /// + /// The ID returned by . + /// The with the initial parameters to use for rendering. + protected void RenderRootComponent(int componentId, ParameterCollection initialParameters) + { + GetRequiredComponentState(componentId) + .SetDirectParameters(initialParameters); + } + private ComponentState AttachAndInitComponent(IComponent component, int parentComponentId) { var componentId = _nextComponentId++; diff --git a/src/Components/test/Microsoft.AspNetCore.Components.Test/Microsoft.AspNetCore.Components.Test.csproj b/src/Components/test/Microsoft.AspNetCore.Components.Test/Microsoft.AspNetCore.Components.Test.csproj index 8497a58081..269922dc98 100644 --- a/src/Components/test/Microsoft.AspNetCore.Components.Test/Microsoft.AspNetCore.Components.Test.csproj +++ b/src/Components/test/Microsoft.AspNetCore.Components.Test/Microsoft.AspNetCore.Components.Test.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -11,6 +11,8 @@ + + diff --git a/src/Components/test/Microsoft.AspNetCore.Components.Test/Rendering/HtmlRendererTests.cs b/src/Components/test/Microsoft.AspNetCore.Components.Test/Rendering/HtmlRendererTests.cs new file mode 100644 index 0000000000..040d0cb414 --- /dev/null +++ b/src/Components/test/Microsoft.AspNetCore.Components.Test/Rendering/HtmlRendererTests.cs @@ -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 _encoder = (string t) => HtmlEncoder.Default.Encode(t); + + [Fact] + public void RenderComponent_CanRenderEmptyElement() + { + // Arrange + var expectedHtml = new[] { "<", "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(ParameterCollection.Empty); + + // Assert + Assert.Equal(expectedHtml, result); + } + + [Fact] + public void RenderComponent_CanRenderSimpleComponent() + { + // Arrange + var expectedHtml = new[] { "<", "p", ">", "Hello world!", "" }; + 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(ParameterCollection.Empty); + + // Assert + Assert.Equal(expectedHtml, result); + } + + [Fact] + public void RenderComponent_HtmlEncodesContent() + { + // Arrange + var expectedHtml = new[] { "<", "p", ">", "<Hello world!>", "" }; + var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => + { + rtb.OpenElement(0, "p"); + rtb.AddContent(1, ""); + rtb.CloseElement(); + })).BuildServiceProvider(); + var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder); + + // Act + var result = htmlRenderer.RenderComponent(ParameterCollection.Empty); + + // Assert + Assert.Equal(expectedHtml, result); + } + + + [Fact] + public void RenderComponent_DoesNotEncodeMarkup() + { + // Arrange + var expectedHtml = new[] { "<", "p", ">", "Hello world!", "" }; + var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => + { + rtb.OpenElement(0, "p"); + rtb.AddMarkupContent(1, "Hello world!"); + rtb.CloseElement(); + })).BuildServiceProvider(); + var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder); + + // Act + var result = htmlRenderer.RenderComponent(ParameterCollection.Empty); + + // Assert + Assert.Equal(expectedHtml, result); + } + + + [Fact] + public void RenderComponent_CanRenderWithAttributes() + { + // Arrange + var expectedHtml = new[] { "<", "p", " ", "class", "=", "\"", "lead", "\"", ">", "Hello world!", "" }; + 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(ParameterCollection.Empty); + + // Assert + Assert.Equal(expectedHtml, result); + } + + [Fact] + public void RenderComponent_HtmlEncodesAttributeValues() + { + // Arrange + var expectedHtml = new[] { "<", "p", " ", "class", "=", "\"", "<lead", "\"", ">", "Hello world!", "" }; + var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => + { + rtb.OpenElement(0, "p"); + rtb.AddAttribute(1, "class", "(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(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(ParameterCollection.Empty); + + // Assert + Assert.Equal(expectedHtml, result); + } + + [Fact] + public void RenderComponent_CanRenderWithChildren() + { + // Arrange + var expectedHtml = new[] { "<", "p", ">", "<", "span", ">", "Hello world!", "", "" }; + 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(ParameterCollection.Empty); + + // Assert + Assert.Equal(expectedHtml, result); + } + + [Fact] + public void RenderComponent_CanRenderWithMultipleChildren() + { + // Arrange + var expectedHtml = new[] { "<", "p", ">", + "<", "span", ">", "Hello world!", "", + "<", "span", ">", "Bye Bye world!", "", + "" + }; + 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(ParameterCollection.Empty); + + // Assert + Assert.Equal(expectedHtml, result); + } + + [Fact] + public void RenderComponent_CanRenderComponentWithChildrenComponents() + { + // Arrange + var expectedHtml = new[] { + "<", "p", ">", "<", "span", ">", "Hello world!", "", "", + "<", "span", ">", "Child content!", "" + }; + 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(ParameterCollection.Empty); + + // Assert + Assert.Equal(expectedHtml, result); + } + + [Fact] + public void RenderComponent_ComponentReferenceNoops() + { + // Arrange + var expectedHtml = new[] { + "<", "p", ">", "<", "span", ">", "Hello world!", "", "", + "<", "span", ">", "Child content!", "" + }; + 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(ParameterCollection.Empty); + + // Assert + Assert.Equal(expectedHtml, result); + } + + [Fact] + public void RenderComponent_CanPassParameters() + { + // Arrange + var expectedHtml = new[] { + "<", "p", ">", "<", "input", " ", "value", "=", "\"", "5", "\"", " />", "" }; + + RenderFragment Content(ParameterCollection pc) => new RenderFragment((RenderTreeBuilder rtb) => + { + rtb.OpenElement(0, "p"); + rtb.OpenElement(1, "input"); + rtb.AddAttribute(2, "change", pc.GetValueOrDefault>("update")); + rtb.AddAttribute(3, "value", pc.GetValueOrDefault("value")); + rtb.CloseElement(); + rtb.CloseElement(); + }); + + var serviceProvider = new ServiceCollection() + .AddSingleton(new Func(Content)) + .BuildServiceProvider(); + + var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder); + Action change = (UIChangeEventArgs changeArgs) => throw new InvalidOperationException(); + + // Act + var result = htmlRenderer.RenderComponent( + 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!", "", "" }; + 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(ParameterCollection.Empty); + + // Assert + Assert.Equal(expectedHtml, result); + } + + [Fact] + public void RenderComponent_ElementRefsNoops() + { + // Arrange + var expectedHtml = new[] { + "<", "p", ">", "<", "span", ">", "Hello world!", "", "" }; + 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(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 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("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); + } + } + } +}