Skip to content

Commit d937177

Browse files
Dispatch dispose to sync context. Fixes #32411 (#35217)
1 parent 3535cfb commit d937177

File tree

2 files changed

+77
-10
lines changed

2 files changed

+77
-10
lines changed

src/Components/Components/src/RenderTree/Renderer.cs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable
4343
private ulong _lastEventHandlerId;
4444
private List<Task>? _pendingTasks;
4545
private Task? _disposeTask;
46+
private bool _rendererIsDisposed;
4647

4748
/// <summary>
4849
/// Allows the caller to handle exceptions from the SynchronizationContext when one is available.
@@ -128,7 +129,7 @@ private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvid
128129
/// <summary>
129130
/// Gets whether the renderer has been disposed.
130131
/// </summary>
131-
internal bool Disposed { get; private set; }
132+
internal bool Disposed => _rendererIsDisposed;
132133

133134
private async void RenderRootComponentsOnHotReload()
134135
{
@@ -582,9 +583,10 @@ private ComponentState GetRequiredRootComponentState(int componentId)
582583
/// </summary>
583584
protected virtual void ProcessPendingRender()
584585
{
585-
if (Disposed)
586+
if (_rendererIsDisposed)
586587
{
587-
throw new ObjectDisposedException(nameof(Renderer), "Cannot process pending renders after the renderer has been disposed.");
588+
// Once we're disposed, we'll disregard further attempts to render anything
589+
return;
588590
}
589591

590592
ProcessRenderQueue();
@@ -974,7 +976,17 @@ private void HandleExceptionViaErrorBoundary(Exception error, ComponentState? er
974976
/// <param name="disposing"><see langword="true"/> if this method is being invoked by <see cref="IDisposable.Dispose"/>, otherwise <see langword="false"/>.</param>
975977
protected virtual void Dispose(bool disposing)
976978
{
977-
Disposed = true;
979+
if (!Dispatcher.CheckAccess())
980+
{
981+
// It's important that we only call the components' Dispose/DisposeAsync lifecycle methods
982+
// on the sync context, like other lifecycle methods. In almost all cases we'd already be
983+
// on the sync context here since DisposeAsync dispatches, but just in case someone is using
984+
// Dispose directly, we'll dispatch and block.
985+
Dispatcher.InvokeAsync(() => Dispose(disposing)).Wait();
986+
return;
987+
}
988+
989+
_rendererIsDisposed = true;
978990

979991
if (TestableMetadataUpdate.IsSupported)
980992
{
@@ -1079,7 +1091,7 @@ public void Dispose()
10791091
/// <inheritdoc />
10801092
public async ValueTask DisposeAsync()
10811093
{
1082-
if (Disposed)
1094+
if (_rendererIsDisposed)
10831095
{
10841096
return;
10851097
}
@@ -1090,7 +1102,8 @@ public async ValueTask DisposeAsync()
10901102
}
10911103
else
10921104
{
1093-
Dispose();
1105+
await Dispatcher.InvokeAsync(Dispose);
1106+
10941107
if (_disposeTask != null)
10951108
{
10961109
await _disposeTask;

src/Components/Components/test/RendererTest.cs

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3930,15 +3930,24 @@ public void DisposingRenderer_DisposesTopLevelComponents()
39303930
}
39313931

39323932
[Fact]
3933-
public void DisposingRenderer_RejectsAttemptsToStartMoreRenderBatches()
3933+
public void DisposingRenderer_DisregardsAttemptsToStartMoreRenderBatches()
39343934
{
39353935
// Arrange
39363936
var renderer = new TestRenderer();
3937+
var component = new TestComponent(builder =>
3938+
{
3939+
builder.OpenElement(0, "my element");
3940+
builder.AddContent(1, "some text");
3941+
builder.CloseElement();
3942+
});
3943+
3944+
// Act
3945+
renderer.AssignRootComponentId(component);
39373946
renderer.Dispose();
3947+
component.TriggerRender();
39383948

3939-
// Act/Assert
3940-
var ex = Assert.Throws<ObjectDisposedException>(() => renderer.ProcessPendingRender());
3941-
Assert.Contains("Cannot process pending renders after the renderer has been disposed.", ex.Message);
3949+
// Assert
3950+
Assert.Empty(renderer.Batches);
39423951
}
39433952

39443953
[Fact]
@@ -4679,6 +4688,51 @@ public void RemoveRootComponentHandlesDisposalExceptions()
46794688
Assert.Same(exception2, renderer.HandledExceptions[1]);
46804689
}
46814690

4691+
[Fact]
4692+
public void DisposeCallsComponentDisposeOnSyncContext()
4693+
{
4694+
// Arrange
4695+
var renderer = new TestRenderer();
4696+
var wasOnSyncContext = false;
4697+
var component = new DisposableComponent
4698+
{
4699+
DisposeAction = () =>
4700+
{
4701+
wasOnSyncContext = renderer.Dispatcher.CheckAccess();
4702+
}
4703+
};
4704+
4705+
// Act
4706+
var componentId = renderer.AssignRootComponentId(component);
4707+
renderer.Dispose();
4708+
4709+
// Assert
4710+
Assert.True(wasOnSyncContext);
4711+
}
4712+
4713+
[Fact]
4714+
public async Task DisposeAsyncCallsComponentDisposeAsyncOnSyncContext()
4715+
{
4716+
// Arrange
4717+
var renderer = new TestRenderer();
4718+
var wasOnSyncContext = false;
4719+
var component = new AsyncDisposableComponent
4720+
{
4721+
AsyncDisposeAction = () =>
4722+
{
4723+
wasOnSyncContext = renderer.Dispatcher.CheckAccess();
4724+
return ValueTask.CompletedTask;
4725+
}
4726+
};
4727+
4728+
// Act
4729+
var componentId = renderer.AssignRootComponentId(component);
4730+
await renderer.DisposeAsync();
4731+
4732+
// Assert
4733+
Assert.True(wasOnSyncContext);
4734+
}
4735+
46824736
private class TestComponentActivator<TResult> : IComponentActivator where TResult : IComponent, new()
46834737
{
46844738
public List<Type> RequestedComponentTypes { get; } = new List<Type>();

0 commit comments

Comments
 (0)