[Blazor][Fixes #13056] Renderer use-after-disposal tweaks
* Improves Renderer handling use after disposal. * Ensures RemoteRenderer skips resuming the render queue after ACK if it was since disposed
This commit is contained in:
parent
9bd027aa99
commit
e8917fc92f
|
|
@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
||||||
private bool _isBatchInProgress;
|
private bool _isBatchInProgress;
|
||||||
private ulong _lastEventHandlerId;
|
private ulong _lastEventHandlerId;
|
||||||
private List<Task> _pendingTasks;
|
private List<Task> _pendingTasks;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Allows the caller to handle exceptions from the SynchronizationContext when one is available.
|
/// Allows the caller to handle exceptions from the SynchronizationContext when one is available.
|
||||||
|
|
@ -403,6 +404,11 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual void ProcessPendingRender()
|
protected virtual void ProcessPendingRender()
|
||||||
{
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
throw new ObjectDisposedException(nameof(Renderer), "Cannot process pending renders after the renderer has been disposed.");
|
||||||
|
}
|
||||||
|
|
||||||
ProcessRenderQueue();
|
ProcessRenderQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -696,6 +702,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
||||||
/// <param name="disposing"><see langword="true"/> if this method is being invoked by <see cref="IDisposable.Dispose"/>, otherwise <see langword="false"/>.</param>
|
/// <param name="disposing"><see langword="true"/> if this method is being invoked by <see cref="IDisposable.Dispose"/>, otherwise <see langword="false"/>.</param>
|
||||||
protected virtual void Dispose(bool disposing)
|
protected virtual void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
|
_disposed = true;
|
||||||
|
|
||||||
// It's important that we handle all exceptions here before reporting any of them.
|
// 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.
|
// This way we can dispose all components before an error handler kicks in.
|
||||||
List<Exception> exceptions = null;
|
List<Exception> exceptions = null;
|
||||||
|
|
@ -717,6 +725,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_componentStateById.Clear(); // So we know they were all disposed
|
||||||
_batchBuilder.Dispose();
|
_batchBuilder.Dispose();
|
||||||
|
|
||||||
if (exceptions?.Count > 1)
|
if (exceptions?.Count > 1)
|
||||||
|
|
|
||||||
|
|
@ -2552,7 +2552,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
||||||
.ComponentId;
|
.ComponentId;
|
||||||
var origEventHandlerId = renderer.Batches.Single()
|
var origEventHandlerId = renderer.Batches.Single()
|
||||||
.ReferenceFrames
|
.ReferenceFrames
|
||||||
.Where(f => f.FrameType == RenderTreeFrameType.Attribute && f.AttributeName == "onclick")
|
.Where(f => f.FrameType == RenderTreeFrameType.Attribute && f.AttributeName == "onmycustomevent")
|
||||||
.Single(f => f.AttributeEventHandlerId != 0)
|
.Single(f => f.AttributeEventHandlerId != 0)
|
||||||
.AttributeEventHandlerId;
|
.AttributeEventHandlerId;
|
||||||
|
|
||||||
|
|
@ -3490,6 +3490,41 @@ namespace Microsoft.AspNetCore.Components.Test
|
||||||
Assert.True(component.Disposed);
|
Assert.True(component.Disposed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DisposingRenderer_RejectsAttemptsToStartMoreRenderBatches()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var renderer = new TestRenderer();
|
||||||
|
renderer.Dispose();
|
||||||
|
|
||||||
|
// Act/Assert
|
||||||
|
var ex = Assert.Throws<ObjectDisposedException>(() => 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]
|
[Fact]
|
||||||
public void DisposingRenderer_DisposesNestedComponents()
|
public void DisposingRenderer_DisposesNestedComponents()
|
||||||
{
|
{
|
||||||
|
|
@ -3936,7 +3971,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
||||||
=> _renderHandle.Render(builder =>
|
=> _renderHandle.Render(builder =>
|
||||||
{
|
{
|
||||||
builder.OpenElement(0, "my button");
|
builder.OpenElement(0, "my button");
|
||||||
builder.AddAttribute(1, "my click handler", new Action<EventArgs>(eventArgs => OnClick(eventArgs)));
|
builder.AddAttribute(1, "onmycustomevent", EventCallback.Factory.Create(this, eventArgs => OnClick(eventArgs)));
|
||||||
builder.CloseElement();
|
builder.CloseElement();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -284,7 +284,15 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
// missing.
|
// missing.
|
||||||
|
|
||||||
// We return the task in here, but the caller doesn't await it.
|
// 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -125,5 +125,8 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
|
||||||
OnUpdateDisplayComplete?.Invoke();
|
OnUpdateDisplayComplete?.Invoke();
|
||||||
return NextRenderResultTask;
|
return NextRenderResultTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public new void ProcessPendingRender()
|
||||||
|
=> base.ProcessPendingRender();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue