Closed uazo closed 5 years ago
I've found another way: blazor can only change the delegate for eventBindings dictionary, keeping same EventHandlerId. I do not think there are contraindications: @SteveSandersonMS what do you think?
here https://github.com/uazo/Blazor/tree/retain-eventhandlerid code and samples
...
if (oldFrame.AttributeEventHandlerId > 0)
{
// DONT REMOVE, SIMPLY CHANGE
//diffContext.BatchBuilder.DisposedEventHandlerIds.Append(oldFrame.AttributeEventHandlerId);
diffContext.Renderer.AssignEventHandlerId(ref newFrame, oldFrame.AttributeEventHandlerId);
newFrame = oldFrame;
}
...
internal void AssignEventHandlerId(ref RenderTreeFrame frame, int id)
{
if( id <= 0 ) id = ++_lastEventHandlerId;
if (frame.AttributeValue is MulticastDelegate @delegate)
{
if (_eventBindings.ContainsKey(id))
_eventBindings[id] = new EventHandlerInvoker(@delegate);
else
_eventBindings.Add(id, new EventHandlerInvoker(@delegate));
}
frame = frame.WithAttributeEventHandlerId(id);
}
@uazo I agree it would be great if we could do that. But won't this break cases where the event handler is meant to change?
Example:
<button onclick=@clickHandler>Click me</button>
@functions {
Action clickHandler = () => {
Console.WriteLine("First click");
// Now change the delegate
clickHandler = () => {
Console.WriteLine("Second click");
};
};
}
that code doesn't compile...
Making it static all is working as expected.
It's because EventHandlerId
is a marker for the javascript code for the current delegate (c# side), regardless of what it is.
Maybe a problem happens for nested events, with multiple StateHasChanged()
, just because is StateHasChanged (when calling ProcessRenderQueue
) to assign the current EventHandlerInvoker
. So, if a code change the delegate in a event, and in the browser there is another event that what to call the older delegate, my code doesn't works.
I try to code an example.
there is no way (for now :) to generate nested events. I tried with webcomponents but also the current code returns unexpected results
Trying with https://github.com/aspnet/Blazor/issues/802#issuecomment-387953426, it works very well.
I am stuck with this problem as well. Its kind of funny how this is working. Consider the next code and how it behaves where i is an index:
onclick="@OnClick(i)"
In this approach I get the correct index of the record but all records OnClick event gets fired.
onclick="@(() => OnClick(i))"
In this approach only one event gets fired but the index is wrong, it turns to be the length of the array.
I have spend days trying to do a work around it and I just went out of ideas for it...
@arivera12
In this approach only one event gets fired but the index is wrong, it turns to be the length of the array.
I don't see your code but maybe you have problem like this: https://github.com/aspnet/Blazor/issues/1402
@Andrzej-W
I don't see your code but maybe you have problem like this: aspnet/Blazor#1402
This worked as expected and yes that was my issue! Thanks so much!
Beyond the performance concerns mentioned above this (anti?-) pattern seems to trigger aspnet/AspNetCore#6385.
I have a table (with lambda event handlers) that grows rapidly to a couple hundred elements, and ends up failing to AssignEventHandlerId
within less than a minute of use.
I was able to work around it by creating a new element and passing the value as a parameter removing the need for a lambda.
Before:
@for (var i = 0; i < Messages.Count; i++) {
var message = Messages[i];
<tr ondblclick="@(() => MessageDoubleClick(message))">
...
After:
@for (var i = 0; i < Messages.Count; i++) {
<MessageRow Message="@Messages[i]" OnDoubleClick="@MessageDoubleClick"></MessageRow>
}
It's a little verbose, but hopefully it can get anyone hitting this issue moving until the issue is addressed.
@SteveSandersonMS is #8232 the desired resolution of this?
No, I'm not aware that there is any resolution for this. In general we'd have to make the compiler able to know whether a given delegate can change or not, and if it can't, implement some caching logic. It's outside the scope of anything we plan to do in the short term.
One useful benefit of moving to newer versions of C# is that the compiler does get smarter about it. For event handlers that are methodgroups (e.g., onclick=@SomeMethod
), the compiler is increasingly able to inject its own methodgroup-to-delegate-conversion cache field on the class. Last time I checked it only worked with static methods, but maybe it will get support for instance methods later.
Since I don't think there's any action for us to take, I'm closing this.
CC @rynowak in case he has any more knowledge about where the C# compiler is going with this, or in case he has any new ideas about what we might do inside the Razor compiler to improve it. Ryan, if you do think we could do anything here (even in the long term), please feel free to reopen.
you must excuse me for the late reply.
I agree it would be great if we could do that. But won't this break cases where the event handler is meant to change?
Maybe I don't understand but we don't have to check if the delegate is the same or not, my code always replaces it! I don't modify the EventHandlerId to avoid triggering the javascript "handler elimination/generation" part, and in server-side it reduces the frame size. That is "Same EventHandlerId but different delegate"
Invocation:
public virtual Task DispatchEventAsync(int eventHandlerId, UIEventArgs eventArgs)
{
EnsureSynchronizationContext();
The callback is referenced by eventHandlerId vvvvv
if (!_eventBindings.TryGetValue(eventHandlerId, out var callback))
{
throw new ArgumentException($"There is no event handler with ID {eventHandlerId}");
}
Task task = null;
try
{
// The event handler might request multiple renders in sequence. Capture them
// all in a single batch.
_isBatchInProgress = true;
task = callback.InvokeAsync(eventArgs);
}
Assignment:
internal void AssignEventHandlerId(ref RenderTreeFrame frame, int id)
{
if( id <= 0 ) id = ++_lastEventHandlerId;
if (frame.AttributeValue is MulticastDelegate @delegate)
{
if (_eventBindings.ContainsKey(id))
_eventBindings[id] = new EventHandlerInvoker(@delegate);
else
_eventBindings.Add(id, new EventHandlerInvoker(@delegate));
}
frame = frame.WithAttributeEventHandlerId(id);
}
Note: the code must be updated with the current master, but the logic is always the same.
Why we have to Delete and Recreate the id, after all in the events if something changes is only c# side, in javascript the reference is always to the same event, "onclick" for example (and necessarily because AppendDiffEntriesForAttributeFrame takes for granted that oldTree and newTree refer to the same attribute of the same element).
// This should only be called for attributes that have the same name. This is an
// invariant maintained by the callers.
private static void AppendDiffEntriesForAttributeFrame(
what am I missing here?
It's not safe to replace change which delegate an event handler ID refers to. If we did that, then it's possible that:
Keeping the event-handler-ID-to-delegate mapping preserves the association between a user action and whatever data is captured within the delegate's lambda.
Consider this:
blazor will generate three
EventHandlerId
. When I press the button, the firstEventHandlerId
is retained (just because the ladba "point" at the same address) but the others are simple new labda expression, so blazor will delete and recreate the handler. In a list with many elements and many handler, it's a performance problem.a way to retain can be:
other ideas?