Allow non-property attributes to be set

This removes a limitation that prevented callers from passing
attributes to a component that aren't backed by properties.

The majority of the complication here is required to deal with the more
sophisticated way that HTML attributes are represented in the Razor IR.
This commit is contained in:
Ryan Nowak 2018-03-08 22:12:40 -08:00 committed by Steve Sanderson
parent c05657c7f2
commit 0c162e8c5a
8 changed files with 269 additions and 69 deletions

View File

@ -413,7 +413,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
// This rougly follows the design of the runtime writer for simplicity.
if (node.AttributeStructure == AttributeStructure.Minimized)
{
// Do nothhing
// Do nothing
}
else if (
node.Children.Count != 1 ||
@ -423,7 +423,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
// We don't expect this to happen, we just want to know if it can.
throw new InvalidOperationException("Attribute nodes should either be minimized or a single content node.");
}
else if (node.BoundAttribute.IsDelegateProperty())
else if (node.BoundAttribute?.IsDelegateProperty() ?? false)
{
// See the runtime version of this code for a thorough description of what we're doing here
if ((cSharpNode = node.Children[0] as CSharpExpressionIntermediateNode) != null)

View File

@ -69,23 +69,10 @@ namespace Microsoft.AspNetCore.Blazor.Razor
() => "Component attributes do not support complex content (mixed C# and markup). Attribute: '{0}', text '{1}'",
RazorDiagnosticSeverity.Error);
public static RazorDiagnostic Create_UnsupportedComplexContent(
SourceSpan? source,
TagHelperPropertyIntermediateNode node,
IntermediateNodeCollection children)
public static RazorDiagnostic Create_UnsupportedComplexContent(IntermediateNode node, string attributeName)
{
var content = string.Join("", children.OfType<IntermediateToken>().Select(t => t.Content));
return RazorDiagnostic.Create(UnsupportedComplexContent, source ?? SourceSpan.Undefined, node.AttributeName, content);
}
public static readonly RazorDiagnosticDescriptor UnboundComponentAttribute = new RazorDiagnosticDescriptor(
"BL9987",
() => "The component '{0}' does not have an attribute named '{1}'.",
RazorDiagnosticSeverity.Error);
public static RazorDiagnostic Create_UnboundComponentAttribute(SourceSpan? source, string componentType, TagHelperHtmlAttributeIntermediateNode node)
{
return RazorDiagnostic.Create(UnboundComponentAttribute, source ?? SourceSpan.Undefined, componentType, node.AttributeName);
var content = string.Join("", node.FindDescendantNodes<IntermediateToken>().Select(t => t.Content));
return RazorDiagnostic.Create(UnsupportedComplexContent, node.Source ?? SourceSpan.Undefined, attributeName, content);
}
private static SourceSpan? CalculateSourcePosition(

View File

@ -470,7 +470,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
// We don't expect this to happen, we just want to know if it can.
throw new InvalidOperationException("Attribute nodes should either be minimized or a single content node.");
}
else if (node.BoundAttribute.IsDelegateProperty())
else if (node.BoundAttribute?.IsDelegateProperty() ?? false)
{
// This is a UIEventHandler property. We do some special code generation for this
// case so that it's easier to write for common cases.

View File

@ -14,6 +14,28 @@ namespace Microsoft.AspNetCore.Blazor.Razor
{
}
public ComponentAttributeExtensionNode(TagHelperHtmlAttributeIntermediateNode attributeNode)
{
if (attributeNode == null)
{
throw new ArgumentNullException(nameof(attributeNode));
}
AttributeName = attributeNode.AttributeName;
AttributeStructure = attributeNode.AttributeStructure;
Source = attributeNode.Source;
for (var i = 0; i < attributeNode.Children.Count; i++)
{
Children.Add(attributeNode.Children[i]);
}
for (var i = 0; i < attributeNode.Diagnostics.Count; i++)
{
Diagnostics.Add(attributeNode.Diagnostics[i]);
}
}
public ComponentAttributeExtensionNode(TagHelperPropertyIntermediateNode propertyNode)
{
if (propertyNode == null)
@ -24,7 +46,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
AttributeName = propertyNode.AttributeName;
AttributeStructure = propertyNode.AttributeStructure;
BoundAttribute = propertyNode.BoundAttribute;
IsIndexerNameMatch = propertyNode.IsIndexerNameMatch;
PropertyName = propertyNode.BoundAttribute.GetPropertyName();
Source = propertyNode.Source;
TagHelper = propertyNode.TagHelper;
@ -47,10 +69,6 @@ namespace Microsoft.AspNetCore.Blazor.Razor
public BoundAttributeDescriptor BoundAttribute { get; set; }
public string FieldName { get; set; }
public bool IsIndexerNameMatch { get; set; }
public string PropertyName { get; set; }
public TagHelperDescriptor TagHelper { get; set; }

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
@ -65,7 +66,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
node.Children.Add(new ComponentCloseExtensionNode());
// Now we need to rewrite any set property nodes to call the appropriate AddAttribute api.
// Now we need to rewrite any set property or HTML nodes to call the appropriate AddAttribute api.
for (var i = node.Children.Count - 1; i >= 0; i--)
{
if (node.Children[i] is TagHelperPropertyIntermediateNode propertyNode &&
@ -76,60 +77,99 @@ namespace Microsoft.AspNetCore.Blazor.Razor
//
// This is where a lot of the complexity in the Razor/TagHelpers model creeps in and we
// might be able to avoid it if these features aren't needed.
if (propertyNode.Children.Count == 1 &&
propertyNode.Children[0] is HtmlAttributeIntermediateNode htmlNode &&
htmlNode.Children.Count > 1)
{
// This case can be hit for a 'string' attribute
node.Diagnostics.Add(BlazorDiagnosticFactory.Create_UnsupportedComplexContent(
propertyNode.Source,
propertyNode,
htmlNode.Children));
node.Children.RemoveAt(i);
continue;
}
if (propertyNode.Children.Count == 1 &&
propertyNode.Children[0] is CSharpExpressionIntermediateNode cSharpNode &&
cSharpNode.Children.Count > 1)
{
// This case can be hit when the attribute has an explicit @ inside, which
// 'escapes' any special sugar we provide for codegen.
node.Diagnostics.Add(BlazorDiagnosticFactory.Create_UnsupportedComplexContent(
propertyNode.Source,
propertyNode,
cSharpNode.Children));
node.Children.RemoveAt(i);
continue;
}
else if (propertyNode.Children.Count > 1)
if (HasComplexChildContent(propertyNode))
{
node.Diagnostics.Add(BlazorDiagnosticFactory.Create_UnsupportedComplexContent(
propertyNode.Source,
propertyNode,
propertyNode.Children));
propertyNode.AttributeName));
node.Children.RemoveAt(i);
continue;
}
node.Children[i] = new ComponentAttributeExtensionNode(propertyNode)
{
PropertyName = propertyNode.BoundAttribute.GetPropertyName(),
};
node.Children[i] = new ComponentAttributeExtensionNode(propertyNode);
}
}
// Add an error and remove any nodes that don't map to a component property.
for (var i = node.Children.Count - 1; i >= 0; i--)
{
if (node.Children[i] is TagHelperHtmlAttributeIntermediateNode attributeNode)
else if (node.Children[i] is TagHelperHtmlAttributeIntermediateNode htmlNode)
{
node.Diagnostics.Add(BlazorDiagnosticFactory.Create_UnboundComponentAttribute(
attributeNode.Source,
tagHelper.GetTypeName(),
attributeNode));
node.Children.RemoveAt(i);
if (HasComplexChildContent(htmlNode))
{
node.Diagnostics.Add(BlazorDiagnosticFactory.Create_UnsupportedComplexContent(
htmlNode,
htmlNode.AttributeName));
node.Children.RemoveAt(i);
continue;
}
// For any nodes that don't map to a component property we won't have type information
// but these should follow the same path through the runtime.
var attributeNode = new ComponentAttributeExtensionNode(htmlNode);
node.Children[i] = attributeNode;
// Since we don't support complex content, we can rewrite the inside of this
// node to the rather simpler form that property nodes usually have.
for (var j = 0; j < attributeNode.Children.Count; j++)
{
if (attributeNode.Children[j] is HtmlAttributeValueIntermediateNode htmlValue)
{
attributeNode.Children[j] = new HtmlContentIntermediateNode()
{
Children =
{
htmlValue.Children.Single(),
},
Source = htmlValue.Source,
};
}
else if (attributeNode.Children[j] is CSharpExpressionAttributeValueIntermediateNode expressionValue)
{
attributeNode.Children[j] = new CSharpExpressionIntermediateNode()
{
Children =
{
expressionValue.Children.Single(),
},
Source = expressionValue.Source,
};
}
else if (attributeNode.Children[j] is CSharpCodeAttributeValueIntermediateNode codeValue)
{
attributeNode.Children[j] = new CSharpExpressionIntermediateNode()
{
Children =
{
codeValue.Children.Single(),
},
Source = codeValue.Source,
};
}
}
}
}
}
private static bool HasComplexChildContent(IntermediateNode node)
{
if (node.Children.Count == 1 &&
node.Children[0] is HtmlAttributeIntermediateNode htmlNode &&
htmlNode.Children.Count > 1)
{
// This case can be hit for a 'string' attribute
return true;
}
else if (node.Children.Count == 1 &&
node.Children[0] is CSharpExpressionIntermediateNode cSharpNode &&
cSharpNode.Children.Count > 1)
{
// This case can be hit when the attribute has an explicit @ inside, which
// 'escapes' any special sugar we provide for codegen.
return true;
}
else if (node.Children.Count > 1)
{
// This is the common case for 'mixed' content
return true;
}
return false;
}
}
}

