@@ -28,7 +28,7 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable
28
28
private readonly Dictionary < int , ComponentState > _componentStateById = new Dictionary < int , ComponentState > ( ) ;
29
29
private readonly Dictionary < IComponent , ComponentState > _componentStateByComponent = new Dictionary < IComponent , ComponentState > ( ) ;
30
30
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 ( ) ;
32
32
private readonly Dictionary < ulong , ulong > _eventHandlerIdReplacements = new Dictionary < ulong , ulong > ( ) ;
33
33
private readonly ILogger _logger ;
34
34
private readonly ComponentFactory _componentFactory ;
@@ -416,7 +416,22 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie
416
416
_pendingTasks ??= new ( ) ;
417
417
}
418
418
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
+
420
435
Log . HandlingEvent ( _logger , eventHandlerId , eventArgs ) ;
421
436
422
437
// 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
480
495
/// <returns>The parameter type expected by the event handler. Normally this is a subclass of <see cref="EventArgs"/>.</returns>
481
496
public Type GetEventArgsType ( ulong eventHandlerId )
482
497
{
483
- var methodInfo = GetRequiredEventCallback ( eventHandlerId ) . Delegate ? . Method ;
498
+ var methodInfo = GetRequiredEventBindingEntry ( eventHandlerId ) . Callback . Delegate ? . Method ;
484
499
485
500
// The DispatchEventAsync code paths allow for the case where Delegate or its method
486
501
// 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)
581
596
_pendingTasks ? . Add ( task ) ;
582
597
}
583
598
584
- internal void AssignEventHandlerId ( ref RenderTreeFrame frame )
599
+ internal void AssignEventHandlerId ( int renderedByComponentId , ref RenderTreeFrame frame )
585
600
{
586
601
var id = ++ _lastEventHandlerId ;
587
602
@@ -593,15 +608,15 @@ internal void AssignEventHandlerId(ref RenderTreeFrame frame)
593
608
//
594
609
// When that happens we intentionally box the EventCallback because we need to hold on to
595
610
// the receiver.
596
- _eventBindings . Add ( id , callback ) ;
611
+ _eventBindings . Add ( id , ( renderedByComponentId , callback ) ) ;
597
612
}
598
613
else if ( frame . AttributeValueField is MulticastDelegate @delegate )
599
614
{
600
615
// This is the common case for a delegate, where the receiver of the event
601
616
// is the same as delegate.Target. In this case since the receiver is implicit we can
602
617
// avoid boxing the EventCallback object and just re-hydrate it on the other side of the
603
618
// 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 ) ) ) ;
605
620
}
606
621
607
622
// 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
645
660
_eventHandlerIdReplacements . Add ( oldEventHandlerId , newEventHandlerId ) ;
646
661
}
647
662
648
- private EventCallback GetRequiredEventCallback ( ulong eventHandlerId )
663
+ private ( int RenderedByComponentId , EventCallback Callback ) GetRequiredEventBindingEntry ( ulong eventHandlerId )
649
664
{
650
- if ( ! _eventBindings . TryGetValue ( eventHandlerId , out var callback ) )
665
+ if ( ! _eventBindings . TryGetValue ( eventHandlerId , out var entry ) )
651
666
{
652
667
throw new ArgumentException ( $ "There is no event handler associated with this event. EventId: '{ eventHandlerId } '.", nameof ( eventHandlerId ) ) ;
653
668
}
654
669
655
- return callback ;
670
+ return entry ;
656
671
}
657
672
658
673
private ulong FindLatestEventHandlerIdInChain ( ulong eventHandlerId )
0 commit comments