From 719597a0e1d4a916a88bcde441156fe13b1e30d4 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 9 Jul 2021 14:03:25 +0100 Subject: [PATCH 1/6] Support removal of root components --- .../Components/src/PublicAPI.Unshipped.txt | 1 + .../Components/src/RenderTree/Renderer.cs | 56 ++++++-- .../Components/test/RendererTest.cs | 124 ++++++++++++++++++ src/Components/Shared/test/TestRenderer.cs | 3 + 4 files changed, 175 insertions(+), 9 deletions(-) diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index cfb5294d5d43..6b6a4b04377f 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -41,6 +41,7 @@ Microsoft.AspNetCore.Components.NavigationOptions.NavigationOptions() -> void Microsoft.AspNetCore.Components.NavigationOptions.ReplaceHistoryEntry.get -> bool Microsoft.AspNetCore.Components.NavigationOptions.ReplaceHistoryEntry.init -> void Microsoft.AspNetCore.Components.RenderHandle.IsRenderingOnMetadataUpdate.get -> bool +Microsoft.AspNetCore.Components.RenderTree.Renderer.RemoveRootComponent(int componentId) -> void Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.Dispose() -> void Microsoft.AspNetCore.Components.Routing.Router.PreferExactMatches.get -> bool Microsoft.AspNetCore.Components.Routing.Router.PreferExactMatches.set -> void diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 50f98618f33f..86926c2c5a0d 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -35,7 +35,7 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable private readonly Dictionary _eventHandlerIdReplacements = new Dictionary(); private readonly ILogger _logger; private readonly ComponentFactory _componentFactory; - private List<(ComponentState, ParameterView)>? _rootComponents; + private Dictionary? _rootComponentsLatestParameters; private int _nextComponentId; private bool _isBatchInProgress; @@ -134,7 +134,7 @@ private async void RenderRootComponentsOnHotReload() await Dispatcher.InvokeAsync(() => { - if (_rootComponents is null) + if (_rootComponentsLatestParameters is null) { return; } @@ -142,9 +142,10 @@ await Dispatcher.InvokeAsync(() => IsRenderingOnMetadataUpdate = true; try { - foreach (var (componentState, initialParameters) in _rootComponents) + foreach (var (componentId, parameters) in _rootComponentsLatestParameters) { - componentState.SetDirectParameters(initialParameters); + var componentState = GetRequiredComponentState(componentId); + componentState.SetDirectParameters(parameters); } } finally @@ -231,8 +232,8 @@ protected async Task RenderRootComponentAsync(int componentId, ParameterView ini { // when we're doing hot-reload, stash away the parameters used while rendering root components. // We'll use this to trigger re-renders on hot reload updates. - _rootComponents ??= new(); - _rootComponents.Add((componentState, initialParameters.Clone())); + _rootComponentsLatestParameters ??= new(); + _rootComponentsLatestParameters[componentId] = initialParameters.Clone(); } componentState.SetDirectParameters(initialParameters); @@ -248,6 +249,29 @@ protected async Task RenderRootComponentAsync(int componentId, ParameterView ini } } + /// + /// Removes the specified component from the renderer, causing the component and its + /// descendants to be disposed. + /// + /// The ID of the root component. + protected void RemoveRootComponent(int componentId) + { + Dispatcher.AssertAccess(); + + var rootComponentState = GetRequiredComponentState(componentId); + if (rootComponentState.ParentComponentState is not null) + { + throw new InvalidOperationException("The specified component is not a root component"); + } + + // This assumes there isn't currently a batch in progress, and will throw if there is. + // Currently there's no known scenario where we need to support calling RemoveRootComponentAsync + // during a batch, but if a scenario emerges we can add support. + _batchBuilder.ComponentDisposalQueue.Enqueue(componentId); + _rootComponentsLatestParameters?.Remove(componentId); + ProcessRenderQueue(); + } + /// /// Allows derived types to handle exceptions during rendering. Defaults to rethrowing the original exception. /// @@ -544,7 +568,17 @@ private void ProcessRenderQueue() { if (_batchBuilder.ComponentRenderQueue.Count == 0) { - return; + if (_batchBuilder.ComponentDisposalQueue.Count == 0) + { + // Nothing to do + return; + } + else + { + // Normally we process the disposal queue after each component rendering step, + // but in this case disposal is the only pending action so far + ProcessDisposalQueueInExistingBatch(); + } } // Process render queue until empty @@ -714,9 +748,13 @@ private void RenderInExistingBatch(RenderQueueEntry renderQueueEntry) HandleExceptionViaErrorBoundary(renderFragmentException, componentState); } - List exceptions = null; - // Process disposal queue now in case it causes further component renders to be enqueued + ProcessDisposalQueueInExistingBatch(); + } + + private void ProcessDisposalQueueInExistingBatch() + { + List exceptions = null; while (_batchBuilder.ComponentDisposalQueue.Count > 0) { var disposeComponentId = _batchBuilder.ComponentDisposalQueue.Dequeue(); diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs index f4604a5eaec4..a9a299fa8508 100644 --- a/src/Components/Components/test/RendererTest.cs +++ b/src/Components/Components/test/RendererTest.cs @@ -4411,6 +4411,130 @@ public async Task EventDispatchExceptionsCanBeHandledByClosestErrorBoundary_Afte component => Assert.Same(exception, component.ReceivedException)); } + [Fact] + public async Task CanRemoveRootComponents() + { + // Arrange + var renderer = new TestRenderer(); + var rootComponent = new TestComponent(builder => + { + builder.OpenComponent(0); + builder.CloseComponent(); + + builder.OpenComponent(1); + builder.CloseComponent(); + }); + var unrelatedComponent = new DisposableComponent(); + var rootComponentId = renderer.AssignRootComponentId(rootComponent); + var unrelatedRootComponentId = renderer.AssignRootComponentId(unrelatedComponent); + rootComponent.TriggerRender(); + unrelatedComponent.TriggerRender(); + Assert.Equal(2, renderer.Batches.Count); + + var nestedDisposableComponentFrame = renderer.Batches[0] + .GetComponentFrames().Single(); + var nestedAsyncDisposableComponentFrame = renderer.Batches[0] + .GetComponentFrames().Single(); + + // Act + _ = renderer.Dispatcher.InvokeAsync(() => renderer.RemoveRootComponent(rootComponentId)); + + // Assert: we disposed the specified root component and its descendants, but not + // the other root component + Assert.Equal(3, renderer.Batches.Count); + var batch = renderer.Batches.Last(); + Assert.Equal(new[] + { + rootComponentId, + nestedDisposableComponentFrame.ComponentId, + nestedAsyncDisposableComponentFrame.ComponentId, + }, batch.DisposedComponentIDs); + + // Assert: component instances were disposed properly + Assert.True(((DisposableComponent)nestedDisposableComponentFrame.Component).Disposed); + Assert.True(((AsyncDisposableComponent)nestedAsyncDisposableComponentFrame.Component).Disposed); + + // Assert: it's no longer known as a component + await renderer.Dispatcher.InvokeAsync(() => + { + var ex = Assert.Throws(() => + renderer.RemoveRootComponent(rootComponentId)); + Assert.Equal($"The renderer does not have a component with ID {rootComponentId}.", ex.Message); + }); + } + + [Fact] + public async Task CannotRemoveNonRootComponentsDirectly() + { + // Arrange + var renderer = new TestRenderer(); + var rootComponent = new TestComponent(builder => + { + builder.OpenComponent(0); + builder.CloseComponent(); + }); + var rootComponentId = renderer.AssignRootComponentId(rootComponent); + rootComponent.TriggerRender(); + + var nestedComponentFrame = renderer.Batches[0] + .GetComponentFrames().Single(); + var nestedComponent = (DisposableComponent)nestedComponentFrame.Component; + + // Act/Assert + await renderer.Dispatcher.InvokeAsync(() => + { + var ex = Assert.Throws(() => + renderer.RemoveRootComponent(nestedComponentFrame.ComponentId)); + Assert.Equal("The specified component is not a root component", ex.Message); + }); + + Assert.False(nestedComponent.Disposed); + } + + [Fact] + public void RemoveRootComponentHandlesDisposalExceptions() + { + // Arrange + var autoResetEvent = new AutoResetEvent(false); + var renderer = new TestRenderer { ShouldHandleExceptions = true }; + renderer.OnExceptionHandled = () => autoResetEvent.Set(); + var exception1 = new InvalidTimeZoneException(); + var exception2Tcs = new TaskCompletionSource(); + var rootComponent = new TestComponent(builder => + { + builder.AddContent(0, "Hello"); + builder.OpenComponent(1); + builder.AddAttribute(1, nameof(DisposableComponent.DisposeAction), (Action)(() => throw exception1)); + builder.CloseComponent(); + + builder.OpenComponent(2); + builder.AddAttribute(1, nameof(AsyncDisposableComponent.AsyncDisposeAction), (Func)(async () => await exception2Tcs.Task)); + builder.CloseComponent(); + }); + var rootComponentId = renderer.AssignRootComponentId(rootComponent); + rootComponent.TriggerRender(); + Assert.Single(renderer.Batches); + + var nestedDisposableComponentFrame = renderer.Batches[0] + .GetComponentFrames().Single(); + var nestedAsyncDisposableComponentFrame = renderer.Batches[0] + .GetComponentFrames().Single(); + + // Act + renderer.Dispatcher.InvokeAsync(() => renderer.RemoveRootComponent(rootComponentId)); + + // Assert: we get the synchronous exception synchronously + Assert.Same(exception1, Assert.Single(renderer.HandledExceptions)); + + // Assert: we get the asynchronous exception asynchronously + var exception2 = new InvalidTimeZoneException(); + autoResetEvent.Reset(); + exception2Tcs.SetException(exception2); + autoResetEvent.WaitOne(); + Assert.Equal(2, renderer.HandledExceptions.Count); + Assert.Same(exception2, renderer.HandledExceptions[1]); + } + private class TestComponentActivator : IComponentActivator where TResult : IComponent, new() { public List RequestedComponentTypes { get; } = new List(); diff --git a/src/Components/Shared/test/TestRenderer.cs b/src/Components/Shared/test/TestRenderer.cs index 418eb7a56eaa..df5791a6b45d 100644 --- a/src/Components/Shared/test/TestRenderer.cs +++ b/src/Components/Shared/test/TestRenderer.cs @@ -55,6 +55,9 @@ public TestRenderer(IServiceProvider serviceProvider, IComponentActivator compon public new int AssignRootComponentId(IComponent component) => base.AssignRootComponentId(component); + public new void RemoveRootComponent(int componentId) + => base.RemoveRootComponent(componentId); + public new ArrayRange GetCurrentRenderTreeFrames(int componentId) => base.GetCurrentRenderTreeFrames(componentId); From 41d9ddb525bf54f54adb5814b647238c3c8c8064 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 9 Jul 2021 15:26:49 +0100 Subject: [PATCH 2/6] Support supplying new parameters to root components, even while not quiescent --- .../Components/src/RenderTree/Renderer.cs | 93 ++++++------ .../Components/test/RendererTest.cs | 132 ++++++++++++++++++ 2 files changed, 186 insertions(+), 39 deletions(-) diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 86926c2c5a0d..81d3a096f9a1 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -200,37 +200,35 @@ protected Task RenderRootComponentAsync(int componentId) } /// - /// Performs the first render for a root component, waiting for this component and all - /// children components to finish rendering in case there is any asynchronous work being - /// done by any of the components. After this, the root component - /// makes its own decisions about when to re-render, so there is no need to call - /// this more than once. + /// Supplies parameters for a root component, normally causing it to render. This can be + /// used to trigger the first render of a root component, or to update its parameters and + /// trigger a subsequent render. Note that components may also make their own decisions about + /// when to re-render, and may re-render at any time. + /// + /// The returned waits for this component and all descendant components to + /// finish rendering in case there is any asynchronous work being done by any of them. /// /// The ID returned by . - /// The with the initial parameters to use for rendering. + /// The with the initial or updated parameters to use for rendering. /// /// Rendering a root component is an asynchronous operation. Clients may choose to not await the returned task to /// start, but not wait for the entire render to complete. /// protected async Task RenderRootComponentAsync(int componentId, ParameterView initialParameters) { - if (Interlocked.CompareExchange(ref _pendingTasks, new List(), null) != null) - { - throw new InvalidOperationException("There is an ongoing rendering in progress."); - } + Dispatcher.AssertAccess(); + + // Since this is a "render root" operation being invoked from outside the system, we start tracking + // any async tasks from this point until we reach quiescence. This allows external code such as prerendering + // to know when the renderer has some finished output. We don't track async tasks at other times + // because nobody would be waiting for quiescence at other times. + // Having a nonnull value for _pendingTasks is what signals that we should be capturing the async tasks. + _pendingTasks ??= new(); - // During the rendering process we keep a list of components performing work in _pendingTasks. - // _renderer.AddToPendingTasks will be called by ComponentState.SetDirectParameters to add the - // the Task produced by Component.SetParametersAsync to _pendingTasks in order to track the - // remaining work. - // During the synchronous rendering process we don't wait for the pending asynchronous - // work to finish as it will simply trigger new renders that will be handled afterwards. - // During the asynchronous rendering process we want to wait up until all components have - // finished rendering so that we can produce the complete output. var componentState = GetRequiredComponentState(componentId); if (TestableMetadataUpdate.IsSupported) { - // when we're doing hot-reload, stash away the parameters used while rendering root components. + // When we're doing hot-reload, stash away the parameters used while rendering root components. // We'll use this to trigger re-renders on hot reload updates. _rootComponentsLatestParameters ??= new(); _rootComponentsLatestParameters[componentId] = initialParameters.Clone(); @@ -238,15 +236,8 @@ protected async Task RenderRootComponentAsync(int componentId, ParameterView ini componentState.SetDirectParameters(initialParameters); - try - { - await ProcessAsynchronousWork(); - Debug.Assert(_pendingTasks.Count == 0); - } - finally - { - _pendingTasks = null; - } + await WaitForQuiescence(); + Debug.Assert(_pendingTasks == null); } /// @@ -278,21 +269,45 @@ protected void RemoveRootComponent(int componentId) /// The . protected abstract void HandleException(Exception exception); - private async Task ProcessAsynchronousWork() + private Task? _ongoingQuiescenceTask; + + private async Task WaitForQuiescence() { - // Child components SetParametersAsync are stored in the queue of pending tasks, - // which might trigger further renders. - while (_pendingTasks.Count > 0) + // If there's already a loop waiting for quiescence, just join it + if (_ongoingQuiescenceTask is not null) + { + await _ongoingQuiescenceTask; + return; + } + + try + { + _ongoingQuiescenceTask ??= ProcessAsynchronousWork(); + await _ongoingQuiescenceTask; + } + finally + { + Debug.Assert(_pendingTasks.Count == 0); + _pendingTasks = null; + _ongoingQuiescenceTask = null; + } + + async Task ProcessAsynchronousWork() { - // Create a Task that represents the remaining ongoing work for the rendering process - var pendingWork = Task.WhenAll(_pendingTasks); + // Child components SetParametersAsync are stored in the queue of pending tasks, + // which might trigger further renders. + while (_pendingTasks.Count > 0) + { + // Create a Task that represents the remaining ongoing work for the rendering process + var pendingWork = Task.WhenAll(_pendingTasks); - // Clear all pending work. - _pendingTasks.Clear(); + // Clear all pending work. + _pendingTasks.Clear(); - // new work might be added before we check again as a result of waiting for all - // the child components to finish executing SetParametersAsync - await pendingWork; + // new work might be added before we check again as a result of waiting for all + // the child components to finish executing SetParametersAsync + await pendingWork; + } } } diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs index a9a299fa8508..1fc573c8a878 100644 --- a/src/Components/Components/test/RendererTest.cs +++ b/src/Components/Components/test/RendererTest.cs @@ -273,6 +273,121 @@ await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(co AssertStream(1, logForChild); } + [Fact] + public void CanReRenderRootComponentsWithNewParameters() + { + // This differs from the other "CanReRender..." tests above in that the root component is being supplied + // with new parameters from outside, as opposed to making its own decision to re-render. + + // Arrange + var renderer = new TestRenderer(); + var component = new MessageComponent(); + var componentId = renderer.AssignRootComponentId(component); + renderer.RenderRootComponentAsync(componentId, ParameterView.FromDictionary(new Dictionary + { + [nameof(MessageComponent.Message)] = "Hello" + })); + + // Assert 1: First render + var batch = renderer.Batches.Single(); + var diff = batch.DiffsByComponentId[componentId].Single(); + Assert.Collection(diff.Edits, edit => + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + Assert.Equal(0, edit.ReferenceFrameIndex); + }); + AssertFrame.Text(batch.ReferenceFrames[0], "Hello"); + + // Act 2: Update params + renderer.RenderRootComponentAsync(componentId, ParameterView.FromDictionary(new Dictionary + { + [nameof(MessageComponent.Message)] = "Goodbye" + })); + + // Assert 2: Second render + var batch2 = renderer.Batches.Skip(1).Single(); + var diff2 = batch2.DiffsByComponentId[componentId].Single(); + Assert.Collection(diff2.Edits, edit => + { + Assert.Equal(RenderTreeEditType.UpdateText, edit.Type); + Assert.Equal(0, edit.ReferenceFrameIndex); + }); + AssertFrame.Text(batch2.ReferenceFrames[0], "Goodbye"); + } + + [Fact] + public async Task CanAddAndRenderNewRootComponentsWhileNotQuiescent() + { + // Arrange 1: An async root component + var renderer = new TestRenderer(); + var tcs1 = new TaskCompletionSource(); + var component1 = new AsyncComponent(tcs1.Task, 1); + var component1Id = renderer.AssignRootComponentId(component1); + + // Act/Assert 1: Its SetParametersAsync task remains incomplete + var renderTask1 = renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(component1Id)); + Assert.False(renderTask1.IsCompleted); + + // Arrange/Act 2: Can add a second root component while not quiescent + var tcs2 = new TaskCompletionSource(); + var component2 = new AsyncComponent(tcs2.Task, 1); + var component2Id = renderer.AssignRootComponentId(component2); + var renderTask2 = renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(component2Id)); + + // Assert 2 + Assert.False(renderTask1.IsCompleted); + Assert.False(renderTask2.IsCompleted); + + // Completing the first task isn't enough to consider the system quiescent, because there's now a second task + tcs1.SetResult(); + + // renderTask1 should not complete until we finish tcs2. + // We can't really prove that absolutely, but at least show it doesn't happen during a certain time period. + await Assert.ThrowsAsync(() => renderTask1.WaitAsync(TimeSpan.FromMilliseconds(250))); + Assert.False(renderTask1.IsCompleted); + Assert.False(renderTask2.IsCompleted); + + // Completing the second task does finally complete both render tasks + tcs2.SetResult(); + await Task.WhenAll(renderTask1, renderTask2); + } + + [Fact] + public async Task AsyncComponentTriggeringRootReRenderDoesNotDeadlock() + { + // Arrange + var renderer = new TestRenderer(); + var tcs = new TaskCompletionSource(); + int? componentId = null; + var hasRendered = false; + var component = new CallbackDuringSetParametersAsyncComponent + { + Callback = async () => + { + await tcs.Task; + if (!hasRendered) + { + hasRendered = true; + + // If we were to await here, then it would deadlock, because the component would be saying it's not + // finished rendering until the rendering system has already finished. The point of this test is to + // show that, as long as we don't await quiescence here, nothing within the system will be doing so + // and hence the whole process can complete. + _ = renderer.RenderRootComponentAsync(componentId.Value, ParameterView.Empty); + } + } + }; + componentId = renderer.AssignRootComponentId(component); + + // Act + var renderTask = renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId.Value)); + + // Assert + Assert.False(renderTask.IsCompleted); + tcs.SetResult(); + await renderTask; + } + [Fact] public async Task CanRenderAsyncComponentsWithSyncChildComponents() { @@ -5415,5 +5530,22 @@ public async Task HandleEventAsync(EventCallbackWorkItem item, object arg) } } } + + private class CallbackDuringSetParametersAsyncComponent : AutoRenderComponent + { + public int RenderCount { get; private set; } + public Func Callback { get; set; } + + public override async Task SetParametersAsync(ParameterView parameters) + { + await Callback(); + await base.SetParametersAsync(parameters); + } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + RenderCount++; + } + } } } From d573961988d9430b90ed31f413d06759f6c6f35b Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 12 Jul 2021 10:45:25 +0100 Subject: [PATCH 3/6] Tidyings --- src/Components/Components/src/RenderTree/Renderer.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 81d3a096f9a1..ab1d1db163dc 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -36,6 +36,7 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable private readonly ILogger _logger; private readonly ComponentFactory _componentFactory; private Dictionary? _rootComponentsLatestParameters; + private Task? _ongoingQuiescenceTask; private int _nextComponentId; private bool _isBatchInProgress; @@ -259,7 +260,11 @@ protected void RemoveRootComponent(int componentId) // Currently there's no known scenario where we need to support calling RemoveRootComponentAsync // during a batch, but if a scenario emerges we can add support. _batchBuilder.ComponentDisposalQueue.Enqueue(componentId); - _rootComponentsLatestParameters?.Remove(componentId); + if (HotReloadFeature.IsSupported) + { + _rootComponentsLatestParameters?.Remove(componentId); + } + ProcessRenderQueue(); } @@ -269,8 +274,6 @@ protected void RemoveRootComponent(int componentId) /// The . protected abstract void HandleException(Exception exception); - private Task? _ongoingQuiescenceTask; - private async Task WaitForQuiescence() { // If there's already a loop waiting for quiescence, just join it @@ -282,7 +285,7 @@ private async Task WaitForQuiescence() try { - _ongoingQuiescenceTask ??= ProcessAsynchronousWork(); + _ongoingQuiescenceTask = ProcessAsynchronousWork(); await _ongoingQuiescenceTask; } finally From 09bba1e450762710cb1f32545f7165ad2a6f2f7f Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 12 Jul 2021 10:58:41 +0100 Subject: [PATCH 4/6] Fix unit tests, now we better validate use of sync context Not regarding this as a breaking change, because it would never have been supported before to call the renderer from outside its sync context. --- .../test/Circuits/RemoteRendererTest.cs | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Components/Server/test/Circuits/RemoteRendererTest.cs b/src/Components/Server/test/Circuits/RemoteRendererTest.cs index f6e0d06da88f..1597c22a4784 100644 --- a/src/Components/Server/test/Circuits/RemoteRendererTest.cs +++ b/src/Components/Server/test/Circuits/RemoteRendererTest.cs @@ -208,12 +208,12 @@ public async Task OnRenderCompletedAsync_DoesNotThrowWhenReceivedDuplicateAcks() .Returns((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task); // This produces the initial batch (id = 2) - await renderer.RenderComponentAsync( - ParameterView.FromDictionary(new Dictionary - { - [nameof(AutoParameterTestComponent.Content)] = initialContent, - [nameof(AutoParameterTestComponent.Trigger)] = trigger - })); + await renderer.Dispatcher.InvokeAsync(() => renderer.RenderComponentAsync( + ParameterView.FromDictionary(new Dictionary + { + [nameof(AutoParameterTestComponent.Content)] = initialContent, + [nameof(AutoParameterTestComponent.Trigger)] = trigger + }))); trigger.Component.Content = (builder) => { builder.OpenElement(0, "offline element"); @@ -271,12 +271,12 @@ public async Task OnRenderCompletedAsync_DoesNotThrowWhenThereAreNoPendingBatche .Returns((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task); // This produces the initial batch (id = 2) - await renderer.RenderComponentAsync( - ParameterView.FromDictionary(new Dictionary - { - [nameof(AutoParameterTestComponent.Content)] = initialContent, - [nameof(AutoParameterTestComponent.Trigger)] = trigger - })); + await renderer.Dispatcher.InvokeAsync(() => renderer.RenderComponentAsync( + ParameterView.FromDictionary(new Dictionary + { + [nameof(AutoParameterTestComponent.Content)] = initialContent, + [nameof(AutoParameterTestComponent.Trigger)] = trigger + }))); trigger.Component.Content = (builder) => { builder.OpenElement(0, "offline element"); @@ -334,12 +334,12 @@ public async Task ConsumesAllPendingBatchesWhenReceivingAHigherSequenceBatchId() var trigger = new Trigger(); // This produces the initial batch (id = 2) - await renderer.RenderComponentAsync( - ParameterView.FromDictionary(new Dictionary - { - [nameof(AutoParameterTestComponent.Content)] = initialContent, - [nameof(AutoParameterTestComponent.Trigger)] = trigger - })); + await renderer.Dispatcher.InvokeAsync(() => renderer.RenderComponentAsync( + ParameterView.FromDictionary(new Dictionary + { + [nameof(AutoParameterTestComponent.Content)] = initialContent, + [nameof(AutoParameterTestComponent.Trigger)] = trigger + }))); trigger.Component.Content = (builder) => { builder.OpenElement(0, "offline element"); @@ -391,12 +391,12 @@ public async Task ThrowsIfWeReceivedAnAcknowledgeForANeverProducedBatch() var trigger = new Trigger(); // This produces the initial batch (id = 2) - await renderer.RenderComponentAsync( - ParameterView.FromDictionary(new Dictionary - { - [nameof(AutoParameterTestComponent.Content)] = initialContent, - [nameof(AutoParameterTestComponent.Trigger)] = trigger - })); + await renderer.Dispatcher.InvokeAsync(() => renderer.RenderComponentAsync( + ParameterView.FromDictionary(new Dictionary + { + [nameof(AutoParameterTestComponent.Content)] = initialContent, + [nameof(AutoParameterTestComponent.Trigger)] = trigger + }))); trigger.Component.Content = (builder) => { builder.OpenElement(0, "offline element"); From 8d71c1e4c035b0636ac87d434e5a835cfe588f2b Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 12 Jul 2021 11:10:09 +0100 Subject: [PATCH 5/6] Fix some unrelated unit tests while I'm here --- .../test/EventCallbackFactoryBinderExtensionsTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs b/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs index 8dccc87d0d8b..3cefe9f195ca 100644 --- a/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs +++ b/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs @@ -396,7 +396,7 @@ public async Task CreateBinder_DateTime() var expectedValue = new DateTime(2018, 3, 4, 1, 2, 3); // Act - await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(CultureInfo.InvariantCulture), }); + await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(CultureInfo.CurrentCulture), }); Assert.Equal(expectedValue, value); Assert.Equal(1, component.Count); @@ -415,7 +415,7 @@ public async Task CreateBinder_NullableDateTime() var expectedValue = new DateTime(2018, 3, 4, 1, 2, 3); // Act - await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(CultureInfo.InvariantCulture), }); + await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(CultureInfo.CurrentCulture), }); Assert.Equal(expectedValue, value); Assert.Equal(1, component.Count); @@ -474,7 +474,7 @@ public async Task CreateBinder_DateTimeOffset() var expectedValue = new DateTime(2018, 3, 4, 1, 2, 3); // Act - await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(CultureInfo.InvariantCulture), }); + await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(CultureInfo.CurrentCulture), }); Assert.Equal(expectedValue, value); Assert.Equal(1, component.Count); @@ -493,7 +493,7 @@ public async Task CreateBinder_NullableDateTimeOffset() var expectedValue = new DateTime(2018, 3, 4, 1, 2, 3); // Act - await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(CultureInfo.InvariantCulture), }); + await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(CultureInfo.CurrentCulture), }); Assert.Equal(expectedValue, value); Assert.Equal(1, component.Count); From a92a11edff42aa3645e7ebc6347bd8a1b40f21ed Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 12 Jul 2021 17:00:28 +0100 Subject: [PATCH 6/6] Update after rebase --- src/Components/Components/src/RenderTree/Renderer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index ab1d1db163dc..09c9e62ac7a0 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -260,7 +260,7 @@ protected void RemoveRootComponent(int componentId) // Currently there's no known scenario where we need to support calling RemoveRootComponentAsync // during a batch, but if a scenario emerges we can add support. _batchBuilder.ComponentDisposalQueue.Enqueue(componentId); - if (HotReloadFeature.IsSupported) + if (TestableMetadataUpdate.IsSupported) { _rootComponentsLatestParameters?.Remove(componentId); }