View File

@ -70,7 +70,7 @@ namespace Test
IntProperty=""123""
BoolProperty=""true""
StringProperty=""My string""
ObjectProperty=""new SomeType()""/>");
ObjectProperty=""new SomeType()"" />");
// Act
var frames = GetRenderTree(component);
@ -119,6 +119,40 @@ namespace Test
frame => AssertFrame.Attribute(frame, "StringProperty", "42", 1));
}
[Fact]
public void Render_ChildComponent_WithNonPropertyAttributes()
{
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent, IComponent
{
void IComponent.SetParameters(ParameterCollection parameters)
{
}
}
}
"));
var component = CompileToComponent(@"
@addTagHelper *, TestAssembly
<MyComponent some-attribute=""foo"" another-attribute=""@(42.ToString())"" />");
// Act
var frames = GetRenderTree(component);
// Assert
Assert.Collection(
frames,
frame => AssertFrame.Component(frame, "Test.MyComponent", 3, 0),
frame => AssertFrame.Attribute(frame, "some-attribute", "foo", 1),
frame => AssertFrame.Attribute(frame, "another-attribute", "42", 2));
}
[Fact]
public void Render_ChildComponent_WithLambdaEventHandler()
{

View File

@ -164,6 +164,75 @@ global::System.Object __typeHelper = ""*, TestAssembly"";
#line 2 ""x:\dir\subdir\Test\TestComponent.cshtml""
42.ToString()
#line default
#line hidden
;
builder.AddAttribute(-1, ""ChildContent"", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => {
}
));
}
#pragma warning restore 1998
}
}
#pragma warning restore 1591
".Trim(), generated.Code.Trim(), ignoreLineEndingDifferences: true);
}
[Fact]
public void CodeGeneration_ChildComponent_WithNonPropertyAttributes()
{
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
}
}
"));
// Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
<MyComponent some-attribute=""foo"" another-attribute=""@(43.ToString())""/>");
// Assert
CompileToAssembly(generated);
Assert.Equal(@"
// <auto-generated/>
#pragma warning disable 1591
namespace Test
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
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 =
#line 2 ""x:\dir\subdir\Test\TestComponent.cshtml""
43.ToString()
#line default
#line hidden
;

View File

@ -175,6 +175,58 @@ namespace Test
".Trim(), generated.Code.Trim(), ignoreLineEndingDifferences: true);
}
[Fact]
public void CodeGeneration_ChildComponent_WithNonPropertyAttributes()
{
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
}
}
"));
// Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
<MyComponent some-attribute=""foo"" another-attribute=""@(43.ToString())""/>");
// Assert
CompileToAssembly(generated);
Assert.Equal(@"
// <auto-generated/>
#pragma warning disable 1591
namespace Test
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
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, ""some-attribute"", ""foo"");
builder.AddAttribute(2, ""another-attribute"", 43.ToString());
builder.CloseComponent();
}
#pragma warning restore 1998
}
}
#pragma warning restore 1591
".Trim(), generated.Code.Trim(), ignoreLineEndingDifferences: true);
}
[Fact]
public void CodeGeneration_ChildComponent_WithLambdaEventHandler()
{