[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:
Steve Sanderson 2019-08-13 20:51:39 +01:00 committed by Javier Calvarro Nelson
parent 9bd027aa99
commit e8917fc92f
4 changed files with 58 additions and 3 deletions

View File

@ -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)

View File

@ -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();
});
}

View File

@ -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();
}
});
}
}

View File

@ -125,5 +125,8 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
OnUpdateDisplayComplete?.Invoke();
return NextRenderResultTask;
}
public new void ProcessPendingRender()
=> base.ProcessPendingRender();
}
}