Skip to content

Commit 82d850d

Browse files
Components that accept bind-Something can request SomethingExpression (dotnet/razor#213)
* In binding to components, automatically supply FooExpression when requested * Fix tests * CR feedback \n\nCommit migrated from dotnet/razor@ca9de74
1 parent 0ab2a1e commit 82d850d

File tree

6 files changed

+87
-5
lines changed

6 files changed

+87
-5
lines changed

src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorMetadata.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public static class Bind
2424
public readonly static string ValueAttribute = "Blazor.Bind.ValueAttribute";
2525

2626
public readonly static string ChangeAttribute = "Blazor.Bind.ChangeAttribute";
27+
28+
public readonly static string ExpressionAttribute = "Blazor.Bind.ExpressionAttribute";
2729
}
2830

2931
public static class ChildContent

src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentBindLoweringPass.cs

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,11 @@ private IntermediateNode[] RewriteUsage(IntermediateNode parent, TagHelperProper
148148
{
149149
// Bind works similarly to a macro, it always expands to code that the user could have written.
150150
//
151-
// For the nodes that are related to the bind-attribute rewrite them to look like a pair of
151+
// For the nodes that are related to the bind-attribute rewrite them to look like a set of
152152
// 'normal' HTML attributes similar to the following transformation.
153153
//
154154
// Input: <MyComponent bind-Value="@currentCount" />
155-
// Output: <MyComponent Value ="...<get the value>..." ValueChanged ="... <set the value>..." />
155+
// Output: <MyComponent Value ="...<get the value>..." ValueChanged ="... <set the value>..." ValueExpression ="() => ...<get the value>..." />
156156
//
157157
// This means that the expression that appears inside of 'bind' must be an LValue or else
158158
// there will be errors. In general the errors that come from C# in this case are good enough
@@ -171,8 +171,10 @@ private IntermediateNode[] RewriteUsage(IntermediateNode parent, TagHelperProper
171171
node.AttributeName,
172172
out var valueAttributeName,
173173
out var changeAttributeName,
174+
out var expressionAttributeName,
174175
out var valueAttribute,
175-
out var changeAttribute))
176+
out var changeAttribute,
177+
out var expressionAttribute))
176178
{
177179
// Skip anything we can't understand. It's important that we don't crash, that will bring down
178180
// the build.
@@ -340,7 +342,32 @@ private IntermediateNode[] RewriteUsage(IntermediateNode parent, TagHelperProper
340342
changeNode.Children[0].Children.Add(changeExpressionTokens[i]);
341343
}
342344

343-
return new[] { valueNode, changeNode };
345+
// Finally, also emit a node for the "Expression" attribute, but only if the target
346+
// component is defined to accept one
347+
ComponentAttributeIntermediateNode expressionNode = null;
348+
if (expressionAttribute != null)
349+
{
350+
expressionNode = new ComponentAttributeIntermediateNode(node)
351+
{
352+
AttributeName = expressionAttributeName,
353+
BoundAttribute = expressionAttribute,
354+
PropertyName = expressionAttribute.GetPropertyName(),
355+
TagHelper = node.TagHelper,
356+
TypeName = expressionAttribute.IsWeaklyTyped() ? null : expressionAttribute.TypeName,
357+
};
358+
359+
expressionNode.Children.Clear();
360+
expressionNode.Children.Add(new CSharpExpressionIntermediateNode());
361+
expressionNode.Children[0].Children.Add(new IntermediateToken()
362+
{
363+
Content = $"() => {original.Content}",
364+
Kind = TokenKind.CSharp
365+
});
366+
}
367+
368+
return expressionNode == null
369+
? new[] { valueNode, changeNode }
370+
: new[] { valueNode, changeNode, expressionNode };
344371
}
345372
}
346373

