diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorIntermediateNodeWriter.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorIntermediateNodeWriter.cs
index 99e9ad0d41..35128dafb2 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorIntermediateNodeWriter.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorIntermediateNodeWriter.cs
@@ -19,8 +19,6 @@ namespace Microsoft.AspNetCore.Blazor.Razor
///
internal class BlazorIntermediateNodeWriter : IntermediateNodeWriter
{
- private const string builderVarName = "builder";
-
// Per the HTML spec, the following elements are inherently self-closing
// For example,
is the same as
(and therefore it cannot contain descendants)
private static HashSet htmlVoidElementsLookup
@@ -77,7 +75,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
{
if (node.Children[i] is IntermediateToken token && token.IsCSharp)
{
- _scopeStack.IncrementCurrentScopeChildCount();
+ _scopeStack.IncrementCurrentScopeChildCount(context);
context.CodeWriter.Write(token.Content);
}
else
@@ -121,9 +119,9 @@ namespace Microsoft.AspNetCore.Blazor.Razor
// Since we're not in the middle of writing an element, this must evaluate as some
// text to display
- _scopeStack.IncrementCurrentScopeChildCount();
+ _scopeStack.IncrementCurrentScopeChildCount(context);
context.CodeWriter
- .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddContent)}")
+ .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.AddContent)}")
.Write((_sourceSequence++).ToString())
.WriteParameterSeparator();
@@ -211,9 +209,9 @@ namespace Microsoft.AspNetCore.Blazor.Razor
case HtmlTokenType.Character:
{
// Text node
- _scopeStack.IncrementCurrentScopeChildCount();
+ _scopeStack.IncrementCurrentScopeChildCount(context);
codeWriter
- .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddContent)}")
+ .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.AddContent)}")
.Write((_sourceSequence++).ToString())
.WriteParameterSeparator()
.WriteStringLiteral(nextToken.Data)
@@ -230,18 +228,18 @@ namespace Microsoft.AspNetCore.Blazor.Razor
if (nextToken.Type == HtmlTokenType.StartTag)
{
- _scopeStack.IncrementCurrentScopeChildCount();
+ _scopeStack.IncrementCurrentScopeChildCount(context);
if (isComponent)
{
codeWriter
- .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.OpenComponent)}<{componentTypeName}>")
+ .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.OpenComponent)}<{componentTypeName}>")
.Write((_sourceSequence++).ToString())
.WriteEndMethodInvocation();
}
else
{
codeWriter
- .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.OpenElement)}")
+ .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.OpenElement)}")
.Write((_sourceSequence++).ToString())
.WriteParameterSeparator()
.WriteStringLiteral(nextTag.Data)
@@ -267,7 +265,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
foreach (var token in _currentElementAttributeTokens)
{
codeWriter
- .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}")
+ .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}")
.Write((_sourceSequence++).ToString())
.WriteParameterSeparator()
.Write(token.AttributeValue.Content)
@@ -286,6 +284,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|| (!isComponent && htmlVoidElementsLookup.Contains(nextTag.Data)))
{
_scopeStack.CloseScope(
+ context: context,
tagName: isComponent ? tagNameOriginalCase : nextTag.Data,
isComponent: isComponent,
source: CalculateSourcePosition(node.Source, nextToken.Position));
@@ -293,7 +292,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
? nameof(RenderTreeBuilder.CloseComponent)
: nameof(RenderTreeBuilder.CloseElement);
codeWriter
- .WriteStartMethodInvocation($"{builderVarName}.{closeMethodName}")
+ .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{closeMethodName}")
.WriteEndMethodInvocation();
}
break;
@@ -372,15 +371,20 @@ namespace Microsoft.AspNetCore.Blazor.Razor
}
private void WriteAttribute(CodeWriter codeWriter, string key, object value)
+ {
+ BeginWriteAttribute(codeWriter, key);
+ WriteAttributeValue(codeWriter, value);
+ codeWriter.WriteEndMethodInvocation();
+ }
+
+ public void BeginWriteAttribute(CodeWriter codeWriter, string key)
{
codeWriter
- .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}")
+ .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}")
.Write((_sourceSequence++).ToString())
.WriteParameterSeparator()
.WriteStringLiteral(key)
.WriteParameterSeparator();
- WriteAttributeValue(codeWriter, value);
- codeWriter.WriteEndMethodInvocation();
}
public override void WriteUsingDirective(CodeRenderingContext context, UsingDirectiveIntermediateNode node)
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/RenderTreeBuilder.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/RenderTreeBuilder.cs
index 40dd24f7f9..00742c7f34 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/RenderTreeBuilder.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/RenderTreeBuilder.cs
@@ -22,5 +22,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
public static readonly string Clear = nameof(Clear);
public static readonly string GetFrames = nameof(GetFrames);
+
+ public static readonly string ChildContent = nameof(ChildContent);
}
}
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ScopeStack.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ScopeStack.cs
index abc543a36c..f61ffac0ed 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ScopeStack.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ScopeStack.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Razor.Language;
+using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using System;
using System.Collections.Generic;
@@ -14,14 +15,19 @@ namespace Microsoft.AspNetCore.Blazor.Razor
///
internal class ScopeStack
{
+ private const string _renderFragmentTypeName = "Microsoft.AspNetCore.Blazor.RenderTree.RenderFragment";
+
private readonly Stack _stack = new Stack();
+ private int _builderVarNumber = 1;
+
+ public string BuilderVarName { get; private set; } = "builder";
public void OpenScope(string tagName, bool isComponent)
{
_stack.Push(new ScopeEntry(tagName, isComponent));
}
- public void CloseScope(string tagName, bool isComponent, SourceSpan? source)
+ public void CloseScope(CodeRenderingContext context, string tagName, bool isComponent, SourceSpan? source)
{
if (_stack.Count == 0)
{
@@ -29,26 +35,59 @@ namespace Microsoft.AspNetCore.Blazor.Razor
$"Unexpected closing tag '{tagName}' with no matching start tag.", source);
}
- var expected = _stack.Pop();
- if (!tagName.Equals(expected.TagName, StringComparison.Ordinal))
+ var currentScope = _stack.Pop();
+ if (!tagName.Equals(currentScope.TagName, StringComparison.Ordinal))
{
throw new RazorCompilerException(
- $"Mismatching closing tag. Found '{tagName}' but expected '{expected.TagName}'.", source);
+ $"Mismatching closing tag. Found '{tagName}' but expected '{currentScope.TagName}'.", source);
}
// Note: there's no unit test to cover the following, because there's no known way of
- // triggering it from user code (i.e., Razor source code). But the test is here anyway
+ // triggering it from user code (i.e., Razor source code). But the check is here anyway
// just in case one day it turns out there is some way of causing this error.
- if (isComponent != expected.IsComponent)
+ if (isComponent != currentScope.IsComponent)
{
throw new RazorCompilerException(
- $"Mismatching closing tag. Found '{tagName}' of type '{(isComponent ? "component" : "element")}' but expected type '{(expected.IsComponent ? "component" : "element")}'.");
+ $"Mismatching closing tag. Found '{tagName}' of type '{(isComponent ? "component" : "element")}' but expected type '{(currentScope.IsComponent ? "component" : "element")}'.", source);
+ }
+
+ // When closing the scope for a component with children, it's time to close the lambda
+ if (currentScope.LambdaScope != null)
+ {
+ currentScope.LambdaScope.Dispose();
+ context.CodeWriter.Write(")");
+ context.CodeWriter.WriteEndMethodInvocation();
+ OffsetBuilderVarNumber(-1);
}
}
- public void IncrementCurrentScopeChildCount()
+ public void IncrementCurrentScopeChildCount(CodeRenderingContext context)
{
+ if (_stack.Count > 0)
+ {
+ var currentScope = _stack.Peek();
+ if (currentScope.IsComponent && currentScope.ChildCount == 0)
+ {
+ // When we're about to insert the first child into a component,
+ // it's time to open a new lambda
+ var blazorNodeWriter = (BlazorIntermediateNodeWriter)context.NodeWriter;
+ blazorNodeWriter.BeginWriteAttribute(context.CodeWriter, RenderTreeBuilder.ChildContent);
+ OffsetBuilderVarNumber(1);
+ context.CodeWriter.Write($"({_renderFragmentTypeName})(");
+ currentScope.LambdaScope = context.CodeWriter.BuildLambda(BuilderVarName);
+ }
+
+ currentScope.ChildCount++;
+ }
+ }
+
+ private void OffsetBuilderVarNumber(int delta)
+ {
+ _builderVarNumber += delta;
+ BuilderVarName = _builderVarNumber == 1
+ ? "builder"
+ : $"builder{_builderVarNumber}";
}
private class ScopeEntry
@@ -56,6 +95,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
public readonly string TagName;
public readonly bool IsComponent;
public int ChildCount;
+ public IDisposable LambdaScope;
public ScopeEntry(string tagName, bool isComponent)
{
diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs
index 4b5f7e6bb2..e4936a1fd0 100644
--- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs
+++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs
@@ -23,6 +23,11 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
private readonly Stack _openElementIndices = new Stack();
private RenderTreeFrameType? _lastNonAttributeFrameType;
+ ///
+ /// The reserved parameter name used for supplying child content.
+ ///
+ public static string ChildContent = nameof(ChildContent);
+
///
/// Constructs an instance of .
///
diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs
index 9a91cb5bef..ef5745aa00 100644
--- a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs
@@ -420,13 +420,53 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
$"");
var frames = GetRenderTree(component);
+ // Assert: component frames are correct
Assert.Collection(frames,
- frame => AssertFrame.Component(frame, 6, 0),
+ frame => AssertFrame.Component(frame, 3, 0),
frame => AssertFrame.Attribute(frame, "attr", "abc", 1),
- frame => AssertFrame.Text(frame, "Some text", 2),
- frame => AssertFrame.Element(frame, "some-child", 3, 3),
- frame => AssertFrame.Attribute(frame, "a", "1", 4),
- frame => AssertFrame.Text(frame, "Nested text", 5));
+ frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 2));
+
+ // Assert: Captured ChildContent frames are correct
+ var childFrames = GetFrames((RenderFragment)frames[2].AttributeValue);
+ Assert.Collection(childFrames,
+ frame => AssertFrame.Text(frame, "Some text", 3),
+ frame => AssertFrame.Element(frame, "some-child", 3, 4),
+ frame => AssertFrame.Attribute(frame, "a", "1", 5),
+ frame => AssertFrame.Text(frame, "Nested text", 6));
+ }
+
+ [Fact]
+ public void CanNestComponentChildContent()
+ {
+ // Arrange/Act
+ var testComponentTypeName = typeof(TestComponent).FullName.Replace('+', '.');
+ var component = CompileToComponent(
+ $"" +
+ $"" +
+ $"Some text" +
+ $"" +
+ $"");
+ var frames = GetRenderTree(component);
+
+ // Assert: outer component frames are correct
+ Assert.Collection(frames,
+ frame => AssertFrame.Component(frame, 2, 0),
+ frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 1));
+
+ // Assert: first level of ChildContent is correct
+ // Note that we don't really need the sequence numbers to continue on from the
+ // sequence numbers at the parent level. All that really matters is that they are
+ // correct relative to each other (i.e., incrementing) within the nesting level.
+ // As an implementation detail, it happens that they do follow on from the parent
+ // level, but we could change that part of the implementation if we wanted.
+ var innerFrames = GetFrames((RenderFragment)frames[1].AttributeValue).ToArray();
+ Assert.Collection(innerFrames,
+ frame => AssertFrame.Component(frame, 2, 2),
+ frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 3));
+
+ // Assert: second level of ChildContent is correct
+ Assert.Collection(GetFrames((RenderFragment)innerFrames[1].AttributeValue),
+ frame => AssertFrame.Text(frame, "Some text", 4));
}
[Fact]
@@ -651,6 +691,13 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
}
}
+ private ArrayRange GetFrames(RenderFragment fragment)
+ {
+ var builder = new RenderTreeBuilder(new TestRenderer());
+ fragment(builder);
+ return builder.GetFrames();
+ }
+
private class CompileToCSharpResult
{
public string Code { get; set; }