@@ -28,7 +28,7 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable
2828 private readonly Dictionary < int , ComponentState > _componentStateById = new Dictionary < int , ComponentState > ( ) ;
2929 private readonly Dictionary < IComponent , ComponentState > _componentStateByComponent = new Dictionary < IComponent , ComponentState > ( ) ;
3030 private readonly RenderBatchBuilder _batchBuilder = new RenderBatchBuilder ( ) ;
31- private readonly Dictionary < ulong , EventCallback > _eventBindings = new Dictionary < ulong , EventCallback > ( ) ;
31+ private readonly Dictionary < ulong , ( int RenderedByComponentId , EventCallback Callback ) > _eventBindings = new ( ) ;
3232 private readonly Dictionary < ulong , ulong > _eventHandlerIdReplacements = new Dictionary < ulong , ulong > ( ) ;
3333 private readonly ILogger _logger ;
3434 private readonly ComponentFactory _componentFactory ;
@@ -416,7 +416,22 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie
416416 _pendingTasks ??= new ( ) ;
417417 }
418418
419- var callback = GetRequiredEventCallback ( eventHandlerId ) ;
419+ var ( renderedByComponentId , callback ) = GetRequiredEventBindingEntry ( eventHandlerId ) ;
420+
421+ // If this event attribute was rendered by a component that's since been disposed, don't dispatch the event at all.
422+ // This can occur because event handler disposal is deferred, so event handler IDs can outlive their components.
423+ // The reason the following check is based on "which component rendered this frame" and not on "which component
424+ // receives the callback" (i.e., callback.Receiver) is that if parent A passes a RenderFragment with events to child B,
425+ // and then child B is disposed, we don't want to dispatch the events (because the developer considers them removed
426+ // from the UI) even though the receiver A is still alive.
427+ if ( ! _componentStateById . ContainsKey ( renderedByComponentId ) )
428+ {
429+ // This is not an error since it can happen legitimately (in Blazor Server, the user might click a button at the same
430+ // moment that the component is disposed remotely, and then the click event will arrive after disposal).
431+ Log . SkippingEventOnDisposedComponent ( _logger , renderedByComponentId , eventHandlerId , eventArgs ) ;
432+ return Task . CompletedTask ;
433+ }
434+
420435 Log . HandlingEvent ( _logger , eventHandlerId , eventArgs ) ;
421436
422437 // Try to match it up with a receiver so that, if the event handler later throws, we can route the error to the
@@ -480,7 +495,7 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie
480495 /// <returns>The parameter type expected by the event handler. Normally this is a subclass of <see cref="EventArgs"/>.</returns>
481496 public Type GetEventArgsType ( ulong eventHandlerId )
482497 {
483- var methodInfo = GetRequiredEventCallback ( eventHandlerId ) . Delegate ? . Method ;
498+ var methodInfo = GetRequiredEventBindingEntry ( eventHandlerId ) . Callback . Delegate ? . Method ;
484499
485500 // The DispatchEventAsync code paths allow for the case where Delegate or its method
486501 // is null, and in this case the event receiver just receives null. This won't happen
@@ -581,7 +596,7 @@ protected virtual void AddPendingTask(ComponentState? componentState, Task task)
581596 _pendingTasks ? . Add ( task ) ;
582597 }
583598
584- internal void AssignEventHandlerId ( ref RenderTreeFrame frame )
599+ internal void AssignEventHandlerId ( int renderedByComponentId , ref RenderTreeFrame frame )
585600 {
586601 var id = ++ _lastEventHandlerId ;
587602
@@ -593,15 +608,15 @@ internal void AssignEventHandlerId(ref RenderTreeFrame frame)
593608 //
594609 // When that happens we intentionally box the EventCallback because we need to hold on to
595610 // the receiver.
596- _eventBindings . Add ( id , callback ) ;
611+ _eventBindings . Add ( id , ( renderedByComponentId , callback ) ) ;
597612 }
598613 else if ( frame . AttributeValueField is MulticastDelegate @delegate )
599614 {
600615 // This is the common case for a delegate, where the receiver of the event
601616 // is the same as delegate.Target. In this case since the receiver is implicit we can
602617 // avoid boxing the EventCallback object and just re-hydrate it on the other side of the
603618 // render tree.
604- _eventBindings . Add ( id , new EventCallback ( @delegate . Target as IHandleEvent , @delegate ) ) ;
619+ _eventBindings . Add ( id , ( renderedByComponentId , new EventCallback ( @delegate . Target as IHandleEvent , @delegate ) ) ) ;
605620 }
606621
607622 // NOTE: we do not to handle EventCallback<T> here. EventCallback<T> is only used when passing
@@ -645,14 +660,14 @@ internal void TrackReplacedEventHandlerId(ulong oldEventHandlerId, ulong newEven
645660 _eventHandlerIdReplacements . Add ( oldEventHandlerId , newEventHandlerId ) ;
646661 }
647662
648- private EventCallback GetRequiredEventCallback ( ulong eventHandlerId )
663+ private ( int RenderedByComponentId , EventCallback Callback ) GetRequiredEventBindingEntry ( ulong eventHandlerId )
649664 {
650- if ( ! _eventBindings . TryGetValue ( eventHandlerId , out var callback ) )
665+ if ( ! _eventBindings . TryGetValue ( eventHandlerId , out var entry ) )
651666 {
652667 throw new ArgumentException ( $ "There is no event handler associated with this event. EventId: '{ eventHandlerId } '.", nameof ( eventHandlerId ) ) ;
653668 }
654669
655- return callback ;
670+ return entry ;
656671 }
657672
658673 private ulong FindLatestEventHandlerIdInChain ( ulong eventHandlerId )
0 commit comments