@@ -394,11 +421,15 @@ private bool TryComputeAttributeNames(
394421
string attributeName,
395422
out string valueAttributeName,
396423
out string changeAttributeName,
424+
out string expressionAttributeName,
397425
out BoundAttributeDescriptor valueAttribute,
398-
out BoundAttributeDescriptor changeAttribute)
426+
out BoundAttributeDescriptor changeAttribute,
427+
out BoundAttributeDescriptor expressionAttribute)
399428
{
400429
valueAttribute = null;
401430
changeAttribute = null;
431+
expressionAttribute = null;
432+
expressionAttributeName = null;
402433

403434
// Even though some of our 'bind' tag helpers specify the attribute names, they
404435
// should still satisfy one of the valid syntaxes.
@@ -415,6 +446,7 @@ private bool TryComputeAttributeNames(
415446
// We expect 1 bind tag helper per-node.
416447
valueAttributeName = node.TagHelper.GetValueAttributeName() ?? valueAttributeName;
417448
changeAttributeName = node.TagHelper.GetChangeAttributeName() ?? changeAttributeName;
449+
expressionAttributeName = node.TagHelper.GetExpressionAttributeName() ?? expressionAttributeName;
418450

419451
// We expect 0-1 components per-node.
420452
var componentTagHelper = (parent as ComponentIntermediateNode)?.Component;
@@ -437,6 +469,12 @@ private bool TryComputeAttributeNames(
437469
changeAttributeName = valueAttributeName + "Changed";
438470
}
439471

472+
// Likewise for the expression attribute
473+
if (expressionAttributeName == null)
474+
{
475+
expressionAttributeName = valueAttributeName + "Expression";
476+
}
477+
440478
for (var i = 0; i < componentTagHelper.BoundAttributes.Count; i++)
441479
{
442480
var attribute = componentTagHelper.BoundAttributes[i];
@@ -450,6 +488,11 @@ private bool TryComputeAttributeNames(
450488
{
451489
changeAttribute = attribute;
452490
}
491+
492+
if (string.Equals(expressionAttributeName, attribute.Name))
493+
{
494+
expressionAttribute = attribute;
495+
}
453496
}
454497

455498
return true;

src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/TagHelperDescriptorExtensions.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,17 @@ public static string GetChangeAttributeName(this TagHelperDescriptor tagHelper)
9595
return result;
9696
}
9797

98+
public static string GetExpressionAttributeName(this TagHelperDescriptor tagHelper)
99+
{
100+
if (tagHelper == null)
101+
{
102+
throw new ArgumentNullException(nameof(tagHelper));
103+
}
104+
105+
tagHelper.Metadata.TryGetValue(BlazorMetadata.Bind.ExpressionAttribute, out var result);
106+
return result;
107+
}
108+
98109
public static bool IsChildContentTagHelper(this TagHelperDescriptor tagHelper)
99110
{
100111
if (tagHelper == null)

src/Razor/Microsoft.CodeAnalysis.Razor/src/BindTagHelperDescriptorProvider.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,8 @@ private List<TagHelperDescriptor> CreateComponentBindTagHelpers(ICollection<TagH
351351
//
352352
// The easiest way to figure this out without a lot of backtracking is to look for `FooChanged` and then
353353
// try to find a matching "Foo".
354+
//
355+
// We also look for a corresponding FooExpression attribute, though its presence is optional.
354356
for (var i = 0; i < tagHelper.BoundAttributes.Count; i++)
355357
{
356358
var changeAttribute = tagHelper.BoundAttributes[i];
@@ -360,12 +362,25 @@ private List<TagHelperDescriptor> CreateComponentBindTagHelpers(ICollection<TagH
360362
}
361363

362364
BoundAttributeDescriptor valueAttribute = null;
365+
BoundAttributeDescriptor expressionAttribute = null;
363366
var valueAttributeName = changeAttribute.Name.Substring(0, changeAttribute.Name.Length - "Changed".Length);
367+
var expressionAttributeName = valueAttributeName + "Expression";
364368
for (var j = 0; j < tagHelper.BoundAttributes.Count; j++)
365369
{
366370
if (tagHelper.BoundAttributes[j].Name == valueAttributeName && !tagHelper.BoundAttributes[j].IsDelegateProperty())
367371
{
368372
valueAttribute = tagHelper.BoundAttributes[j];
373+
374+
}
375+
376+
if (tagHelper.BoundAttributes[j].Name == expressionAttributeName)
377+
{
378+
expressionAttribute = tagHelper.BoundAttributes[j];
379+
}
380+
381+
if (valueAttribute != null && expressionAttribute != null)
382+
{
383+
// We found both, so we can stop looking now
369384
break;
370385
}
371386
}
@@ -388,6 +403,11 @@ private List<TagHelperDescriptor> CreateComponentBindTagHelpers(ICollection<TagH
388403
builder.Metadata[BlazorMetadata.Bind.ValueAttribute] = valueAttribute.Name;
389404
builder.Metadata[BlazorMetadata.Bind.ChangeAttribute] = changeAttribute.Name;
390405

406+
if (expressionAttribute != null)
407+
{
408+
builder.Metadata[BlazorMetadata.Bind.ExpressionAttribute] = expressionAttribute.Name;
409+
}
410+
391411
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
392412
// a C# property will crash trying to create the toolips.
393413
builder.SetTypeName(tagHelper.GetTypeName());

src/Razor/Microsoft.CodeAnalysis.Razor/test/BindTagHelperDescriptorProviderTest.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public void Execute_FindsBindTagHelperOnComponentType_CreatesDescriptor()
1616
// Arrange
1717
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
1818
using System;
19+
using System.Linq.Expressions;
1920
using Microsoft.AspNetCore.Components;
2021
2122
namespace Test
@@ -31,6 +32,9 @@ public void SetParameters(ParameterCollection parameters) { }
3132
3233
[Parameter]
3334
Action<string> MyPropertyChanged { get; set; }
35+
36+
[Parameter]
37+
Expression<Func<string>> MyPropertyExpression { get; set; }
3438
}
3539
}
3640
"));
@@ -69,6 +73,7 @@ public void SetParameters(ParameterCollection parameters) { }
6973

7074
Assert.Equal("MyProperty", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]);
7175
Assert.Equal("MyPropertyChanged", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]);
76+
Assert.Equal("MyPropertyExpression", bind.Metadata[BlazorMetadata.Bind.ExpressionAttribute]);
7277

7378
Assert.Equal(
7479
"Binds the provided expression to the 'MyProperty' property and a change event " +

src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorIntegrationTestBase.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ static RazorIntegrationTestBase()
3636
{
3737
typeof(System.Runtime.AssemblyTargetedPatchBandAttribute).Assembly, // System.Runtime
3838
typeof(Enumerable).Assembly, // Other .NET fundamental types
39+
typeof(System.Linq.Expressions.Expression).Assembly,
3940
typeof(ComponentBase).Assembly,
4041
};
4142

0 commit comments

Comments
 (0)