Skip to content

Components that accept bind-Something can request SomethingExpression #213

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public static class Bind
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,11 @@ private IntermediateNode[] RewriteUsage(IntermediateNode parent, TagHelperProper
{
// 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: <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
// there will be errors. In general the errors that come from C# in this case are good enough
Expand All @@ -171,8 +171,10 @@ private IntermediateNode[] RewriteUsage(IntermediateNode parent, TagHelperProper
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.
Expand Down Expand Up @@ -340,7 +342,32 @@ private IntermediateNode[] RewriteUsage(IntermediateNode parent, TagHelperProper
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 };
}
}

Expand Down Expand Up @@ -394,11 +421,15 @@ private bool TryComputeAttributeNames(
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.
Expand All @@ -415,6 +446,7 @@ private bool TryComputeAttributeNames(
// 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;
Expand All @@ -437,6 +469,12 @@ private bool TryComputeAttributeNames(
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];
Expand All @@ -450,6 +488,11 @@ private bool TryComputeAttributeNames(
{
changeAttribute = attribute;
}

if (string.Equals(expressionAttributeName, attribute.Name))
{
expressionAttribute = attribute;
}
}

return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,17 @@ public static string GetChangeAttributeName(this TagHelperDescriptor tagHelper)
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,8 @@ private List<TagHelperDescriptor> CreateComponentBindTagHelpers(ICollection<TagH
//
// 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];
Expand All @@ -360,12 +362,25 @@ private List<TagHelperDescriptor> CreateComponentBindTagHelpers(ICollection<TagH
}

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;
}
}
Expand All @@ -388,6 +403,11 @@ private List<TagHelperDescriptor> CreateComponentBindTagHelpers(ICollection<TagH
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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,122 @@ void IComponent.SetParameters(ParameterCollection parameters)
CompileToAssembly(generated);
}

[Fact]
public void BindToComponent_SpecifiesValueAndExpression()
{
// Arrange
AdditionalSyntaxTrees.Add(Parse(@"
using System;
using System.Linq.Expressions;
using Microsoft.AspNetCore.Components;

namespace Test
{
public class MyComponent : ComponentBase
{
[Parameter]
int Value { get; set; }

[Parameter]
Action<int> ValueChanged { get; set; }

[Parameter]
Expression<Func<int>> ValueExpression { get; set; }
}
}"));

// Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
<MyComponent bind-Value=""ParentValue"" />
@functions {
public int ParentValue { get; set; } = 42;
}");

// Assert
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
CompileToAssembly(generated);
}

[Fact]
public void BindToComponent_SpecifiesValueAndExpression_TypeChecked()
{
// Arrange
AdditionalSyntaxTrees.Add(Parse(@"
using System;
using System.Linq.Expressions;
using Microsoft.AspNetCore.Components;

namespace Test
{
public class MyComponent : ComponentBase
{
[Parameter]
int Value { get; set; }

[Parameter]
Action<int> ValueChanged { get; set; }

[Parameter]
Expression<Func<string>> ValueExpression { get; set; }
}
}"));

// Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
<MyComponent bind-Value=""ParentValue"" />
@functions {
public int ParentValue { get; set; } = 42;
}");

var assembly = CompileToAssembly(generated, throwOnFailure: false);
// This has some errors
Assert.Collection(
assembly.Diagnostics.OrderBy(d => d.Id),
d => Assert.Equal("CS0029", d.Id),
d => Assert.Equal("CS1662", d.Id));
}

[Fact]
public void BindToComponent_SpecifiesValueAndExpression_Generic()
{
// Arrange
AdditionalSyntaxTrees.Add(Parse(@"
using System;
using System.Linq.Expressions;
using Microsoft.AspNetCore.Components;

namespace Test
{
public class MyComponent<T> : ComponentBase
{
[Parameter]
T SomeParam { get; set; }

[Parameter]
Action<T> SomeParamChanged { get; set; }

[Parameter]
Expression<Func<T>> SomeParamExpression { get; set; }
}
}"));

// Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
<MyComponent bind-SomeParam=""ParentValue"" />
@functions {
public DateTime ParentValue { get; set; } = DateTime.Now;
}");

// Assert
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
CompileToAssembly(generated);
}

[Fact]
public void BindToElement_WritesAttributes()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// <auto-generated/>
#pragma warning disable 1591
namespace Test
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
public class TestComponent : Microsoft.AspNetCore.Components.ComponentBase
{
#pragma warning disable 219
private void __RazorDirectiveTokenHelpers__() {
((System.Action)(() => {
#line 1 "x:\dir\subdir\Test\TestComponent.cshtml"
global::System.Object __typeHelper = "*, TestAssembly";

#line default
#line hidden
}
))();
}
#pragma warning restore 219
#pragma warning disable 0414
private static System.Object __o = null;
#pragma warning restore 0414
#pragma warning disable 1998
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
__o = Microsoft.AspNetCore.Components.RuntimeHelpers.TypeCheck<System.Int32>(Microsoft.AspNetCore.Components.BindMethods.GetValue(
#line 2 "x:\dir\subdir\Test\TestComponent.cshtml"
ParentValue

#line default
#line hidden
));
__o = new System.Action<System.Int32>(
__value => ParentValue = __value);
__o = Microsoft.AspNetCore.Components.RuntimeHelpers.TypeCheck<System.Linq.Expressions.Expression<System.Func<System.Int32>>>(() => ParentValue);
builder.AddAttribute(-1, "ChildContent", (Microsoft.AspNetCore.Components.RenderFragment)((builder2) => {
}
));
}
#pragma warning restore 1998
#line 3 "x:\dir\subdir\Test\TestComponent.cshtml"

public int ParentValue { get; set; } = 42;

#line default
#line hidden
}
}
#pragma warning restore 1591
Loading