Skip to content

Commit a017f74

Browse files
Renderer support for removing root components or updating parameters on them (#34232)
1 parent 5753baf commit a017f74

File tree

6 files changed

+388
-72
lines changed

6 files changed

+388
-72
lines changed

src/Components/Components/src/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Microsoft.AspNetCore.Components.NavigationOptions.NavigationOptions() -> void
4141
Microsoft.AspNetCore.Components.NavigationOptions.ReplaceHistoryEntry.get -> bool
4242
Microsoft.AspNetCore.Components.NavigationOptions.ReplaceHistoryEntry.init -> void
4343
Microsoft.AspNetCore.Components.RenderHandle.IsRenderingOnMetadataUpdate.get -> bool
44+
Microsoft.AspNetCore.Components.RenderTree.Renderer.RemoveRootComponent(int componentId) -> void
4445
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.Dispose() -> void
4546
Microsoft.AspNetCore.Components.Routing.Router.PreferExactMatches.get -> bool
4647
Microsoft.AspNetCore.Components.Routing.Router.PreferExactMatches.set -> void

src/Components/Components/src/RenderTree/Renderer.cs

Lines changed: 100 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable
3535
private readonly Dictionary<ulong, ulong> _eventHandlerIdReplacements = new Dictionary<ulong, ulong>();
3636
private readonly ILogger<Renderer> _logger;
3737
private readonly ComponentFactory _componentFactory;
38-
private List<(ComponentState, ParameterView)>? _rootComponents;
38+
private Dictionary<int, ParameterView>? _rootComponentsLatestParameters;
39+
private Task? _ongoingQuiescenceTask;
3940

4041
private int _nextComponentId;
4142
private bool _isBatchInProgress;
@@ -134,17 +135,18 @@ private async void RenderRootComponentsOnHotReload()
134135

135136
await Dispatcher.InvokeAsync(() =>
136137
{
137-
if (_rootComponents is null)
138+
if (_rootComponentsLatestParameters is null)
138139
{
139140
return;
140141
}
141142

142143
IsRenderingOnMetadataUpdate = true;
143144
try
144145
{
145-
foreach (var (componentState, initialParameters) in _rootComponents)
146+
foreach (var (componentId, parameters) in _rootComponentsLatestParameters)
146147
{
147-
componentState.SetDirectParameters(initialParameters);
148+
var componentState = GetRequiredComponentState(componentId);
149+
componentState.SetDirectParameters(parameters);
148150
}
149151
}
150152
finally
@@ -199,53 +201,71 @@ protected Task RenderRootComponentAsync(int componentId)
199201
}
200202

201203
/// <summary>
202-
/// Performs the first render for a root component, waiting for this component and all
203-
/// children components to finish rendering in case there is any asynchronous work being
204-
/// done by any of the components. After this, the root component
205-
/// makes its own decisions about when to re-render, so there is no need to call
206-
/// this more than once.
204+
/// Supplies parameters for a root component, normally causing it to render. This can be
205+
/// used to trigger the first render of a root component, or to update its parameters and
206+
/// trigger a subsequent render. Note that components may also make their own decisions about
207+
/// when to re-render, and may re-render at any time.
208+
///
209+
/// The returned <see cref="Task"/> waits for this component and all descendant components to
210+
/// finish rendering in case there is any asynchronous work being done by any of them.
207211
/// </summary>
208212
/// <param name="componentId">The ID returned by <see cref="AssignRootComponentId(IComponent)"/>.</param>
209-
/// <param name="initialParameters">The <see cref="ParameterView"/>with the initial parameters to use for rendering.</param>
213+
/// <param name="initialParameters">The <see cref="ParameterView"/> with the initial or updated parameters to use for rendering.</param>
210214
/// <remarks>
211215
/// Rendering a root component is an asynchronous operation. Clients may choose to not await the returned task to
212216
/// start, but not wait for the entire render to complete.
213217
/// </remarks>
214218
protected async Task RenderRootComponentAsync(int componentId, ParameterView initialParameters)
215219
{
216-
if (Interlocked.CompareExchange(ref _pendingTasks, new List<Task>(), null) != null)
217-
{
218-
throw new InvalidOperationException("There is an ongoing rendering in progress.");
219-
}
220+
Dispatcher.AssertAccess();
221+
222+
// Since this is a "render root" operation being invoked from outside the system, we start tracking
223+
// any async tasks from this point until we reach quiescence. This allows external code such as prerendering
224+
// to know when the renderer has some finished output. We don't track async tasks at other times
225+
// because nobody would be waiting for quiescence at other times.
226+
// Having a nonnull value for _pendingTasks is what signals that we should be capturing the async tasks.
227+
_pendingTasks ??= new();
220228

221-
// During the rendering process we keep a list of components performing work in _pendingTasks.
222-
// _renderer.AddToPendingTasks will be called by ComponentState.SetDirectParameters to add the
223-
// the Task produced by Component.SetParametersAsync to _pendingTasks in order to track the
224-
// remaining work.
225-
// During the synchronous rendering process we don't wait for the pending asynchronous
226-
// work to finish as it will simply trigger new renders that will be handled afterwards.
227-
// During the asynchronous rendering process we want to wait up until all components have
228-
// finished rendering so that we can produce the complete output.
229229
var componentState = GetRequiredComponentState(componentId);
230230
if (TestableMetadataUpdate.IsSupported)
231231
{
232-
// when we're doing hot-reload, stash away the parameters used while rendering root components.
232+
// When we're doing hot-reload, stash away the parameters used while rendering root components.
233233
// We'll use this to trigger re-renders on hot reload updates.
234-
_rootComponents ??= new();
235-
_rootComponents.Add((componentState, initialParameters.Clone()));
234+
_rootComponentsLatestParameters ??= new();
235+
_rootComponentsLatestParameters[componentId] = initialParameters.Clone();
236236
}
237237

238238
componentState.SetDirectParameters(initialParameters);
239239

240-
try
240+
await WaitForQuiescence();
241+
Debug.Assert(_pendingTasks == null);
242+
}
243+
244+
/// <summary>
245+
/// Removes the specified component from the renderer, causing the component and its
246+
/// descendants to be disposed.
247+
/// </summary>
248+
/// <param name="componentId">The ID of the root component.</param>
249+
protected void RemoveRootComponent(int componentId)
250+
{
251+
Dispatcher.AssertAccess();
252+
253+
var rootComponentState = GetRequiredComponentState(componentId);
254+
if (rootComponentState.ParentComponentState is not null)
241255
{
242-
await ProcessAsynchronousWork();
243-
Debug.Assert(_pendingTasks.Count == 0);
256+
throw new InvalidOperationException("The specified component is not a root component");
244257
}
245-
finally
258+
259+
// This assumes there isn't currently a batch in progress, and will throw if there is.
260+
// Currently there's no known scenario where we need to support calling RemoveRootComponentAsync
261+
// during a batch, but if a scenario emerges we can add support.
262+
_batchBuilder.ComponentDisposalQueue.Enqueue(componentId);
263+
if (TestableMetadataUpdate.IsSupported)
246264
{
247-
_pendingTasks = null;
265+
_rootComponentsLatestParameters?.Remove(componentId);
248266
}
267+
268+
ProcessRenderQueue();
249269
}
250270

251271
/// <summary>
@@ -254,21 +274,43 @@ protected async Task RenderRootComponentAsync(int componentId, ParameterView ini
254274
/// <param name="exception">The <see cref="Exception"/>.</param>
255275
protected abstract void HandleException(Exception exception);
256276

257-
private async Task ProcessAsynchronousWork()
277+
private async Task WaitForQuiescence()
258278
{
259-
// Child components SetParametersAsync are stored in the queue of pending tasks,
260-
// which might trigger further renders.
261-
while (_pendingTasks.Count > 0)
279+
// If there's already a loop waiting for quiescence, just join it
280+
if (_ongoingQuiescenceTask is not null)
262281
{
263-
// Create a Task that represents the remaining ongoing work for the rendering process
264-
var pendingWork = Task.WhenAll(_pendingTasks);
282+
await _ongoingQuiescenceTask;
283+
return;
284+
}
265285

266-
// Clear all pending work.
267-
_pendingTasks.Clear();
286+
try
287+
{
288+
_ongoingQuiescenceTask = ProcessAsynchronousWork();
289+
await _ongoingQuiescenceTask;
290+
}
291+
finally
292+
{
293+
Debug.Assert(_pendingTasks.Count == 0);
294+
_pendingTasks = null;
295+
_ongoingQuiescenceTask = null;
296+
}
297+
298+
async Task ProcessAsynchronousWork()
299+
{
300+
// Child components SetParametersAsync are stored in the queue of pending tasks,
301+
// which might trigger further renders.
302+
while (_pendingTasks.Count > 0)
303+
{
304+
// Create a Task that represents the remaining ongoing work for the rendering process
305+
var pendingWork = Task.WhenAll(_pendingTasks);
306+
307+
// Clear all pending work.
308+
_pendingTasks.Clear();
268309

269-
// new work might be added before we check again as a result of waiting for all
270-
// the child components to finish executing SetParametersAsync
271-
await pendingWork;
310+
// new work might be added before we check again as a result of waiting for all
311+
// the child components to finish executing SetParametersAsync
312+
await pendingWork;
313+
}
272314
}
273315
}
274316

@@ -544,7 +586,17 @@ private void ProcessRenderQueue()
544586
{
545587
if (_batchBuilder.ComponentRenderQueue.Count == 0)
546588
{
547-
return;
589+
if (_batchBuilder.ComponentDisposalQueue.Count == 0)
590+
{
591+
// Nothing to do
592+
return;
593+
}
594+
else
595+
{
596+
// Normally we process the disposal queue after each component rendering step,
597+
// but in this case disposal is the only pending action so far
598+
ProcessDisposalQueueInExistingBatch();
599+
}
548600
}
549601

550602
// Process render queue until empty
@@ -714,9 +766,13 @@ private void RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
714766
HandleExceptionViaErrorBoundary(renderFragmentException, componentState);
715767
}
716768

717-
List<Exception> exceptions = null;
718-
719769
// Process disposal queue now in case it causes further component renders to be enqueued
770+
ProcessDisposalQueueInExistingBatch();
771+
}
772+
773+
private void ProcessDisposalQueueInExistingBatch()
774+
{
775+
List<Exception> exceptions = null;
720776
while (_batchBuilder.ComponentDisposalQueue.Count > 0)
721777
{
722778
var disposeComponentId = _batchBuilder.ComponentDisposalQueue.Dequeue();

src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ public async Task CreateBinder_DateTime()
396396
var expectedValue = new DateTime(2018, 3, 4, 1, 2, 3);
397397

398398
// Act
399-
await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(CultureInfo.InvariantCulture), });
399+
await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(CultureInfo.CurrentCulture), });
400400

