-
Notifications
You must be signed in to change notification settings - Fork 35
Support @bind:get, @bind:set, @bind:after #70
Conversation
src/Microsoft.CodeAnalysis.Razor/src/BindTagHelperDescriptorProvider.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.CodeAnalysis.Razor/src/BindTagHelperDescriptorProvider.cs
Outdated
Show resolved
Hide resolved
@@ -261,6 +263,57 @@ bool HasTypeParameter(ITypeSymbol type) | |||
} | |||
} | |||
|
|||
private static string IsAwaitable(IPropertySymbol prop) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have to admit I don't really have any sense of what this is for. Could you briefly summarise what the IPropertySymbol
values here represent (e.g., where they come from) and what difference it makes whether we want to regard them as awaitable or not?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need for component bindings in the form of Value and ValueChanged where ValueChanged can be Action<T>
or Func<T, Task>
.
Unlike for other bindings when we can always create an EventCallback, in this case we can't (in the synchronous case at least). So what this method does is, when we are inspecting the ValueChanged property, (which we already know is a delegate) we check to see if it has a return type, and if that return type is awaitable
, which covers Task, ValueTask, and any other potential awaitable in the future.
The logic in this method is the same that the roslyn compiler uses to make that determination.
We use this information during the bindloweringphase to generate either an Task returning callback or a void returning callback
src/Microsoft.CodeAnalysis.Razor/test/BindTagHelperDescriptorProviderTest.cs
Show resolved
Hide resolved
src/Microsoft.AspNetCore.Razor.Language/src/TagMatchingRuleDescriptor.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.AspNetCore.Razor.Language/src/Intermediate/ComponentAttributeIntermediateNode.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentBindLoweringPass.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentBindLoweringPass.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentBindLoweringPass.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentBindLoweringPass.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentBindLoweringPass.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentBindLoweringPass.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentBindLoweringPass.cs
Show resolved
Hide resolved
src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentBindLoweringPass.cs
Show resolved
Hide resolved
src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentMetadata.cs
Outdated
Show resolved
Hide resolved
@@ -109,6 +109,8 @@ public static class RuntimeHelpers | |||
{ | |||
public const string TypeCheck = "global::Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.TypeCheck"; | |||
public const string CreateInferredEventCallback = "global::Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.CreateInferredEventCallback"; | |||
public const string InvokeSynchronousDelegate = "global::Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.InvokeSynchronousDelegate"; | |||
public const string InvokeAsynchronousDelegate = "global::Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.InvokeAsynchronousDelegate"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be interested to see how these helpers will look (e.g., their range of overloads). Does that info exist anywhere yet?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/Microsoft.AspNetCore.Razor.Language/src/Components/TagHelperDescriptorExtensions.cs
Show resolved
Hide resolved
...Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentCodeGenerationTestBase.cs
Outdated
Show resolved
Hide resolved
...Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentCodeGenerationTestBase.cs
Outdated
Show resolved
Hide resolved
...tRuntimeCodeGenerationTest/EventHandler_WithDelegate_PreventDefault/TestComponent.codegen.cs
Outdated
Show resolved
Hide resolved
IntermediateToken - - CSharp - global::Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.CreateInferredEventCallback(this, __value => ParentValue = __value, ParentValue) | ||
IntermediateToken - - CSharp - global::Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.CreateInferredEventCallback(this, | ||
IntermediateToken - - CSharp - __value => ParentValue = __value | ||
IntermediateToken - - CSharp - , ParentValue) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if breaking this over multiple lines is for an important reason, but it does add a lot of noise to the baseline diffs.
Is there any chance you could put this part of the code formatting back to how it was before, to minimize baseline churn, and then maybe do a separate PR (or at least an independent commit for it at the end of this PR after the code review is done)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's quite painful to do this, instead I would suggest the following:
- Clone the PR:
- run
git diff origin/main --diff-filter=M -- 'src/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/**'
to isolate these changes - run
git diff origin/main --diff-filter=A -- 'src/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/**'
to only look at new baselines
- run
Yep, it is a bit of a challenge, but I think we can handle it. The main issue is just trying to keep the baseline diffs to a human-readable level. I suggest avoiding the existing test case name changes until right at the end when everything is done and signed-off, and similarly revert the change that splits up the generated Likewise, if you could avoid doing any squash-rebasing from here on, it will make it possible just to review the subsequent commits and not have to go back over the existing changes. |
So far, this looks like a great enhancement! |
99bef6d
to
1b7302a
Compare
🆙 📅 |
src/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDiagnosticFactory.cs
Outdated
Show resolved
Hide resolved
115b37a
to
3c4b8e7
Compare
@SteveSandersonMS is there any concrete feedback on the PR you want us to discuss or are we good to move forward? |
…nd:get and bind:set style
…ntDiagnosticFactory.cs Co-authored-by: Steve Sanderson <[email protected]>
96e14b5
to
eaab3d6
Compare
Is there any article that explains a bit more? Don't we need to call ValueChanged anymore, or we will call in after method? |
@mckaragoz Please have a look at ASP.NET Core Blazor data binding |
Adds support for bind:get, bind:set, bind:after
This PR introduces a new syntax for bind that allows specifying an action to run after the binding or specifying the setter for the binding directly.
There are two companion PRs to this one:
There are in general three scenarios to cover:
<input type=text />
.When binding to components the receiving property can either be an
Action<T>, Func<T,Task>, Func<T,ValueTask>
or anEventCallback<T>
and we need to generate different code for each one of them.When binding to elements we rely on new overloads of CreateBinder that take in an
EventCallback<T>
with the appropriate T for the elements that we know how to bind to in addition to the existing overloads that receive anAction<T>
. For example, for T = bool?:Within the compiler we take care of creating the EventCallback using a CreateInferredEventCallback helper when the developer uses
bind:set
orbind:after
. For example:and
For components we also have to deal with the cases where the component property is an
Action<T>
or aFunc<T,<<awaitable>>>
where<<awaitable>>
follows the same rules as the C# compiler for determining if usingawait
is supported and coversTask
,ValueTask
, and any future addition. In these cases we can't leverageCreateInferredCallback
and instead we rely on two helper methodsInvokeSynchronousDelegate
andInvokeAsynchronousDelegate
that are provided by the runtime. These two methods help us make possible to invoke the provided expression by the developer no matter whether it is a method group or a lambda and take care of producing an error when the developer tries to bind to an Action with a function returning an awaitable result.For example, for:
we generate code like the one below: