diff --git a/src/Components/Components/src/Rendering/Renderer.cs b/src/Components/Components/src/Rendering/Renderer.cs index d1fdfa35ff..fba917bf05 100644 --- a/src/Components/Components/src/Rendering/Renderer.cs +++ b/src/Components/Components/src/Rendering/Renderer.cs @@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Components.Rendering private bool _isBatchInProgress; private ulong _lastEventHandlerId; private List _pendingTasks; + private bool _disposed; /// /// Allows the caller to handle exceptions from the SynchronizationContext when one is available. @@ -403,6 +404,11 @@ namespace Microsoft.AspNetCore.Components.Rendering /// protected virtual void ProcessPendingRender() { + if (_disposed) + { + throw new ObjectDisposedException(nameof(Renderer), "Cannot process pending renders after the renderer has been disposed."); + } + ProcessRenderQueue(); } @@ -696,6 +702,8 @@ namespace Microsoft.AspNetCore.Components.Rendering /// if this method is being invoked by , otherwise . protected virtual void Dispose(bool disposing) { + _disposed = true; + // It's important that we handle all exceptions here before reporting any of them. // This way we can dispose all components before an error handler kicks in. List exceptions = null; @@ -717,6 +725,7 @@ namespace Microsoft.AspNetCore.Components.Rendering } } + _componentStateById.Clear(); // So we know they were all disposed _batchBuilder.Dispose(); if (exceptions?.Count > 1) diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs index fbd91decf7..ccca6b8006 100644 --- a/src/Components/Components/test/RendererTest.cs +++ b/src/Components/Components/test/RendererTest.cs @@ -2552,7 +2552,7 @@ namespace Microsoft.AspNetCore.Components.Test .ComponentId; var origEventHandlerId = renderer.Batches.Single() .ReferenceFrames - .Where(f => f.FrameType == RenderTreeFrameType.Attribute && f.AttributeName == "onclick") + .Where(f => f.FrameType == RenderTreeFrameType.Attribute && f.AttributeName == "onmycustomevent") .Single(f => f.AttributeEventHandlerId != 0) .AttributeEventHandlerId; @@ -3490,6 +3490,41 @@ namespace Microsoft.AspNetCore.Components.Test Assert.True(component.Disposed); } + [Fact] + public void DisposingRenderer_RejectsAttemptsToStartMoreRenderBatches() + { + // Arrange + var renderer = new TestRenderer(); + renderer.Dispose(); + + // Act/Assert + var ex = Assert.Throws(() => renderer.ProcessPendingRender()); + Assert.Contains("Cannot process pending renders after the renderer has been disposed.", ex.Message); + } + + [Fact] + public void WhenRendererIsDisposed_ComponentRenderRequestsAreSkipped() + { + // The important point of this is that user code in components may continue to call + // StateHasChanged (e.g., after an async task completion), and we don't want that to + // show up as an error. In general, components should skip rendering after disposal. + // This test shows that we don't add any new entries to the render queue after disposal. + // There's a different test showing that if the render queue entry was already added + // before a component got individually disposed, that render queue entry gets skipped. + + // Arrange + var renderer = new TestRenderer(); + var component = new DisposableComponent(); + renderer.AssignRootComponentId(component); + + // Act + renderer.Dispose(); + component.TriggerRender(); + + // Assert: no exception, no batch produced + Assert.Empty(renderer.Batches); + } + [Fact] public void DisposingRenderer_DisposesNestedComponents() { @@ -3936,7 +3971,7 @@ namespace Microsoft.AspNetCore.Components.Test => _renderHandle.Render(builder => { builder.OpenElement(0, "my button"); - builder.AddAttribute(1, "my click handler", new Action(eventArgs => OnClick(eventArgs))); + builder.AddAttribute(1, "onmycustomevent", EventCallback.Factory.Create(this, eventArgs => OnClick(eventArgs))); builder.CloseElement(); }); } diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs index 4c77968d42..2a1ded9244 100644 --- a/src/Components/Server/src/Circuits/RemoteRenderer.cs +++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs @@ -284,7 +284,15 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering // missing. // We return the task in here, but the caller doesn't await it. - return Dispatcher.InvokeAsync(() => ProcessPendingRender()); + return Dispatcher.InvokeAsync(() => + { + // Now we're on the sync context, check again whether we got disposed since this + // work item was queued. If so there's nothing to do. + if (!_disposing) + { + ProcessPendingRender(); + } + }); } } diff --git a/src/Components/Shared/test/TestRenderer.cs b/src/Components/Shared/test/TestRenderer.cs index 135a5cbda2..b44cdf479b 100644 --- a/src/Components/Shared/test/TestRenderer.cs +++ b/src/Components/Shared/test/TestRenderer.cs @@ -125,5 +125,8 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers OnUpdateDisplayComplete?.Invoke(); return NextRenderResultTask; } + + public new void ProcessPendingRender() + => base.ProcessPendingRender(); } }