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
+
+