Adds support for 'Context' parameters on components (#1470)

Adds support for 'Context' parameters on components

This change allows you to set the parameter name of a parameterized child content by using the `Context` attribute on the component. The `Context` attribute will be defined (and shown by completion) when the component has one or more declared parameterized (`RenderFragment<>`) child content parameters.

This is nice for cases where you are using implicit child content:

```
<ol>
  <Repeater Items="People" Context="person">
    <li>@person.FirstName</li>
  </Repeater>
</ol>
```

 or, when you have multiple child content elements and want them all to have the same parameter name:

 ```
<MyComponent Items="People" Context="person">
    <ChildContent1><div>@person.FirstName</div></ChildContent1>
    <ChildContent2><div>@person.LastName</div></ChildContent2>
</Repeater>
```

The parameter name can be overridden by using the `Context` parameter on the child content element:

 ```
<MyComponent Items="People" Context="person">
    <ChildContent1 Context="item"><div>@item.FirstName</div></ChildContent1>
    <ChildContent2><div>@person.LastName</div></ChildContent2>
</Repeater>
```

If the component defines a `Context` parameter already then we won't synthesize one - your component's parameter will work exactly as it did before this feature.
This commit is contained in:
Ryan Nowak 2018-09-24 12:22:16 -07:00 committed by GitHub
parent 984fabb89f
commit 6f383a0f0f
18 changed files with 570 additions and 35 deletions

View File

@ -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);
}
}
}

View File

@ -33,12 +33,24 @@ namespace Microsoft.AspNetCore.Blazor.Razor
public static readonly string TagHelperKind = "Blazor.ChildContent";
public static readonly string ParameterNameBoundAttributeKind = "Blazor.ChildContentParameterName";
/// <summary>
/// The name of the synthesized attribute used to set a child content parameter.
/// </summary>
public static readonly string ParameterAttributeName = "Context";
/// <summary>
/// The default name of the child content parameter (unless set by a Context attribute).
/// </summary>
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";

View File

@ -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; }

View File

@ -23,6 +23,11 @@ namespace Microsoft.AspNetCore.Blazor.Razor
public TagHelperDescriptor Component { get; set; }
/// <summary>
/// Gets the child content parameter name (null if unset) that was applied at the component level.
/// </summary>
public string ChildContentParameterName { get; set; }
public IEnumerable<ComponentTypeArgumentExtensionNode> TypeArguments => Children.OfType<ComponentTypeArgumentExtensionNode>();
public string TagName { get; set; }

View File

@ -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.
//
// <MyComponent Context="@Foo()">...</MyComponent>
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));
}

View File

@ -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.
//

View File

@ -106,7 +106,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor {
}
/// <summary>
/// Looks up a localized string similar to Specifies the parameter name for the &apos;{0}&apos; lambda expression..
/// Looks up a localized string similar to Specifies the parameter name for the &apos;{0}&apos; child content expression..
/// </summary>
internal static string ChildContentParameterName_Documentation {
get {
@ -114,6 +114,15 @@ namespace Microsoft.AspNetCore.Blazor.Razor {
}
}
/// <summary>
/// Looks up a localized string similar to Specifies the parameter name for all child content expressions..
/// </summary>
internal static string ChildContentParameterName_TopLevelDocumentation {
get {
return ResourceManager.GetString("ChildContentParameterName_TopLevelDocumentation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Specifies the type of the type parameter {0} for the {1} component..
/// </summary>

View File

@ -133,7 +133,10 @@
<value>Specifies a format to convert the value specified by the corresponding bind attribute. For example: &lt;code&gt;format-value="..."&lt;/code&gt; will apply a format string to the value specified in &lt;code&gt;bind-value-...&lt;/code&gt;. The format string can currently only be used with expressions of type &lt;code&gt;DateTime&lt;/code&gt;.</value>
</data>
<data name="ChildContentParameterName_Documentation" xml:space="preserve">
<value>Specifies the parameter name for the '{0}' lambda expression.</value>
<value>Specifies the parameter name for the '{0}' child content expression.</value>
</data>
<data name="ChildContentParameterName_TopLevelDocumentation" xml:space="preserve">
<value>Specifies the parameter name for all child content expressions.</value>
</data>
<data name="ComponentTypeParameter_Documentation" xml:space="preserve">
<value>Specifies the type of the type parameter {0} for the {1} component.</value>

View File

@ -78,6 +78,25 @@ namespace Microsoft.AspNetCore.Blazor.Razor
string.Equals(value, bool.TrueString);
}
/// <summary>
/// Gets a value that indicates whether the property is a child content property. Properties are
/// considered child content if they have the type <c>RenderFragment</c> or <c>RenderFragment{T}</c>.
/// </summary>
/// <param name="attribute">The <see cref="BoundAttributeDescriptorBuilder"/>.</param>
/// <returns>Returns <c>true</c> if the property is child content, otherwise <c>false</c>.</returns>
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);
}
/// <summary>
/// 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 <c>RenderFragment{T}</c> (for some T).
@ -94,5 +113,44 @@ namespace Microsoft.AspNetCore.Blazor.Razor
return attribute.IsChildContentProperty() &&
!string.Equals(attribute.TypeName, BlazorApi.RenderFragment.FullTypeName, StringComparison.Ordinal);
}
/// <summary>
/// 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 <c>RenderFragment{T}</c> (for some T).
/// </summary>
/// <param name="attribute">The <see cref="BoundAttributeDescriptor"/>.</param>
/// <returns>Returns <c>true</c> if the property is parameterized child content, otherwise <c>false</c>.</returns>
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);
}
/// <summary>
/// Gets a value that indicates whether the property is used to specify the name of the parameter
/// for a parameterized child content property.
/// </summary>
/// <param name="attribute">The <see cref="BoundAttributeDescriptor"/>.</param>
/// <returns>
/// Returns <c>true</c> if the property specifies the name of a parameter for a parameterized child content,
/// otherwise <c>false</c>.
/// </returns>
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);
}
}
}

