diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs index ff10295a09..47187c2eac 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs @@ -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) diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDiagnosticFactory.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDiagnosticFactory.cs index 66af53b7c7..15306c769e 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDiagnosticFactory.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDiagnosticFactory.cs @@ -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().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().Select(t => t.Content)); + return RazorDiagnostic.Create(UnsupportedComplexContent, node.Source ?? SourceSpan.Undefined, attributeName, content); } private static SourceSpan? CalculateSourcePosition( diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs index d9765cbe10..e80dd2ff7e 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs @@ -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. diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentAttributeExtensionNode.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentAttributeExtensionNode.cs index 29fe41b10d..425d5becf5 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentAttributeExtensionNode.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentAttributeExtensionNode.cs @@ -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; } diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentLoweringPass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentLoweringPass.cs index ea7ff5cf3d..519c37a504 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentLoweringPass.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentLoweringPass.cs @@ -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; + } } } diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs index 4b9f75b409..f590d43691 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs @@ -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 +"); + + // 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() { diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/DesignTimeCodeGenerationRazorIntegrationTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/DesignTimeCodeGenerationRazorIntegrationTest.cs index 2cc8ad2075..53c4c8a86b 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/DesignTimeCodeGenerationRazorIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/DesignTimeCodeGenerationRazorIntegrationTest.cs @@ -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 +"); + + // Assert + CompileToAssembly(generated); + + Assert.Equal(@" +// +#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 ; diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/RuntimeCodeGenerationRazorIntegrationTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/RuntimeCodeGenerationRazorIntegrationTest.cs index 722f644b3f..3452057002 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/RuntimeCodeGenerationRazorIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/RuntimeCodeGenerationRazorIntegrationTest.cs @@ -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 +"); + + // Assert + CompileToAssembly(generated); + + Assert.Equal(@" +// +#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(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() {