[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 ulong _lastEventHandlerId;
|
||||
private List<Task> _pendingTasks;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Allows the caller to handle exceptions from the SynchronizationContext when one is available.
|
||||
|
|
@ -403,6 +404,11 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// </summary>
|
||||
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
|
|||
/// <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)
|
||||
{
|
||||
_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<Exception> 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)
|
||||
|
|
|
|||
|
|
@ -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<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]
|
||||
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>(eventArgs => OnClick(eventArgs)));
|
||||
builder.AddAttribute(1, "onmycustomevent", EventCallback.Factory.Create(this, eventArgs => OnClick(eventArgs)));
|
||||
builder.CloseElement();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -125,5 +125,8 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
|
|||
OnUpdateDisplayComplete?.Invoke();
|
||||
return NextRenderResultTask;
|
||||
}
|
||||
|
||||
public new void ProcessPendingRender()
|
||||
=> base.ProcessPendingRender();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue