-
Notifications
You must be signed in to change notification settings - Fork 645
Route events based on delegate target, not component ID #1651
Conversation
| @@ -1,6 +1,4 @@ | |||
| import { System_Array, MethodHandle } from '../Platform/Platform'; | |||
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 assuming these are removed as unused (same with platform)
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.
Correct
|
|
||
| const eventDescriptor = { | ||
| browserRendererId, | ||
| componentId, |
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 assuming that a lot of this got simpler because we no longer track the componentId associated with an eventHandler on the JS side. If this isn't right let me know 😆
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.
Exactly
| } | ||
| } | ||
|
|
||
| internal bool CheckDelegateTarget(object target) |
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.
IsSameTarget? I would expect a method named like this to throw
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.
That's reasonable. I'll change it.
| } | ||
| else | ||
| { | ||
| throw new InvalidOperationException( |
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 guessing that this validation is lost now that we've generalized this. I don't have a big concern about it because I haven't seen anyone complain about this error message.
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.
This error is no longer applicable as a concept.
| // Copyright (c) .NET Foundation. All rights reserved. | ||
| // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
|
||
| using Microsoft.AspNetCore.Blazor.Components; |
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.
Sort order is wrong
| </CascadingValue> | ||
|
|
||
| <p><button id="increment-count" onclick=@counterState.IncrementCount>Increment</button></p> | ||
| <p><button id="increment-count" onclick=@(() => counterState.IncrementCount())>Increment</button></p> |
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.
Is this an intentional change or did we create another gotcha here?
Based on what I can think of there's now a semantic difference between a capturing lambda and a method-group-to-delegate conversion in this case.
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.
This is the "breaking change" mentioned in the PR description.
There's now a semantic difference between passing delegates (e.g., lambdas) on the component type versus passing delegates on other types. There's no difference between lambdas versus methodgroup-to-delegate on the same component type. Please let me know if you think I'm incorrect or if you disagree with this design.
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.
No wait, there is a problem. Hmm... investigating options.
|
OK, turns out this is fundamentally flawed, and after some consideration, I don't have a sufficiently simple and robust plan to salvage the concept. I should have spotted this earlier, especially given Ryan's hints about how capturing lambdas might cause issues. I had been testing with lambdas that capture This whole idea of relying on the original target getting stamped on the delegate on creation seemed like a great solution because it survives the delegate getting passed through properties on any number of intermediate components. But if Frustratingly the exact data I need is right there on the public Possible approach to salvage this:
Apart from the complexity, what I dislike about this is that, if you're writing a Other possible approaches:
@rynowak Do you agree with this assessment? Any other ideas? I'm not expecting anything in this area to make it in for 0.7.0 any more. |
|
Another possibility is that we declare some new "event handler" types (say, [Parameter] UIEventHandler<UIClickEventArgs> OnMySpecialClick { get; set; }Then we change the codegen both for regular elements and for components so that event attributes produce some value like One nice benefit of this is that component authors no longer have to decide between Components can pass Developers writing a builder.AddAttribute(0, "onclick", new EventHandler(this, () => { ... }));That's not 100% ideal but writing Even if this design is viable, I don't expect it to make it in for 0.7.0. |
|
Further idea (noting to myself for future reference): we may be able to avoid declaring any static Func<Task> CreateEventHandler(Action postEventCallback, Action eventHandler)
static Func<T, Task> CreateEventHandler<T>(Action postEventCallback, Action<T> eventHandler)
static Func<Task> CreateEventHandler(Action postEventCallback, Func<Task> eventHandler)
static Func<T, Task> CreateEventHandler<T>(Action postEventCallback, Func<T, Task> eventHandler)each of which would do something like: if (eventHandler.Target == typeof(TypeContainingCreateEventHandler))
{
return eventHandler; // Pass through existing wrapped delegates without extra wrapping
}
else
{
// Create a new delegate whose Target == typeof(TypeContainingCreateEventHandler)
// as a signal not to wrap it further later
return () =>
{
var result = eventHandler();
postEventCallback();
return result.ContinueWith(() => postEventCallback());
};
}Then, the recipient gets a regular People writing This also completely eliminates the notion of Still need to think through exactly what allocations this leads to. If the methodgroup-to-delegate on |
|
Closing this because we're not concluded on the design yet. @rynowak and I are still discussing where we're going with bind and event handling. |
This fixes multiple issues and potential areas of confusion related to event routing (in effect, the scenarios where you have to call
StateHasChangedor not).Previously, if one component passed an event handler (e.g., an
Action,Func<Task>,MulticastDelegate, etc.) through to another component, either as a[Parameter]or simply by including it in someChildContent, then when the event was fired it would be routed not to the component that owns the event handler, but the one that rendered it into the tree.Example
MyButton.cshtml:
Consumer:
You'd expect that clicking the button would update the click count, but it wouldn't. You'd have to fix it by calling
StateHasChangedmanually in the consumer'sSomeMethodor the lambda. This was annoying and confusing.Example 2:
PassthroughComponent.cshtml:
Consumer.cshtml:
Again, clicking the button would seem to have no effect, unless you manually put
StateHasChangedinto the lambda.Fix
We now route events to the component matching
eventHandlerDelegate.Target, not the component that inserted the delegate into the render tree. This fixes both of the common cases above, and works with event handlers expressed as both methods and lambdas.This also simplifies cases around binding. Components that expose custom bindables can now trigger updates in their consumers easily.
New API
In the case where a custom component wants to trigger a supplied event handler delegate indirectly (e.g., inside one of its own methods), there's now a public static API:
This works with delegates that are
Action,Action<T>,Func<Task>,Func<T, Task>(the fast paths), or arbitraryMulticastDelgate(slower path). It automatically causes the recipient to trigger its own re-rendering logic, including responses to async tasks (i.e., if it's aFunc<Task>, then the recipient renders once synchronously, then again asynchronously after the task, just like normal event handlers).Breaking change
There's one scenario where this may break something people were already doing, i.e., relying on the older strange behavior. Example:
Previously, this would run
SomeOtherObject.SomeMethodand then re-render the component. That might be useful if this method updated some global state that you read back during rendering.Now it will no longer re-render the component, because
SomeMethodisn't on that component. If you actually do want to re-render the component in this case, the solution is to ensure you're actually calling one of its methods or lambdas, e.g.:Alternatively, if you're actually updating some global state container, it's better still to have the state container raise some event that triggers all the necessary UI updates on all components, not just the one that raised the action.
Although it's a slight drawback to have this breaking change, I still think this is the right thing to do. The new rules about when events update components are much simpler and easier to reason about (i.e., we re-render the component that owns the event handler method/lambda, regardless of who passed it where).