diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 66aa9e4908fe..ed3bc5951b65 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -43,6 +43,7 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable private ulong _lastEventHandlerId; private List? _pendingTasks; private Task? _disposeTask; + private bool _rendererIsDisposed; /// /// Allows the caller to handle exceptions from the SynchronizationContext when one is available. @@ -128,7 +129,7 @@ private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvid /// /// Gets whether the renderer has been disposed. /// - internal bool Disposed { get; private set; } + internal bool Disposed => _rendererIsDisposed; private async void RenderRootComponentsOnHotReload() { @@ -582,9 +583,10 @@ private ComponentState GetRequiredRootComponentState(int componentId) /// protected virtual void ProcessPendingRender() { - if (Disposed) + if (_rendererIsDisposed) { - throw new ObjectDisposedException(nameof(Renderer), "Cannot process pending renders after the renderer has been disposed."); + // Once we're disposed, we'll disregard further attempts to render anything + return; } ProcessRenderQueue(); @@ -974,7 +976,17 @@ private void HandleExceptionViaErrorBoundary(Exception error, ComponentState? er /// if this method is being invoked by , otherwise . protected virtual void Dispose(bool disposing) { - Disposed = true; + if (!Dispatcher.CheckAccess()) + { + // It's important that we only call the components' Dispose/DisposeAsync lifecycle methods + // on the sync context, like other lifecycle methods. In almost all cases we'd already be + // on the sync context here since DisposeAsync dispatches, but just in case someone is using + // Dispose directly, we'll dispatch and block. + Dispatcher.InvokeAsync(() => Dispose(disposing)).Wait(); + return; + } + + _rendererIsDisposed = true; if (TestableMetadataUpdate.IsSupported) { @@ -1079,7 +1091,7 @@ public void Dispose() /// public async ValueTask DisposeAsync() { - if (Disposed) + if (_rendererIsDisposed) { return; } @@ -1090,7 +1102,8 @@ public async ValueTask DisposeAsync() } else { - Dispose(); + await Dispatcher.InvokeAsync(Dispose); + if (_disposeTask != null) { await _disposeTask; diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs index 152c3a841d9e..a2b5288acc20 100644 --- a/src/Components/Components/test/RendererTest.cs +++ b/src/Components/Components/test/RendererTest.cs @@ -3930,15 +3930,24 @@ public void DisposingRenderer_DisposesTopLevelComponents() } [Fact] - public void DisposingRenderer_RejectsAttemptsToStartMoreRenderBatches() + public void DisposingRenderer_DisregardsAttemptsToStartMoreRenderBatches() { // Arrange var renderer = new TestRenderer(); + var component = new TestComponent(builder => + { + builder.OpenElement(0, "my element"); + builder.AddContent(1, "some text"); + builder.CloseElement(); + }); + + // Act + renderer.AssignRootComponentId(component); renderer.Dispose(); + component.TriggerRender(); - // Act/Assert - var ex = Assert.Throws(() => renderer.ProcessPendingRender()); - Assert.Contains("Cannot process pending renders after the renderer has been disposed.", ex.Message); + // Assert + Assert.Empty(renderer.Batches); } [Fact] @@ -4679,6 +4688,51 @@ public void RemoveRootComponentHandlesDisposalExceptions() Assert.Same(exception2, renderer.HandledExceptions[1]); } + [Fact] + public void DisposeCallsComponentDisposeOnSyncContext() + { + // Arrange + var renderer = new TestRenderer(); + var wasOnSyncContext = false; + var component = new DisposableComponent + { + DisposeAction = () => + { + wasOnSyncContext = renderer.Dispatcher.CheckAccess(); + } + }; + + // Act + var componentId = renderer.AssignRootComponentId(component); + renderer.Dispose(); + + // Assert + Assert.True(wasOnSyncContext); + } + + [Fact] + public async Task DisposeAsyncCallsComponentDisposeAsyncOnSyncContext() + { + // Arrange + var renderer = new TestRenderer(); + var wasOnSyncContext = false; + var component = new AsyncDisposableComponent + { + AsyncDisposeAction = () => + { + wasOnSyncContext = renderer.Dispatcher.CheckAccess(); + return ValueTask.CompletedTask; + } + }; + + // Act + var componentId = renderer.AssignRootComponentId(component); + await renderer.DisposeAsync(); + + // Assert + Assert.True(wasOnSyncContext); + } + private class TestComponentActivator : IComponentActivator where TResult : IComponent, new() { public List RequestedComponentTypes { get; } = new List();