diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..fa4a904c0
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Force LF for package-lock files - not all version of NPM detect line endings.
+package-lock.json text eol=lf
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs
index 47187c2ea..f003a1fc8 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs
@@ -425,30 +425,27 @@ public override void WriteComponentAttribute(CodeRenderingContext context, Compo
}
else if (node.BoundAttribute?.IsDelegateProperty() ?? false)
{
- // See the runtime version of this code for a thorough description of what we're doing here
+ // We always surround the expression with the delegate constructor. This makes type
+ // inference inside lambdas, and method group conversion do the right thing.
+ IntermediateToken token = null;
if ((cSharpNode = node.Children[0] as CSharpExpressionIntermediateNode) != null)
{
- // This is an escaped event handler
- context.CodeWriter.Write(DesignTimeVariable);
- context.CodeWriter.Write(" = ");
- context.CodeWriter.Write("new ");
- context.CodeWriter.Write(node.BoundAttribute.TypeName);
- context.CodeWriter.Write("(");
- context.CodeWriter.WriteLine();
- WriteCSharpToken(context, ((IntermediateToken)cSharpNode.Children[0]));
- context.CodeWriter.Write(");");
- context.CodeWriter.WriteLine();
+ token = cSharpNode.Children[0] as IntermediateToken;
}
else
+ {
+ token = node.Children[0] as IntermediateToken;
+ }
+
+ if (token != null)
{
context.CodeWriter.Write(DesignTimeVariable);
context.CodeWriter.Write(" = ");
context.CodeWriter.Write("new ");
context.CodeWriter.Write(node.BoundAttribute.TypeName);
context.CodeWriter.Write("(");
- context.CodeWriter.Write(node.BoundAttribute.GetDelegateSignature());
- context.CodeWriter.Write(" => ");
- WriteCSharpToken(context, ((IntermediateToken)node.Children[0]));
+ context.CodeWriter.WriteLine();
+ WriteCSharpToken(context, token);
context.CodeWriter.Write(");");
context.CodeWriter.WriteLine();
}
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs
index a324ce593..982d11121 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs
@@ -66,6 +66,7 @@ public static void Register(RazorProjectEngineBuilder builder)
builder.Features.Add(new ConfigureBlazorCodeGenerationOptions());
builder.Features.Add(new ComponentDocumentClassifierPass());
+ builder.Features.Add(new ComplexAttributeContentPass());
builder.Features.Add(new ComponentLoweringPass());
builder.Features.Add(new ComponentTagHelperDescriptorProvider());
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs
index b1de0647c..7722b3b46 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs
@@ -447,39 +447,24 @@ public override void WriteComponentAttribute(CodeRenderingContext context, Compo
}
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.
- //
- // Example:
- //
- // --> builder.AddAttribute(X, "OnClick", new UIEventHandler((e) => Foo()));
- //
- // The constructor is important because we want to put type inference into a state where
- // we know the delegate's type should be UIEventHandler. AddAttribute has an overload that
- // accepts object, so without the 'new UIEventHandler' things will get ugly.
- //
- // The escape for this behavior is to prefix the expression with @. This is similar to
- // how escaping works for ModelExpression in MVC.
- // Example:
- //
- // --> builder.AddAttribute(X, "OnClick", new UIEventHandler(Foo));
+ // We always surround the expression with the delegate constructor. This makes type
+ // inference inside lambdas, and method group conversion do the right thing.
+ IntermediateToken token = null;
if ((cSharpNode = node.Children[0] as CSharpExpressionIntermediateNode) != null)
{
- // This is an escaped event handler;
- context.CodeWriter.Write("new ");
- context.CodeWriter.Write(node.BoundAttribute.TypeName);
- context.CodeWriter.Write("(");
- context.CodeWriter.Write(((IntermediateToken)cSharpNode.Children[0]).Content);
- context.CodeWriter.Write(")");
+ token = cSharpNode.Children[0] as IntermediateToken;
}
else
+ {
+ token = node.Children[0] as IntermediateToken;
+ }
+
+ if (token != null)
{
context.CodeWriter.Write("new ");
context.CodeWriter.Write(node.BoundAttribute.TypeName);
context.CodeWriter.Write("(");
- context.CodeWriter.Write(node.BoundAttribute.GetDelegateSignature());
- context.CodeWriter.Write(" => ");
- context.CodeWriter.Write(((IntermediateToken)node.Children[0]).Content);
+ context.CodeWriter.Write(token.Content);
context.CodeWriter.Write(")");
}
}
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComplexAttributeContentPass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComplexAttributeContentPass.cs
new file mode 100644
index 000000000..763dddb85
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComplexAttributeContentPass.cs
@@ -0,0 +1,101 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNetCore.Razor.Language;
+using Microsoft.AspNetCore.Razor.Language.Intermediate;
+
+namespace Microsoft.AspNetCore.Blazor.Razor
+{
+ // We don't support 'complex' content for components (mixed C# and markup) right now.
+ // It's not clear yet if Blazor will have a good scenario to use these constructs.
+ //
+ // 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.
+ internal class ComplexAttributeContentPass : IntermediateNodePassBase, IRazorOptimizationPass
+ {
+ // Run before other Blazor passes
+ public override int Order => -1000;
+
+ protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
+ {
+ var nodes = documentNode.FindDescendantNodes();
+ for (var i = 0; i < nodes.Count; i++)
+ {
+ ProcessAttributes(nodes[i]);
+ }
+ }
+
+ private void ProcessAttributes(TagHelperIntermediateNode node)
+ {
+ for (var i = node.Children.Count - 1; i >= 0; i--)
+ {
+ if (node.Children[i] is TagHelperPropertyIntermediateNode propertyNode)
+ {
+ if (HasComplexChildContent(propertyNode))
+ {
+ node.Diagnostics.Add(BlazorDiagnosticFactory.Create_UnsupportedComplexContent(
+ propertyNode,
+ propertyNode.AttributeName));
+ node.Children.RemoveAt(i);
+ continue;
+ }
+
+ node.Children[i] = new ComponentAttributeExtensionNode(propertyNode);
+ }
+ else if (node.Children[i] is TagHelperHtmlAttributeIntermediateNode htmlNode)
+ {
+ if (HasComplexChildContent(htmlNode))
+ {
+ node.Diagnostics.Add(BlazorDiagnosticFactory.Create_UnsupportedComplexContent(
+ htmlNode,
+ htmlNode.AttributeName));
+ node.Children.RemoveAt(i);
+ continue;
+ }
+ }
+ }
+ }
+
+ 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.
+ //
+ // There's a special case here for explicit expressions. See https://github.com/aspnet/Razor/issues/2203
+ // handling this case as a tactical matter since it's important for lambdas.
+ if (cSharpNode.Children.Count == 3 &&
+ cSharpNode.Children[0] is IntermediateToken token0 &&
+ cSharpNode.Children[2] is IntermediateToken token2 &&
+ token0.Content == "(" &&
+ token2.Content == ")")
+ {
+ cSharpNode.Children.RemoveAt(2);
+ cSharpNode.Children.RemoveAt(0);
+
+ // We were able to simplify it, all good.
+ return false;
+ }
+
+ return true;
+ }
+ else if (node.Children.Count > 1)
+ {
+ // This is the common case for 'mixed' content
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentTagHelperDescriptorProvider.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentTagHelperDescriptorProvider.cs
index f04736247..076f8762c 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentTagHelperDescriptorProvider.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentTagHelperDescriptorProvider.cs
@@ -114,11 +114,7 @@ private TagHelperDescriptor CreateDescriptor(INamedTypeSymbol type)
if (property.kind == PropertyKind.Delegate)
{
- var propertyType = (INamedTypeSymbol)property.property.Type;
- var parameters = propertyType.DelegateInvokeMethod.Parameters;
-
- var signature = "(" + string.Join(", ", parameters.Select(p => p.Name)) + ")";
- pb.Metadata.Add(DelegateSignatureMetadata, signature);
+ pb.Metadata.Add(DelegateSignatureMetadata, bool.TrueString);
}
xml = property.property.GetDocumentationCommentXml();
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TagHelperBoundAttributeDescriptorExtensions.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TagHelperBoundAttributeDescriptorExtensions.cs
index e09cfb0f0..fe4dade43 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TagHelperBoundAttributeDescriptorExtensions.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TagHelperBoundAttributeDescriptorExtensions.cs
@@ -16,19 +16,9 @@ public static bool IsDelegateProperty(this BoundAttributeDescriptor attribute)
}
var key = ComponentTagHelperDescriptorProvider.DelegateSignatureMetadata;
- return attribute.Metadata.TryGetValue(key, out var value);
- }
-
- public static string GetDelegateSignature(this BoundAttributeDescriptor attribute)
- {
- if (attribute == null)
- {
- throw new ArgumentNullException(nameof(attribute));
- }
-
- var key = ComponentTagHelperDescriptorProvider.DelegateSignatureMetadata;
- attribute.Metadata.TryGetValue(key, out var value);
- return value;
+ return
+ attribute.Metadata.TryGetValue(key, out var value) &&
+ string.Equals(value, bool.TrueString);
}
}
}
diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs
index f590d4369..22d44a2b1 100644
--- a/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs
@@ -153,8 +153,15 @@ void IComponent.SetParameters(ParameterCollection parameters)
}
- [Fact]
- public void Render_ChildComponent_WithLambdaEventHandler()
+ [Theory]
+ [InlineData("e => Increment(e)")]
+ [InlineData("(e) => Increment(e)")]
+ [InlineData("@(e => Increment(e))")]
+ [InlineData("@(e => { Increment(e); })")]
+ [InlineData("Increment")]
+ [InlineData("@Increment")]
+ [InlineData("@(Increment)")]
+ public void Render_ChildComponent_WithEventHandler(string expression)
{
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
@@ -171,16 +178,17 @@ public class MyComponent : BlazorComponent
}
"));
- var component = CompileToComponent(@"
+ var component = CompileToComponent($@"
@addTagHelper *, TestAssembly
-
+@using Microsoft.AspNetCore.Blazor
+
-@functions {
+@functions {{
private int counter;
- private void Increment() {
+ private void Increment(UIEventArgs e) {{
counter++;
- }
-}");
+ }}
+}}");
// Act
var frames = GetRenderTree(component);
diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/DesignTimeCodeGenerationRazorIntegrationTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/DesignTimeCodeGenerationRazorIntegrationTest.cs
index 2a3f2cad6..1932146b6 100644
--- a/test/Microsoft.AspNetCore.Blazor.Build.Test/DesignTimeCodeGenerationRazorIntegrationTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/DesignTimeCodeGenerationRazorIntegrationTest.cs
@@ -268,7 +268,7 @@ public class MyComponent : BlazorComponent
// Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
-
+ { Increment(); })""/>
@functions {
private int counter;
@@ -308,9 +308,9 @@ protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.R
{
base.BuildRenderTree(builder);
- __o = new Microsoft.AspNetCore.Blazor.UIEventHandler((eventArgs) =>
+ __o = new Microsoft.AspNetCore.Blazor.UIEventHandler(
#line 2 ""x:\dir\subdir\Test\TestComponent.cshtml""
- Increment()
+ (e) => { Increment(); }
#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 51adae432..db989dac4 100644
--- a/test/Microsoft.AspNetCore.Blazor.Build.Test/RuntimeCodeGenerationRazorIntegrationTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/RuntimeCodeGenerationRazorIntegrationTest.cs
@@ -248,7 +248,7 @@ public class MyComponent : BlazorComponent
// Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
-
+ { Increment(); })""/>
@functions {
private int counter;
@@ -277,7 +277,7 @@ protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.R
{
base.BuildRenderTree(builder);
builder.OpenComponent(0);
- builder.AddAttribute(1, ""OnClick"", new Microsoft.AspNetCore.Blazor.UIEventHandler((eventArgs) => Increment()));
+ builder.AddAttribute(1, ""OnClick"", new Microsoft.AspNetCore.Blazor.UIEventHandler(e => { Increment(); }));
builder.CloseComponent();
builder.AddContent(2, ""\n\n"");
}