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 ca9de74f4e
This commit is contained in:
Steve Sanderson 2019-02-18 09:37:41 +00:00 committed by GitHub
parent 0ab2a1e586
commit 82d850d3a7
6 changed files with 87 additions and 5 deletions

View File

@ -24,6 +24,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
public readonly static string ValueAttribute = "Blazor.Bind.ValueAttribute"; public readonly static string ValueAttribute = "Blazor.Bind.ValueAttribute";
public readonly static string ChangeAttribute = "Blazor.Bind.ChangeAttribute"; public readonly static string ChangeAttribute = "Blazor.Bind.ChangeAttribute";
public readonly static string ExpressionAttribute = "Blazor.Bind.ExpressionAttribute";
} }
public static class ChildContent public static class ChildContent

View File

@ -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. // 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. // 'normal' HTML attributes similar to the following transformation.
// //
// Input: <MyComponent bind-Value="@currentCount" /> // Input: <MyComponent bind-Value="@currentCount" />
// Output: <MyComponent Value ="...<get the value>..." ValueChanged ="... <set the value>..." /> // Output: <MyComponent Value ="...<get the value>..." ValueChanged ="... <set the value>..." ValueExpression ="() => ...<get the value>..." />
// //
// This means that the expression that appears inside of 'bind' must be an LValue or else // 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 // 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, node.AttributeName,
out var valueAttributeName, out var valueAttributeName,
out var changeAttributeName, out var changeAttributeName,
out var expressionAttributeName,
out var valueAttribute, 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 // Skip anything we can't understand. It's important that we don't crash, that will bring down
// the build. // the build.
@ -340,7 +342,32 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
changeNode.Children[0].Children.Add(changeExpressionTokens[i]); 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, string attributeName,
out string valueAttributeName, out string valueAttributeName,
out string changeAttributeName, out string changeAttributeName,
out string expressionAttributeName,
out BoundAttributeDescriptor valueAttribute, out BoundAttributeDescriptor valueAttribute,
out BoundAttributeDescriptor changeAttribute) out BoundAttributeDescriptor changeAttribute,
out BoundAttributeDescriptor expressionAttribute)
{ {
valueAttribute = null; valueAttribute = null;
changeAttribute = null; changeAttribute = null;
expressionAttribute = null;
expressionAttributeName = null;
// Even though some of our 'bind' tag helpers specify the attribute names, they // Even though some of our 'bind' tag helpers specify the attribute names, they
// should still satisfy one of the valid syntaxes. // 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. // We expect 1 bind tag helper per-node.
valueAttributeName = node.TagHelper.GetValueAttributeName() ?? valueAttributeName; valueAttributeName = node.TagHelper.GetValueAttributeName() ?? valueAttributeName;
changeAttributeName = node.TagHelper.GetChangeAttributeName() ?? changeAttributeName; changeAttributeName = node.TagHelper.GetChangeAttributeName() ?? changeAttributeName;
expressionAttributeName = node.TagHelper.GetExpressionAttributeName() ?? expressionAttributeName;
// We expect 0-1 components per-node. // We expect 0-1 components per-node.
var componentTagHelper = (parent as ComponentIntermediateNode)?.Component; var componentTagHelper = (parent as ComponentIntermediateNode)?.Component;
@ -437,6 +469,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
changeAttributeName = valueAttributeName + "Changed"; changeAttributeName = valueAttributeName + "Changed";
} }
// Likewise for the expression attribute
if (expressionAttributeName == null)
{
expressionAttributeName = valueAttributeName + "Expression";
}
for (var i = 0; i < componentTagHelper.BoundAttributes.Count; i++) for (var i = 0; i < componentTagHelper.BoundAttributes.Count; i++)
{ {
var attribute = componentTagHelper.BoundAttributes[i]; var attribute = componentTagHelper.BoundAttributes[i];
@ -450,6 +488,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
{ {
changeAttribute = attribute; changeAttribute = attribute;
} }
if (string.Equals(expressionAttributeName, attribute.Name))
{
expressionAttribute = attribute;
}
} }
return true; return true;