401401
Assert.Equal(expectedValue, value);
402402
Assert.Equal(1, component.Count);
@@ -415,7 +415,7 @@ public async Task CreateBinder_NullableDateTime()
415415
var expectedValue = new DateTime(2018, 3, 4, 1, 2, 3);
416416

417417
// Act
418-
await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(CultureInfo.InvariantCulture), });
418+
await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(CultureInfo.CurrentCulture), });
419419

420420
Assert.Equal(expectedValue, value);
421421
Assert.Equal(1, component.Count);
@@ -474,7 +474,7 @@ public async Task CreateBinder_DateTimeOffset()
474474
var expectedValue = new DateTime(2018, 3, 4, 1, 2, 3);
475475

476476
// Act
477-
await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(CultureInfo.InvariantCulture), });
477+
await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(CultureInfo.CurrentCulture), });
478478

479479
Assert.Equal(expectedValue, value);
480480
Assert.Equal(1, component.Count);
@@ -493,7 +493,7 @@ public async Task CreateBinder_NullableDateTimeOffset()
493493
var expectedValue = new DateTime(2018, 3, 4, 1, 2, 3);
494494

495495
// Act
496-
await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(CultureInfo.InvariantCulture), });
496+
await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(CultureInfo.CurrentCulture), });
497497

498498
Assert.Equal(expectedValue, value);
499499
Assert.Equal(1, component.Count);

0 commit comments

Comments
 (0)