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:
parent
0ab2a1e586
commit
82d850d3a7
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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 " +
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue