From 82d850d3a7a81a446dff5bde09154d9a004c32aa Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 18 Feb 2019 09:37:41 +0000 Subject: [PATCH] Components that accept bind-Something can request SomethingExpression (dotnet/aspnetcore-tooling#213) * In binding to components, automatically supply FooExpression when requested * Fix tests * CR feedback \n\nCommit migrated from https://github.com/dotnet/aspnetcore-tooling/commit/ca9de74f4ead1fc544fb73b00212db9c677b8a9b --- .../src/Components/BlazorMetadata.cs | 2 + .../Components/ComponentBindLoweringPass.cs | 53 +++++++++++++++++-- .../TagHelperDescriptorExtensions.cs | 11 ++++ .../src/BindTagHelperDescriptorProvider.cs | 20 +++++++ .../BindTagHelperDescriptorProviderTest.cs | 5 ++ .../RazorIntegrationTestBase.cs | 1 + 6 files changed, 87 insertions(+), 5 deletions(-) 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, };