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

View File

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

View File

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

View File

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