diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorMetadata.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorMetadata.cs
index 688d3a6135..1c15fd28fa 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorMetadata.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorMetadata.cs
@@ -24,6 +24,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
public readonly static string ValueAttribute = "Blazor.Bind.ValueAttribute";
public readonly static string ChangeAttribute = "Blazor.Bind.ChangeAttribute";
+
+ public readonly static string ExpressionAttribute = "Blazor.Bind.ExpressionAttribute";
}
public static class ChildContent
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentBindLoweringPass.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentBindLoweringPass.cs
index fc053b71d4..cd3c831ae6 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentBindLoweringPass.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentBindLoweringPass.cs
@@ -148,11 +148,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
{
// Bind works similarly to a macro, it always expands to code that the user could have written.
//
- // For the nodes that are related to the bind-attribute rewrite them to look like a pair of
+ // For the nodes that are related to the bind-attribute rewrite them to look like a set of
// 'normal' HTML attributes similar to the following transformation.
//
// Input:
- // Output:
+ // Output:
//
// This means that the expression that appears inside of 'bind' must be an LValue or else
// there will be errors. In general the errors that come from C# in this case are good enough
@@ -171,8 +171,10 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
node.AttributeName,
out var valueAttributeName,
out var changeAttributeName,
+ out var expressionAttributeName,
out var valueAttribute,
- out var changeAttribute))
+ out var changeAttribute,
+ out var expressionAttribute))
{
// Skip anything we can't understand. It's important that we don't crash, that will bring down
// the build.
@@ -340,7 +342,32 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
changeNode.Children[0].Children.Add(changeExpressionTokens[i]);
}
- return new[] { valueNode, changeNode };
+ // Finally, also emit a node for the "Expression" attribute, but only if the target
+ // component is defined to accept one
+ ComponentAttributeIntermediateNode expressionNode = null;
+ if (expressionAttribute != null)
+ {
+ expressionNode = new ComponentAttributeIntermediateNode(node)
+ {
+ AttributeName = expressionAttributeName,
+ BoundAttribute = expressionAttribute,
+ PropertyName = expressionAttribute.GetPropertyName(),
+ TagHelper = node.TagHelper,
+ TypeName = expressionAttribute.IsWeaklyTyped() ? null : expressionAttribute.TypeName,
+ };
+
+ expressionNode.Children.Clear();
+ expressionNode.Children.Add(new CSharpExpressionIntermediateNode());
+ expressionNode.Children[0].Children.Add(new IntermediateToken()
+ {
+ Content = $"() => {original.Content}",
+ Kind = TokenKind.CSharp
+ });
+ }
+
+ return expressionNode == null
+ ? new[] { valueNode, changeNode }
+ : new[] { valueNode, changeNode, expressionNode };
}
}
@@ -394,11 +421,15 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
string attributeName,
out string valueAttributeName,
out string changeAttributeName,
+ out string expressionAttributeName,
out BoundAttributeDescriptor valueAttribute,
- out BoundAttributeDescriptor changeAttribute)
+ out BoundAttributeDescriptor changeAttribute,
+ out BoundAttributeDescriptor expressionAttribute)
{
valueAttribute = null;
changeAttribute = null;
+ expressionAttribute = null;
+ expressionAttributeName = null;
// Even though some of our 'bind' tag helpers specify the attribute names, they
// should still satisfy one of the valid syntaxes.
@@ -415,6 +446,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
// We expect 1 bind tag helper per-node.
valueAttributeName = node.TagHelper.GetValueAttributeName() ?? valueAttributeName;
changeAttributeName = node.TagHelper.GetChangeAttributeName() ?? changeAttributeName;
+ expressionAttributeName = node.TagHelper.GetExpressionAttributeName() ?? expressionAttributeName;
// We expect 0-1 components per-node.
var componentTagHelper = (parent as ComponentIntermediateNode)?.Component;
@@ -437,6 +469,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
changeAttributeName = valueAttributeName + "Changed";
}
+ // Likewise for the expression attribute
+ if (expressionAttributeName == null)
+ {
+ expressionAttributeName = valueAttributeName + "Expression";
+ }
+
for (var i = 0; i < componentTagHelper.BoundAttributes.Count; i++)
{
var attribute = componentTagHelper.BoundAttributes[i];
@@ -450,6 +488,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
{
changeAttribute = attribute;
}
+
+ if (string.Equals(expressionAttributeName, attribute.Name))
+ {
+ expressionAttribute = attribute;
+ }
}
return true;
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/TagHelperDescriptorExtensions.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/TagHelperDescriptorExtensions.cs
index ddc2edcec1..76b7932ab8 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/TagHelperDescriptorExtensions.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/TagHelperDescriptorExtensions.cs
@@ -95,6 +95,17 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
return result;
}
+ public static string GetExpressionAttributeName(this TagHelperDescriptor tagHelper)
+ {
+ if (tagHelper == null)
+ {
+ throw new ArgumentNullException(nameof(tagHelper));
+ }
+
+ tagHelper.Metadata.TryGetValue(BlazorMetadata.Bind.ExpressionAttribute, out var result);
+ return result;
+ }
+
public static bool IsChildContentTagHelper(this TagHelperDescriptor tagHelper)
{
if (tagHelper == null)
diff --git a/src/Razor/Microsoft.CodeAnalysis.Razor/src/BindTagHelperDescriptorProvider.cs b/src/Razor/Microsoft.CodeAnalysis.Razor/src/BindTagHelperDescriptorProvider.cs
index 4310a9f24e..97fd3b0483 100644
--- a/src/Razor/Microsoft.CodeAnalysis.Razor/src/BindTagHelperDescriptorProvider.cs
+++ b/src/Razor/Microsoft.CodeAnalysis.Razor/src/BindTagHelperDescriptorProvider.cs
@@ -351,6 +351,8 @@ namespace Microsoft.CodeAnalysis.Razor
//
// The easiest way to figure this out without a lot of backtracking is to look for `FooChanged` and then
// try to find a matching "Foo".
+ //
+ // We also look for a corresponding FooExpression attribute, though its presence is optional.
for (var i = 0; i < tagHelper.BoundAttributes.Count; i++)
{
var changeAttribute = tagHelper.BoundAttributes[i];
@@ -360,12 +362,25 @@ namespace Microsoft.CodeAnalysis.Razor
}
BoundAttributeDescriptor valueAttribute = null;
+ BoundAttributeDescriptor expressionAttribute = null;
var valueAttributeName = changeAttribute.Name.Substring(0, changeAttribute.Name.Length - "Changed".Length);
+ var expressionAttributeName = valueAttributeName + "Expression";
for (var j = 0; j < tagHelper.BoundAttributes.Count; j++)
{
if (tagHelper.BoundAttributes[j].Name == valueAttributeName && !tagHelper.BoundAttributes[j].IsDelegateProperty())
{
valueAttribute = tagHelper.BoundAttributes[j];
+
+ }
+
+ if (tagHelper.BoundAttributes[j].Name == expressionAttributeName)
+ {
+ expressionAttribute = tagHelper.BoundAttributes[j];
+ }
+
+ if (valueAttribute != null && expressionAttribute != null)
+ {
+ // We found both, so we can stop looking now
break;
}
}
@@ -388,6 +403,11 @@ namespace Microsoft.CodeAnalysis.Razor
builder.Metadata[BlazorMetadata.Bind.ValueAttribute] = valueAttribute.Name;
builder.Metadata[BlazorMetadata.Bind.ChangeAttribute] = changeAttribute.Name;
+ if (expressionAttribute != null)
+ {
+ builder.Metadata[BlazorMetadata.Bind.ExpressionAttribute] = expressionAttribute.Name;
+ }
+
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
// a C# property will crash trying to create the toolips.
builder.SetTypeName(tagHelper.GetTypeName());
diff --git a/src/Razor/Microsoft.CodeAnalysis.Razor/test/BindTagHelperDescriptorProviderTest.cs b/src/Razor/Microsoft.CodeAnalysis.Razor/test/BindTagHelperDescriptorProviderTest.cs
index 9c28a2be24..6aa2b87c1d 100644
--- a/src/Razor/Microsoft.CodeAnalysis.Razor/test/BindTagHelperDescriptorProviderTest.cs
+++ b/src/Razor/Microsoft.CodeAnalysis.Razor/test/BindTagHelperDescriptorProviderTest.cs
@@ -16,6 +16,7 @@ namespace Microsoft.CodeAnalysis.Razor
// Arrange
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
using System;
+using System.Linq.Expressions;
using Microsoft.AspNetCore.Components;
namespace Test
@@ -31,6 +32,9 @@ namespace Test
[Parameter]
Action MyPropertyChanged { get; set; }
+
+ [Parameter]
+ Expression> MyPropertyExpression { get; set; }
}
}
"));
@@ -69,6 +73,7 @@ namespace Test
Assert.Equal("MyProperty", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]);
Assert.Equal("MyPropertyChanged", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]);
+ Assert.Equal("MyPropertyExpression", bind.Metadata[BlazorMetadata.Bind.ExpressionAttribute]);
Assert.Equal(
"Binds the provided expression to the 'MyProperty' property and a change event " +
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorIntegrationTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorIntegrationTestBase.cs
index aa716ece02..fe405bcce6 100644
--- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorIntegrationTestBase.cs
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorIntegrationTestBase.cs
@@ -36,6 +36,7 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
{
typeof(System.Runtime.AssemblyTargetedPatchBandAttribute).Assembly, // System.Runtime
typeof(Enumerable).Assembly, // Other .NET fundamental types
+ typeof(System.Linq.Expressions.Expression).Assembly,
typeof(ComponentBase).Assembly,
};