[Blazor] Update bind:set,after runtime apis #44412
Merged
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Updates the runtime helper APIs the compiler will use with bind:get, bind:set, and bind:after.
Description
There is a full technical description of the issue all the way down the issue for all the details.
The TL;DR is that due to the way we decided to implement this feature, customers can run into situations where their components render many more times than expected, which results in confusion and performance degradation.
This was due to the fact that we chose to rely on EventCallback to implement this feature and we did not realize there were some side effects associated with it.
The APIs we are adding bypass the whole EventCallback infrastructure (simplifying the implementation too) and fix the issue.
Fixes #43195
Customer Impact
Low performance, unexpected behavior.
The reason it is important to fix this issue is because it is the result of feedback from the previews and since it involves the razor compiler, we won't be able to easily fix it in the future without introducing a breaking behavior change.
Regression?
[If yes, specify the version the behavior has regressed from]
Risk
The only reason this is medium and not low is because it has a sibling PR on the razor-compiler to update it to use the new methods.
Verification
Packaging changes reviewed?
Complete description and detailed technical analisys
bind get, set, after changes
There was an oversight on the initial design of bind get, set, after. We decided to piggyback on the EventCallback infrastructure to simplify the implementation as the runtime helpers that we already have provide support for coercing functions and actions into an event callback (typed or untyped).
Unfortunately, that has an additional unintended side-effect. The runtime helpers that the compiler uses for creating EventCallback have logic in them to select a "receiver" for the callback. If the callback target implements IHandleEvent, it gets chosen as the receiver. Otherwise, the explicit receiver passed in is used.
The issue is that when the receiver implements IHandleEvent, the EventCallback infrastrucure will call
receiver.HandleEventAsync(evt)
. In the most common implementation (ComponentBase) this will in turn callStateHasChanged()
internally up to two times (at the end of the sync and async work).The result of this is that when using bind get, set, after
StateHasChanged
will be called more than it is needed, which surprises users and can cause unintended performance problems and other side effects.The solution for this unfortunately, involves updating the runtime helper APIs for this scenario as well as the compiler to generate new code that consumes these new APIs.
We can piggyback on EventCallback APIs, but since we need to author new APIs, it is best to avoid this altogether.
Before we go into details on the concrete APIs we will recap the list of supported scenarios.
Binding to elements
In this case we are binding to a DOM event.
Generated callback code
CreateBinder has overloads for the different primitive types we support binding and each overload can receive either an Action or an EventCallback that was added as part of the support we did for bind get, set, after.
Binding to components
In this case we have a "parent" component and a "child" component where the "parent" component is binding to a pair of properties in the child component (for example Value and ValueChanged). ValueChanged can have two different types:
Action<T>
,EventCallback<T>
.Child component
Parent component
Generated code (EventCallback)
In this case, we use a helper
CreateInferredEventCallback
to convert the function to the right type of event callback, since writing the generic signature is complicated when we have generic type arguments.Generated code (Action)
In this case, we emit a type cast.
Bind to elements using set or after
In this scenario you are allowed to provide a callback which can be an
Action<T>
, aFunc<T, Task>
or anEventCallback<T>
for bind:set or anAction
, aFunc<Task>
or anEventCallback
forbind:after
.Bind to elements using bind:set
In this scenario we update the generated code to create a setter ourselves, and since we can't know if what the user provides is a function, an action or an explicit EventCallback, we use the
CreateInferredEventCallback
overloads to coerce everything into anEventCallback<T>
.Bind to elements using bind:after
In this scenario things are even more complicated as we need to compose a setter that invokes the after function.
The issue then becomes that the event callback at
EventCallback.Factory.Create(() => { DoSomething(); })
as well as theRuntimeHelpers.CreateInferredEventCallback
that wraps it, do more work than they should do. (As described above, they don't just invoke the callback, they'll call IHandleEvent.HandleEvent on the receiver if present)As a result, we want to tweak the generated code as follows. Instead of using CreateInferredEventCallback, we are going to use a new helper CreateInferredBindSetter with the following signature and two overloads:
Func<TValue, Task> CreateInferredBindSetter<TValue>(Action<TValue> setter, TValue value)
Func<TValue, Task> CreateInferredBindSetter<TValue>(Func<TValue, Task> setter, TValue value)
We use these two overloads because we do not know when we receive a setter expression whether it is a Task returning function or not.
We will remove the EventCallback based runtime APIs that we added as part of the feature as we do not need them.
We will avoid using CreateEventCallback for BindAfter as we already have
InvokeAsynchronousDelegate
to coalesce bind:after expressions and invoke them.Generated code before
Element bind get, set
Element bind after
Component bind after event callback
Child component
Parent component
Generated code
Component bind get, set event callback
Child component
Parent component
Generated code
Component bind after Task returning function
Child component
Parent component
Generated code
Component bind get, set async returning function
Child component
Parent component
Generated code
Component bind after action
Child component
Parent component
Generated code
Component bind get, set async returning function
Child component
Parent component
Generated code
Generated code after
Element bind get, set
Element bind after
Component bind after event callback
Child component
Parent component
Generated code
Component bind get, set event callback
Child component
Parent component
Generated code
Component bind after Task returning function
Child component
Parent component
Generated code
Component bind get, set async returning function
Child component
Parent component
Generated code
Component bind after action
Child component
Parent component
Generated code
Component bind get, set async returning function
Child component
Parent component
Generated code