View File

@ -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
<RenderMultipleChildContent Name=""billg"" Value=""HI"" Context=""item"">
<Header><div>@item.ToLowerInvariant()</div></Header>
<ChildContent Context=""Context"">Some @Context.ToLowerInvariant() Content</ChildContent>
<Footer>Bye!</Footer>
</RenderMultipleChildContent>");
// 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<string>), 3),
frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, typeof(RenderFragment<string>), 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
<RenderMultipleChildContent Name=""billg"" Value=""HI"" Context=""item"">
<Header><div>@item.ToLowerInvariant()</div></Header>
<ChildContent Context=""item"">Some @item.ToLowerInvariant() Content</ChildContent>
<Footer>Bye!</Footer>
</RenderMultipleChildContent>");
// 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<string>), 3),
frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, typeof(RenderFragment<string>), 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: '<ChildContent Context=\"another_name\"> to resolve the ambiguity",
diagnostic.GetMessage());
}
[Fact]
public void Render_ChildContent_ContextParameterNameOnComponent_Invalid_ProducesDiagnostic()
{
// Arrange
AdditionalSyntaxTrees.Add(RenderChildContentStringComponent);
// Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
<RenderChildContentString Context=""@Foo()"">
</RenderChildContentString>");
// 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());
}
}
}

View File

@ -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<string> ChildContent { get; set; }
}
}
"));
// Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
<MyComponent MyAttr=""abc"" Context=""item"">
<ChildContent>
Some text<some-child a='1'>@item.ToLowerInvariant()</some-child>
</ChildContent>
</MyComponent>");
// Assert
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
CompileToAssembly(generated);
}
[Fact]
public void ChildComponent_WithElementOnlyChildContent()
{

View File

@ -0,0 +1,42 @@
// <auto-generated/>
#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<System.String>)((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

View File

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

View File

@ -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()|

View File

@ -0,0 +1,34 @@
// <auto-generated/>
#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<Test.MyComponent>(0);
builder.AddAttribute(1, "MyAttr", "abc");
builder.AddAttribute(2, "ChildContent", (Microsoft.AspNetCore.Blazor.RenderFragment<System.String>)((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

View File

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

View File

@ -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<System.String>", attribute.TypeName);
Assert.Collection(
component.BoundAttributes,
a =>
{
Assert.Equal("ChildContent2", a.Name);
Assert.Equal("Microsoft.AspNetCore.Blazor.RenderFragment<System.String>", 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<T> 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<string> 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<System.String>", 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<T> 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<T> 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<T> 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<T> 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<T> 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<System.String>", a.TypeName);

View File

@ -1,4 +1,4 @@
<TemplatedTable Items="@Items">
<TemplatedTable Items="@Items" Context="row">
<Header><tr><th>Col1</th><th>Col2</th><th>Col3</th></tr></Header>
<Footer>
@if (ShowFooter)
@ -7,7 +7,7 @@
}
</Footer>
<ItemTemplate>
<tr><td>@context.Col1</td><td>@context.Col2</td><td>@context.Col3</td></tr>
<tr><td>@row.Col1</td><td>@row.Col2</td><td>@row.Col3</td></tr>
</ItemTemplate>
</TemplatedTable>