diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDiagnosticFactory.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDiagnosticFactory.cs index d08afb663a..715a2447b3 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDiagnosticFactory.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDiagnosticFactory.cs @@ -308,6 +308,16 @@ namespace Microsoft.AspNetCore.Blazor.Razor var attributesText = string.Join(", ", attributes.Select(a => $"'{a.Name}'")); return RazorDiagnostic.Create(GenericComponentTypeInferenceUnderspecified, source ?? SourceSpan.Undefined, component.TagName, attributesText); } - } + public static readonly RazorDiagnosticDescriptor ChildContentHasInvalidParameterOnComponent = + new RazorDiagnosticDescriptor( + "BL10002", + () => "Invalid parameter name. The parameter name attribute '{0}' on component '{1}' can only include literal text.", + RazorDiagnosticSeverity.Error); + + public static RazorDiagnostic Create_ChildContentHasInvalidParameterOnComponent(SourceSpan? source, string attribute, string element) + { + return RazorDiagnostic.Create(ChildContentHasInvalidParameterOnComponent, source ?? SourceSpan.Undefined, attribute, element); + } + } } diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorMetadata.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorMetadata.cs index cebb3d9041..b6b4d5b156 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorMetadata.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorMetadata.cs @@ -33,12 +33,24 @@ namespace Microsoft.AspNetCore.Blazor.Razor public static readonly string TagHelperKind = "Blazor.ChildContent"; public static readonly string ParameterNameBoundAttributeKind = "Blazor.ChildContentParameterName"; + + /// + /// The name of the synthesized attribute used to set a child content parameter. + /// + public static readonly string ParameterAttributeName = "Context"; + + /// + /// The default name of the child content parameter (unless set by a Context attribute). + /// + public static readonly string DefaultParameterName = "context"; } public static class Component { public static readonly string ChildContentKey = "Blazor.ChildContent"; + public static readonly string ChildContentParameterNameKey = "Blazor.ChildContentParameterName"; + public static readonly string DelegateSignatureKey = "Blazor.DelegateSignature"; public static readonly string WeaklyTypedKey = "Blazor.IsWeaklyTyped"; diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentChildContentIntermediateNode.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentChildContentIntermediateNode.cs index 211eccfb6f..6b39021b6e 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentChildContentIntermediateNode.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentChildContentIntermediateNode.cs @@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor public bool IsParameterized => BoundAttribute?.IsParameterizedChildContentProperty() ?? false; - public string ParameterName { get; set; } = "context"; + public string ParameterName { get; set; } public string TypeName { get; set; } diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentExtensionNode.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentExtensionNode.cs index 226aa5aa88..28eaf62412 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentExtensionNode.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentExtensionNode.cs @@ -23,6 +23,11 @@ namespace Microsoft.AspNetCore.Blazor.Razor public TagHelperDescriptor Component { get; set; } + /// + /// Gets the child content parameter name (null if unset) that was applied at the component level. + /// + public string ChildContentParameterName { get; set; } + public IEnumerable TypeArguments => Children.OfType(); public string TagName { get; set; } diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentLoweringPass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentLoweringPass.cs index c7264c2e60..34cd953c67 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentLoweringPass.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentLoweringPass.cs @@ -80,6 +80,13 @@ namespace Microsoft.AspNetCore.Blazor.Razor var visitor = new ComponentRewriteVisitor(component); visitor.Visit(node); + // Fixup the parameter names of child content elements. We can't do this during the rewrite + // because we see the nodes in the wrong order. + foreach (var childContent in component.ChildContents) + { + childContent.ParameterName = childContent.ParameterName ?? component.ChildContentParameterName ?? BlazorMetadata.ChildContent.DefaultParameterName; + } + return component; } @@ -234,7 +241,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor } else if (child is TagHelperPropertyIntermediateNode property) { - if (property.BoundAttribute.Kind == BlazorMetadata.ChildContent.TagHelperKind) + if (property.BoundAttribute.IsChildContentParameterNameProperty()) { // Check for each child content with a parameter name, that the parameter name is specified // with literal text. For instance, the following is not allowed and should generate a diagnostic. @@ -347,6 +354,25 @@ namespace Microsoft.AspNetCore.Blazor.Razor return; } + // Another special case here -- this might be a 'Context' parameter, which specifies the name + // for lambda parameter for parameterized child content + if (node.BoundAttribute.IsChildContentParameterNameProperty()) + { + // Check for each child content with a parameter name, that the parameter name is specified + // with literal text. For instance, the following is not allowed and should generate a diagnostic. + // + // ... + if (TryGetAttributeStringContent(node, out var parameterName)) + { + _component.ChildContentParameterName = parameterName; + return; + } + + // The parameter name is invalid. + _component.Diagnostics.Add(BlazorDiagnosticFactory.Create_ChildContentHasInvalidParameterOnComponent(node.Source, node.AttributeName, _component.TagName)); + return; + } + _children.Add(new ComponentAttributeExtensionNode(node)); } diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentTagHelperDescriptorProvider.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentTagHelperDescriptorProvider.cs index 0ed693bb08..3d79bcd023 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentTagHelperDescriptorProvider.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentTagHelperDescriptorProvider.cs @@ -129,8 +129,16 @@ namespace Microsoft.AspNetCore.Blazor.Razor CreateProperty(builder, property.property, property.kind); } - var descriptor = builder.Build(); + if (builder.BoundAttributes.Any(a => a.IsParameterizedChildContentProperty()) && + !builder.BoundAttributes.Any(a => string.Equals(a.Name, BlazorMetadata.ChildContent.ParameterAttributeName, StringComparison.OrdinalIgnoreCase))) + { + // If we have any parameterized child content parameters, synthesize a 'Context' parameter to be + // able to set the variable name (for all child content). If the developer defined a 'Context' parameter + // already, then theirs wins. + CreateContextParameter(builder, childContentName: null); + } + var descriptor = builder.Build(); return descriptor; } @@ -257,12 +265,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor { // For child content attributes with a parameter, synthesize an attribute that allows you to name // the parameter. - builder.BindAttribute(b => - { - b.Name = "Context"; - b.TypeName = typeof(string).FullName; - b.Documentation = string.Format(Resources.ChildContentParameterName_Documentation, attribute.Name); - }); + CreateContextParameter(builder, attribute.Name); } var descriptor = builder.Build(); @@ -270,6 +273,25 @@ namespace Microsoft.AspNetCore.Blazor.Razor return descriptor; } + private void CreateContextParameter(TagHelperDescriptorBuilder builder, string childContentName) + { + builder.BindAttribute(b => + { + b.Name = BlazorMetadata.ChildContent.ParameterAttributeName; + b.TypeName = typeof(string).FullName; + b.Metadata.Add(BlazorMetadata.Component.ChildContentParameterNameKey, bool.TrueString); + + if (childContentName == null) + { + b.Documentation = Resources.ChildContentParameterName_TopLevelDocumentation; + } + else + { + b.Documentation = string.Format(Resources.ChildContentParameterName_Documentation, childContentName); + } + }); + } + // Does a walk up the inheritance chain to determine the set of parameters by using // a dictionary keyed on property name. // diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Resources.Designer.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Resources.Designer.cs index 56fbd920e3..a4a4271f13 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Resources.Designer.cs @@ -106,7 +106,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor { } /// - /// Looks up a localized string similar to Specifies the parameter name for the '{0}' lambda expression.. + /// Looks up a localized string similar to Specifies the parameter name for the '{0}' child content expression.. /// internal static string ChildContentParameterName_Documentation { get { @@ -114,6 +114,15 @@ namespace Microsoft.AspNetCore.Blazor.Razor { } } + /// + /// Looks up a localized string similar to Specifies the parameter name for all child content expressions.. + /// + internal static string ChildContentParameterName_TopLevelDocumentation { + get { + return ResourceManager.GetString("ChildContentParameterName_TopLevelDocumentation", resourceCulture); + } + } + /// /// Looks up a localized string similar to Specifies the type of the type parameter {0} for the {1} component.. /// diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Resources.resx b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Resources.resx index 3cec7f2d43..3fe9868ecd 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Resources.resx +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Resources.resx @@ -133,7 +133,10 @@ Specifies a format to convert the value specified by the corresponding bind attribute. For example: <code>format-value="..."</code> will apply a format string to the value specified in <code>bind-value-...</code>. The format string can currently only be used with expressions of type <code>DateTime</code>. - Specifies the parameter name for the '{0}' lambda expression. + Specifies the parameter name for the '{0}' child content expression. + + + Specifies the parameter name for all child content expressions. Specifies the type of the type parameter {0} for the {1} component. diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TagHelperBoundAttributeDescriptorExtensions.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TagHelperBoundAttributeDescriptorExtensions.cs index 03242a37fa..10996b2800 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TagHelperBoundAttributeDescriptorExtensions.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TagHelperBoundAttributeDescriptorExtensions.cs @@ -78,6 +78,25 @@ namespace Microsoft.AspNetCore.Blazor.Razor string.Equals(value, bool.TrueString); } + /// + /// Gets a value that indicates whether the property is a child content property. Properties are + /// considered child content if they have the type RenderFragment or RenderFragment{T}. + /// + /// The . + /// Returns true if the property is child content, otherwise false. + public static bool IsChildContentProperty(this BoundAttributeDescriptorBuilder attribute) + { + if (attribute == null) + { + throw new ArgumentNullException(nameof(attribute)); + } + + var key = BlazorMetadata.Component.ChildContentKey; + return + attribute.Metadata.TryGetValue(key, out var value) && + string.Equals(value, bool.TrueString); + } + /// /// Gets a value that indicates whether the property is a parameterized child content property. Properties are /// considered parameterized child content if they have the type RenderFragment{T} (for some T). @@ -94,5 +113,44 @@ namespace Microsoft.AspNetCore.Blazor.Razor return attribute.IsChildContentProperty() && !string.Equals(attribute.TypeName, BlazorApi.RenderFragment.FullTypeName, StringComparison.Ordinal); } + + /// + /// Gets a value that indicates whether the property is a parameterized child content property. Properties are + /// considered parameterized child content if they have the type RenderFragment{T} (for some T). + /// + /// The . + /// Returns true if the property is parameterized child content, otherwise false. + public static bool IsParameterizedChildContentProperty(this BoundAttributeDescriptorBuilder attribute) + { + if (attribute == null) + { + throw new ArgumentNullException(nameof(attribute)); + } + + return attribute.IsChildContentProperty() && + !string.Equals(attribute.TypeName, BlazorApi.RenderFragment.FullTypeName, StringComparison.Ordinal); + } + + /// + /// Gets a value that indicates whether the property is used to specify the name of the parameter + /// for a parameterized child content property. + /// + /// The . + /// + /// Returns true if the property specifies the name of a parameter for a parameterized child content, + /// otherwise false. + /// + public static bool IsChildContentParameterNameProperty(this BoundAttributeDescriptor attribute) + { + if (attribute == null) + { + throw new ArgumentNullException(nameof(attribute)); + } + + var key = BlazorMetadata.Component.ChildContentParameterNameKey; + return + attribute.Metadata.TryGetValue(key, out var value) && + string.Equals(value, bool.TrueString); + } } } diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/ChildContentRazorIntegrationTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/ChildContentRazorIntegrationTest.cs index fc2a4e1627..ca48bf2fa5 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/ChildContentRazorIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/ChildContentRazorIntegrationTest.cs @@ -344,6 +344,75 @@ namespace Test frame => AssertFrame.Text(frame, "Bye!", 11)); } + [Fact] + public void Render_MultipleChildContent_ContextParameterOnComponent() + { + // Arrange + AdditionalSyntaxTrees.Add(RenderMultipleChildContent); + + var component = CompileToComponent(@" +@addTagHelper *, TestAssembly + +
@item.ToLowerInvariant()
+ Some @Context.ToLowerInvariant() Content +
Bye!
+
"); + + // Act + var frames = GetRenderTree(component); + + // Assert + Assert.Collection( + frames, + frame => AssertFrame.Component(frame, "Test.RenderMultipleChildContent", 6, 0), + frame => AssertFrame.Attribute(frame, "Name", "billg", 1), + frame => AssertFrame.Attribute(frame, "Value", "HI", 2), + frame => AssertFrame.Attribute(frame, "Header", typeof(RenderFragment), 3), + frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, typeof(RenderFragment), 6), + frame => AssertFrame.Attribute(frame, "Footer", typeof(RenderFragment), 10), + frame => AssertFrame.Element(frame, "div", 2, 4), + frame => AssertFrame.Text(frame, "billg", 5), + frame => AssertFrame.Text(frame, "Some ", 7), + frame => AssertFrame.Text(frame, "hi", 8), + frame => AssertFrame.Text(frame, " Content", 9), + frame => AssertFrame.Text(frame, "Bye!", 11)); + } + + // Verifies that our check for reuse of parameter names isn't too aggressive. + [Fact] + public void Render_MultipleChildContent_ContextParameterOnComponent_SetsSameName() + { + // Arrange + AdditionalSyntaxTrees.Add(RenderMultipleChildContent); + + var component = CompileToComponent(@" +@addTagHelper *, TestAssembly + +
@item.ToLowerInvariant()
+ Some @item.ToLowerInvariant() Content +
Bye!
+
"); + + // Act + var frames = GetRenderTree(component); + + // Assert + Assert.Collection( + frames, + frame => AssertFrame.Component(frame, "Test.RenderMultipleChildContent", 6, 0), + frame => AssertFrame.Attribute(frame, "Name", "billg", 1), + frame => AssertFrame.Attribute(frame, "Value", "HI", 2), + frame => AssertFrame.Attribute(frame, "Header", typeof(RenderFragment), 3), + frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, typeof(RenderFragment), 6), + frame => AssertFrame.Attribute(frame, "Footer", typeof(RenderFragment), 10), + frame => AssertFrame.Element(frame, "div", 2, 4), + frame => AssertFrame.Text(frame, "billg", 5), + frame => AssertFrame.Text(frame, "Some ", 7), + frame => AssertFrame.Text(frame, "hi", 8), + frame => AssertFrame.Text(frame, " Content", 9), + frame => AssertFrame.Text(frame, "Bye!", 11)); + } + [Fact] public void Render_ChildContent_AttributeAndBody_ProducesDiagnostic() { @@ -492,5 +561,25 @@ Some Content "element 'ChildContent' of component 'RenderChildContentString'. Specify the parameter name like: ' to resolve the ambiguity", diagnostic.GetMessage()); } + + [Fact] + public void Render_ChildContent_ContextParameterNameOnComponent_Invalid_ProducesDiagnostic() + { + // Arrange + AdditionalSyntaxTrees.Add(RenderChildContentStringComponent); + + // Act + var generated = CompileToCSharp(@" +@addTagHelper *, TestAssembly + +"); + + // Assert + var diagnostic = Assert.Single(generated.Diagnostics); + Assert.Same(BlazorDiagnosticFactory.ChildContentHasInvalidParameterOnComponent.Id, diagnostic.Id); + Assert.Equal( + "Invalid parameter name. The parameter name attribute 'Context' on component 'RenderChildContentString' can only include literal text.", + diagnostic.GetMessage()); + } } } diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/CodeGenerationTestBase.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/CodeGenerationTestBase.cs index 66471b5370..00ccc5e871 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/CodeGenerationTestBase.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/CodeGenerationTestBase.cs @@ -671,6 +671,42 @@ namespace Test CompileToAssembly(generated); } + [Fact] + public void ChildComponent_WithGenericChildContent_SetsParameterNameOnComponent() + { + // Arrange + AdditionalSyntaxTrees.Add(Parse(@" +using Microsoft.AspNetCore.Blazor; +using Microsoft.AspNetCore.Blazor.Components; + +namespace Test +{ + public class MyComponent : BlazorComponent + { + [Parameter] + string MyAttr { get; set; } + + [Parameter] + RenderFragment ChildContent { get; set; } + } +} +")); + + // Act + var generated = CompileToCSharp(@" +@addTagHelper *, TestAssembly + + + Some text@item.ToLowerInvariant() + +"); + + // Assert + AssertDocumentNodeMatchesBaseline(generated.CodeDocument); + AssertCSharpDocumentMatchesBaseline(generated.CodeDocument); + CompileToAssembly(generated); + } + [Fact] public void ChildComponent_WithElementOnlyChildContent() { diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/DesignTimeCodeGenerationTest/ChildComponent_WithGenericChildContent_SetsParameterNameOnComponent/TestComponent.codegen.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/DesignTimeCodeGenerationTest/ChildComponent_WithGenericChildContent_SetsParameterNameOnComponent/TestComponent.codegen.cs new file mode 100644 index 0000000000..6e6d1eeb9f --- /dev/null +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/DesignTimeCodeGenerationTest/ChildComponent_WithGenericChildContent_SetsParameterNameOnComponent/TestComponent.codegen.cs @@ -0,0 +1,42 @@ +// +#pragma warning disable 1591 +namespace Test +{ + #line hidden + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Blazor; + using Microsoft.AspNetCore.Blazor.Components; + public class TestComponent : Microsoft.AspNetCore.Blazor.Components.BlazorComponent + { + #pragma warning disable 219 + private void __RazorDirectiveTokenHelpers__() { + ((System.Action)(() => { +global::System.Object __typeHelper = "*, TestAssembly"; + } + ))(); + } + #pragma warning restore 219 + #pragma warning disable 0414 + private static System.Object __o = null; + #pragma warning restore 0414 + #pragma warning disable 1998 + protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder) + { + base.BuildRenderTree(builder); + __o = ""; + builder.AddAttribute(-1, "ChildContent", (Microsoft.AspNetCore.Blazor.RenderFragment)((item) => (builder2) => { +#line 4 "x:\dir\subdir\Test\TestComponent.cshtml" + __o = item.ToLowerInvariant(); + +#line default +#line hidden + } + )); + } + #pragma warning restore 1998 + } +} +#pragma warning restore 1591 diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/DesignTimeCodeGenerationTest/ChildComponent_WithGenericChildContent_SetsParameterNameOnComponent/TestComponent.ir.txt b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/DesignTimeCodeGenerationTest/ChildComponent_WithGenericChildContent_SetsParameterNameOnComponent/TestComponent.ir.txt new file mode 100644 index 0000000000..a89ac46d32 --- /dev/null +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/DesignTimeCodeGenerationTest/ChildComponent_WithGenericChildContent_SetsParameterNameOnComponent/TestComponent.ir.txt @@ -0,0 +1,39 @@ +Document - + NamespaceDeclaration - - Test + UsingDirective - (3:1,1 [12] ) - System + UsingDirective - (18:2,1 [32] ) - System.Collections.Generic + UsingDirective - (53:3,1 [17] ) - System.Linq + UsingDirective - (73:4,1 [28] ) - System.Threading.Tasks + UsingDirective - (104:5,1 [33] ) - Microsoft.AspNetCore.Blazor + UsingDirective - (140:6,1 [44] ) - Microsoft.AspNetCore.Blazor.Components + ClassDeclaration - - public - TestComponent - Microsoft.AspNetCore.Blazor.Components.BlazorComponent - + DesignTimeDirective - + DirectiveToken - (14:0,14 [32] ) - "*, Microsoft.AspNetCore.Blazor" + DirectiveToken - (14:0,14 [9] ) - "*, Test" + DirectiveToken - (14:0,14 [15] x:\dir\subdir\Test\TestComponent.cshtml) - *, TestAssembly + CSharpCode - + IntermediateToken - - CSharp - #pragma warning disable 0414 + CSharpCode - + IntermediateToken - - CSharp - private static System.Object __o = null; + CSharpCode - + IntermediateToken - - CSharp - #pragma warning restore 0414 + MethodDeclaration - - protected override - void - BuildRenderTree + CSharpCode - + IntermediateToken - - CSharp - base.BuildRenderTree(builder); + HtmlContent - (29:0,29 [2] x:\dir\subdir\Test\TestComponent.cshtml) + IntermediateToken - (29:0,29 [2] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n + ComponentExtensionNode - (31:1,0 [164] x:\dir\subdir\Test\TestComponent.cshtml) - MyComponent - Test.MyComponent + ComponentChildContent - (76:2,2 [103] x:\dir\subdir\Test\TestComponent.cshtml) - ChildContent + HtmlContent - (90:2,16 [15] x:\dir\subdir\Test\TestComponent.cshtml) + IntermediateToken - (90:2,16 [15] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n Some text + HtmlElement - (105:3,13 [55] x:\dir\subdir\Test\TestComponent.cshtml) - some-child + HtmlAttribute - - - + HtmlAttributeValue - - + IntermediateToken - - Html - 1 + CSharpExpression - (124:3,32 [23] x:\dir\subdir\Test\TestComponent.cshtml) + IntermediateToken - (124:3,32 [23] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - item.ToLowerInvariant() + HtmlContent - (160:3,68 [4] x:\dir\subdir\Test\TestComponent.cshtml) + IntermediateToken - (160:3,68 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n + ComponentAttributeExtensionNode - (52:1,21 [3] x:\dir\subdir\Test\TestComponent.cshtml) - MyAttr - MyAttr + HtmlContent - (52:1,21 [3] x:\dir\subdir\Test\TestComponent.cshtml) + IntermediateToken - (52:1,21 [3] x:\dir\subdir\Test\TestComponent.cshtml) - Html - abc diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/DesignTimeCodeGenerationTest/ChildComponent_WithGenericChildContent_SetsParameterNameOnComponent/TestComponent.mappings.txt b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/DesignTimeCodeGenerationTest/ChildComponent_WithGenericChildContent_SetsParameterNameOnComponent/TestComponent.mappings.txt new file mode 100644 index 0000000000..611fa5c8c0 --- /dev/null +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/DesignTimeCodeGenerationTest/ChildComponent_WithGenericChildContent_SetsParameterNameOnComponent/TestComponent.mappings.txt @@ -0,0 +1,10 @@ +Source Location: (14:0,14 [15] x:\dir\subdir\Test\TestComponent.cshtml) +|*, TestAssembly| +Generated Location: (559:16,38 [15] ) +|*, TestAssembly| + +Source Location: (124:3,32 [23] x:\dir\subdir\Test\TestComponent.cshtml) +|item.ToLowerInvariant()| +Generated Location: (1232:31,32 [23] ) +|item.ToLowerInvariant()| + diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/ChildComponent_WithGenericChildContent_SetsParameterNameOnComponent/TestComponent.codegen.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/ChildComponent_WithGenericChildContent_SetsParameterNameOnComponent/TestComponent.codegen.cs new file mode 100644 index 0000000000..476e4d4103 --- /dev/null +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/ChildComponent_WithGenericChildContent_SetsParameterNameOnComponent/TestComponent.codegen.cs @@ -0,0 +1,34 @@ +// +#pragma warning disable 1591 +namespace Test +{ + #line hidden + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Blazor; + using Microsoft.AspNetCore.Blazor.Components; + public class TestComponent : Microsoft.AspNetCore.Blazor.Components.BlazorComponent + { + #pragma warning disable 1998 + protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder) + { + base.BuildRenderTree(builder); + builder.OpenComponent(0); + builder.AddAttribute(1, "MyAttr", "abc"); + builder.AddAttribute(2, "ChildContent", (Microsoft.AspNetCore.Blazor.RenderFragment)((item) => (builder2) => { + builder2.AddContent(3, "\n Some text"); + builder2.OpenElement(4, "some-child"); + builder2.AddAttribute(5, "a", "1"); + builder2.AddContent(6, item.ToLowerInvariant()); + builder2.CloseElement(); + builder2.AddContent(7, "\n "); + } + )); + builder.CloseComponent(); + } + #pragma warning restore 1998 + } +} +#pragma warning restore 1591 diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/ChildComponent_WithGenericChildContent_SetsParameterNameOnComponent/TestComponent.ir.txt b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/ChildComponent_WithGenericChildContent_SetsParameterNameOnComponent/TestComponent.ir.txt new file mode 100644 index 0000000000..4e5e1bd44d --- /dev/null +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/ChildComponent_WithGenericChildContent_SetsParameterNameOnComponent/TestComponent.ir.txt @@ -0,0 +1,27 @@ +Document - + NamespaceDeclaration - - Test + UsingDirective - (3:1,1 [14] ) - System + UsingDirective - (18:2,1 [34] ) - System.Collections.Generic + UsingDirective - (53:3,1 [19] ) - System.Linq + UsingDirective - (73:4,1 [30] ) - System.Threading.Tasks + UsingDirective - (104:5,1 [35] ) - Microsoft.AspNetCore.Blazor + UsingDirective - (140:6,1 [46] ) - Microsoft.AspNetCore.Blazor.Components + ClassDeclaration - - public - TestComponent - Microsoft.AspNetCore.Blazor.Components.BlazorComponent - + MethodDeclaration - - protected override - void - BuildRenderTree + CSharpCode - + IntermediateToken - - CSharp - base.BuildRenderTree(builder); + ComponentExtensionNode - (31:1,0 [164] x:\dir\subdir\Test\TestComponent.cshtml) - MyComponent - Test.MyComponent + ComponentChildContent - (76:2,2 [103] x:\dir\subdir\Test\TestComponent.cshtml) - ChildContent + HtmlContent - (90:2,16 [15] x:\dir\subdir\Test\TestComponent.cshtml) + IntermediateToken - (90:2,16 [15] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n Some text + HtmlElement - (105:3,13 [55] x:\dir\subdir\Test\TestComponent.cshtml) - some-child + HtmlAttribute - - - + HtmlAttributeValue - - + IntermediateToken - - Html - 1 + CSharpExpression - (124:3,32 [23] x:\dir\subdir\Test\TestComponent.cshtml) + IntermediateToken - (124:3,32 [23] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - item.ToLowerInvariant() + HtmlContent - (160:3,68 [4] x:\dir\subdir\Test\TestComponent.cshtml) + IntermediateToken - (160:3,68 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n + ComponentAttributeExtensionNode - (52:1,21 [3] x:\dir\subdir\Test\TestComponent.cshtml) - MyAttr - MyAttr + HtmlContent - (52:1,21 [3] x:\dir\subdir\Test\TestComponent.cshtml) + IntermediateToken - (52:1,21 [3] x:\dir\subdir\Test\TestComponent.cshtml) - Html - abc diff --git a/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/ComponentTagHelperDescriptorProviderTest.cs b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/ComponentTagHelperDescriptorProviderTest.cs index 5417dc9cde..ad643ef002 100644 --- a/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/ComponentTagHelperDescriptorProviderTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/ComponentTagHelperDescriptorProviderTest.cs @@ -672,18 +672,27 @@ namespace Test Assert.Equal("TestAssembly", component.AssemblyName); Assert.Equal("Test.MyComponent", component.Name); - var attribute = Assert.Single(component.BoundAttributes); - Assert.Equal("ChildContent2", attribute.Name); - Assert.Equal("Microsoft.AspNetCore.Blazor.RenderFragment", attribute.TypeName); + Assert.Collection( + component.BoundAttributes, + a => + { + Assert.Equal("ChildContent2", a.Name); + Assert.Equal("Microsoft.AspNetCore.Blazor.RenderFragment", a.TypeName); - Assert.False(attribute.HasIndexer); - Assert.False(attribute.IsBooleanProperty); - Assert.False(attribute.IsEnum); - Assert.False(attribute.IsStringProperty); - Assert.False(attribute.IsDelegateProperty()); // We treat RenderFragment as separate from generalized delegates - Assert.True(attribute.IsChildContentProperty()); - Assert.True(attribute.IsParameterizedChildContentProperty()); - Assert.False(attribute.IsGenericTypedProperty()); + Assert.False(a.HasIndexer); + Assert.False(a.IsBooleanProperty); + Assert.False(a.IsEnum); + Assert.False(a.IsStringProperty); + Assert.False(a.IsDelegateProperty()); // We treat RenderFragment as separate from generalized delegates + Assert.True(a.IsChildContentProperty()); + Assert.True(a.IsParameterizedChildContentProperty()); + Assert.False(a.IsGenericTypedProperty()); + }, + a => + { + Assert.Equal(BlazorMetadata.ChildContent.ParameterAttributeName, a.Name); + Assert.True(a.IsChildContentParameterNameProperty()); + }); var childContent = Assert.Single(components, c => c.IsChildContentTagHelper()); @@ -692,9 +701,85 @@ namespace Test // A RenderFragment tag helper has a parameter to allow you to set the lambda parameter name. var contextAttribute = Assert.Single(childContent.BoundAttributes); - Assert.Equal("Context", contextAttribute.Name); + Assert.Equal(BlazorMetadata.ChildContent.ParameterAttributeName, contextAttribute.Name); Assert.Equal("System.String", contextAttribute.TypeName); - Assert.Equal("Specifies the parameter name for the 'ChildContent2' lambda expression.", contextAttribute.Documentation); + Assert.Equal("Specifies the parameter name for the 'ChildContent2' child content expression.", contextAttribute.Documentation); + Assert.True(contextAttribute.IsChildContentParameterNameProperty()); + } + + [Fact] + public void Execute_RenderFragmentOfTProperty_ComponentDefinesContextParameter() + { + // Arrange + + var compilation = BaseCompilation.AddSyntaxTrees(Parse(@" +using Microsoft.AspNetCore.Blazor; +using Microsoft.AspNetCore.Blazor.Components; + +namespace Test +{ + public class MyComponent : BlazorComponent + { + [Parameter] + RenderFragment ChildContent2 { get; set; } + + [Parameter] + string Context { get; set; } + } +} + +")); + + Assert.Empty(compilation.GetDiagnostics()); + + var context = TagHelperDescriptorProviderContext.Create(); + context.SetCompilation(compilation); + + var provider = new ComponentTagHelperDescriptorProvider(); + + // Act + provider.Execute(context); + + // Assert + var components = ExcludeBuiltInComponents(context); + var component = Assert.Single(components, c => c.IsComponentTagHelper()); + + Assert.Equal("TestAssembly", component.AssemblyName); + Assert.Equal("Test.MyComponent", component.Name); + + Assert.Collection( + component.BoundAttributes, + a => + { + Assert.Equal("ChildContent2", a.Name); + Assert.Equal("Microsoft.AspNetCore.Blazor.RenderFragment", a.TypeName); + + Assert.False(a.HasIndexer); + Assert.False(a.IsBooleanProperty); + Assert.False(a.IsEnum); + Assert.False(a.IsStringProperty); + Assert.False(a.IsDelegateProperty()); // We treat RenderFragment as separate from generalized delegates + Assert.True(a.IsChildContentProperty()); + Assert.True(a.IsParameterizedChildContentProperty()); + Assert.False(a.IsGenericTypedProperty()); + }, + a => + { + Assert.Equal(BlazorMetadata.ChildContent.ParameterAttributeName, a.Name); + Assert.False(a.IsChildContentParameterNameProperty()); + }); + + var childContent = Assert.Single(components, c => c.IsChildContentTagHelper()); + + Assert.Equal("TestAssembly", childContent.AssemblyName); + Assert.Equal("Test.MyComponent.ChildContent2", childContent.Name); + + // A RenderFragment tag helper has a parameter to allow you to set the lambda parameter name. + var contextAttribute = Assert.Single(childContent.BoundAttributes); + Assert.Equal(BlazorMetadata.ChildContent.ParameterAttributeName, contextAttribute.Name); + Assert.Equal("System.String", contextAttribute.TypeName); + Assert.Equal("Specifies the parameter name for the 'ChildContent2' child content expression.", contextAttribute.Documentation); + Assert.True(contextAttribute.IsChildContentParameterNameProperty()); } [Fact] @@ -752,6 +837,11 @@ namespace Test }, a => + { + Assert.Equal(BlazorMetadata.ChildContent.ParameterAttributeName, a.Name); + Assert.True(a.IsChildContentParameterNameProperty()); + }, + a => { Assert.Equal("T", a.Name); Assert.Equal("T", a.GetPropertyName()); @@ -767,9 +857,10 @@ namespace Test // A RenderFragment tag helper has a parameter to allow you to set the lambda parameter name. var contextAttribute = Assert.Single(childContent.BoundAttributes); - Assert.Equal("Context", contextAttribute.Name); + Assert.Equal(BlazorMetadata.ChildContent.ParameterAttributeName, contextAttribute.Name); Assert.Equal("System.String", contextAttribute.TypeName); - Assert.Equal("Specifies the parameter name for the 'ChildContent2' lambda expression.", contextAttribute.Documentation); + Assert.Equal("Specifies the parameter name for the 'ChildContent2' child content expression.", contextAttribute.Documentation); + Assert.True(contextAttribute.IsChildContentParameterNameProperty()); } [Fact] @@ -828,6 +919,11 @@ namespace Test }, a => + { + Assert.Equal(BlazorMetadata.ChildContent.ParameterAttributeName, a.Name); + Assert.True(a.IsChildContentParameterNameProperty()); + }, + a => { Assert.Equal("T", a.Name); Assert.Equal("T", a.GetPropertyName()); @@ -843,9 +939,10 @@ namespace Test // A RenderFragment tag helper has a parameter to allow you to set the lambda parameter name. var contextAttribute = Assert.Single(childContent.BoundAttributes); - Assert.Equal("Context", contextAttribute.Name); + Assert.Equal(BlazorMetadata.ChildContent.ParameterAttributeName, contextAttribute.Name); Assert.Equal("System.String", contextAttribute.TypeName); - Assert.Equal("Specifies the parameter name for the 'ChildContent2' lambda expression.", contextAttribute.Documentation); + Assert.Equal("Specifies the parameter name for the 'ChildContent2' child content expression.", contextAttribute.Documentation); + Assert.True(contextAttribute.IsChildContentParameterNameProperty()); } [Fact] @@ -904,6 +1001,11 @@ namespace Test }, a => + { + Assert.Equal(BlazorMetadata.ChildContent.ParameterAttributeName, a.Name); + Assert.True(a.IsChildContentParameterNameProperty()); + }, + a => { Assert.Equal("T", a.Name); Assert.Equal("T", a.GetPropertyName()); @@ -919,9 +1021,10 @@ namespace Test // A RenderFragment tag helper has a parameter to allow you to set the lambda parameter name. var contextAttribute = Assert.Single(childContent.BoundAttributes); - Assert.Equal("Context", contextAttribute.Name); + Assert.Equal(BlazorMetadata.ChildContent.ParameterAttributeName, contextAttribute.Name); Assert.Equal("System.String", contextAttribute.TypeName); - Assert.Equal("Specifies the parameter name for the 'ChildContent2' lambda expression.", contextAttribute.Documentation); + Assert.Equal("Specifies the parameter name for the 'ChildContent2' child content expression.", contextAttribute.Documentation); + Assert.True(contextAttribute.IsChildContentParameterNameProperty()); } [Fact] @@ -984,6 +1087,11 @@ namespace Test }, a => + { + Assert.Equal(BlazorMetadata.ChildContent.ParameterAttributeName, a.Name); + Assert.True(a.IsChildContentParameterNameProperty()); + }, + a => { Assert.Equal("T", a.Name); Assert.Equal("T", a.GetPropertyName()); @@ -999,9 +1107,9 @@ namespace Test // A RenderFragment tag helper has a parameter to allow you to set the lambda parameter name. var contextAttribute = Assert.Single(childContent.BoundAttributes); - Assert.Equal("Context", contextAttribute.Name); + Assert.Equal(BlazorMetadata.ChildContent.ParameterAttributeName, contextAttribute.Name); Assert.Equal("System.String", contextAttribute.TypeName); - Assert.Equal("Specifies the parameter name for the 'ChildContent2' lambda expression.", contextAttribute.Documentation); + Assert.Equal("Specifies the parameter name for the 'ChildContent2' child content expression.", contextAttribute.Documentation); } [Fact] @@ -1056,6 +1164,11 @@ namespace Test Assert.True(a.IsChildContentProperty()); }, a => + { + Assert.Equal(BlazorMetadata.ChildContent.ParameterAttributeName, a.Name); + Assert.True(a.IsChildContentParameterNameProperty()); + }, + a => { Assert.Equal("Footer", a.Name); Assert.Equal("Microsoft.AspNetCore.Blazor.RenderFragment", a.TypeName); diff --git a/test/testapps/BasicTestApp/MultipleChildContent.cshtml b/test/testapps/BasicTestApp/MultipleChildContent.cshtml index d383c5adb2..cc0338c7d3 100644 --- a/test/testapps/BasicTestApp/MultipleChildContent.cshtml +++ b/test/testapps/BasicTestApp/MultipleChildContent.cshtml @@ -1,4 +1,4 @@ - +
Col1Col2Col3
@if (ShowFooter) @@ -7,7 +7,7 @@ }
- @context.Col1@context.Col2@context.Col3 + @row.Col1@row.Col2@row.Col3