View File

@ -95,6 +95,17 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
return result; 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) public static bool IsChildContentTagHelper(this TagHelperDescriptor tagHelper)
{ {
if (tagHelper == null) if (tagHelper == null)

View File

@ -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 // 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". // 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++) for (var i = 0; i < tagHelper.BoundAttributes.Count; i++)
{ {
var changeAttribute = tagHelper.BoundAttributes[i]; var changeAttribute = tagHelper.BoundAttributes[i];
@ -360,12 +362,25 @@ namespace Microsoft.CodeAnalysis.Razor
} }
BoundAttributeDescriptor valueAttribute = null; BoundAttributeDescriptor valueAttribute = null;
BoundAttributeDescriptor expressionAttribute = null;
var valueAttributeName = changeAttribute.Name.Substring(0, changeAttribute.Name.Length - "Changed".Length); var valueAttributeName = changeAttribute.Name.Substring(0, changeAttribute.Name.Length - "Changed".Length);
var expressionAttributeName = valueAttributeName + "Expression";
for (var j = 0; j < tagHelper.BoundAttributes.Count; j++) for (var j = 0; j < tagHelper.BoundAttributes.Count; j++)
{ {
if (tagHelper.BoundAttributes[j].Name == valueAttributeName && !tagHelper.BoundAttributes[j].IsDelegateProperty()) if (tagHelper.BoundAttributes[j].Name == valueAttributeName && !tagHelper.BoundAttributes[j].IsDelegateProperty())
{ {
valueAttribute = tagHelper.BoundAttributes[j]; 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; break;
} }
} }
@ -388,6 +403,11 @@ namespace Microsoft.CodeAnalysis.Razor
builder.Metadata[BlazorMetadata.Bind.ValueAttribute] = valueAttribute.Name; builder.Metadata[BlazorMetadata.Bind.ValueAttribute] = valueAttribute.Name;
builder.Metadata[BlazorMetadata.Bind.ChangeAttribute] = changeAttribute.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 // 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. // a C# property will crash trying to create the toolips.
builder.SetTypeName(tagHelper.GetTypeName()); builder.SetTypeName(tagHelper.GetTypeName());

View File

@ -16,6 +16,7 @@ namespace Microsoft.CodeAnalysis.Razor
// Arrange // Arrange
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@" var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
using System; using System;
using System.Linq.Expressions;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
namespace Test namespace Test
@ -31,6 +32,9 @@ namespace Test
[Parameter] [Parameter]
Action<string> MyPropertyChanged { get; set; } Action<string> MyPropertyChanged { get; set; }
[Parameter]
Expression<Func<string>> MyPropertyExpression { get; set; }
} }
} }
")); "));
@ -69,6 +73,7 @@ namespace Test
Assert.Equal("MyProperty", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]); Assert.Equal("MyProperty", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]);
Assert.Equal("MyPropertyChanged", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]); Assert.Equal("MyPropertyChanged", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]);
Assert.Equal("MyPropertyExpression", bind.Metadata[BlazorMetadata.Bind.ExpressionAttribute]);
Assert.Equal( Assert.Equal(
"Binds the provided expression to the 'MyProperty' property and a change event " + "Binds the provided expression to the 'MyProperty' property and a change event " +

View File

@ -36,6 +36,7 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
{ {
typeof(System.Runtime.AssemblyTargetedPatchBandAttribute).Assembly, // System.Runtime typeof(System.Runtime.AssemblyTargetedPatchBandAttribute).Assembly, // System.Runtime
typeof(Enumerable).Assembly, // Other .NET fundamental types typeof(Enumerable).Assembly, // Other .NET fundamental types
typeof(System.Linq.Expressions.Expression).Assembly,
typeof(ComponentBase).Assembly, typeof(ComponentBase).Assembly,
}; };