@@ -35,7 +35,8 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable
35
35
private readonly Dictionary < ulong , ulong > _eventHandlerIdReplacements = new Dictionary < ulong , ulong > ( ) ;
36
36
private readonly ILogger < Renderer > _logger ;
37
37
private readonly ComponentFactory _componentFactory ;
38
- private List < ( ComponentState , ParameterView ) > ? _rootComponents ;
38
+ private Dictionary < int , ParameterView > ? _rootComponentsLatestParameters ;
39
+ private Task ? _ongoingQuiescenceTask ;
39
40
40
41
private int _nextComponentId ;
41
42
private bool _isBatchInProgress ;
@@ -134,17 +135,18 @@ private async void RenderRootComponentsOnHotReload()
134
135
135
136
await Dispatcher . InvokeAsync ( ( ) =>
136
137
{
137
- if ( _rootComponents is null )
138
+ if ( _rootComponentsLatestParameters is null )
138
139
{
139
140
return ;
140
141
}
141
142
142
143
IsRenderingOnMetadataUpdate = true ;
143
144
try
144
145
{
145
- foreach ( var ( componentState , initialParameters ) in _rootComponents )
146
+ foreach ( var ( componentId , parameters ) in _rootComponentsLatestParameters )
146
147
{
147
- componentState . SetDirectParameters ( initialParameters ) ;
148
+ var componentState = GetRequiredComponentState ( componentId ) ;
149
+ componentState . SetDirectParameters ( parameters ) ;
148
150
}
149
151
}
150
152
finally
@@ -199,53 +201,71 @@ protected Task RenderRootComponentAsync(int componentId)
199
201
}
200
202
201
203
/// <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.
207
211
/// </summary>
208
212
/// <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>
210
214
/// <remarks>
211
215
/// Rendering a root component is an asynchronous operation. Clients may choose to not await the returned task to
212
216
/// start, but not wait for the entire render to complete.
213
217
/// </remarks>
214
218
protected async Task RenderRootComponentAsync ( int componentId , ParameterView initialParameters )
215
219
{
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 ( ) ;
220
228
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.
229
229
var componentState = GetRequiredComponentState ( componentId ) ;
230
230
if ( TestableMetadataUpdate . IsSupported )
231
231
{
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.
233
233
// 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 ( ) ;
236
236
}
237
237
238
238
componentState . SetDirectParameters ( initialParameters ) ;
239
239
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 )
241
255
{
242
- await ProcessAsynchronousWork ( ) ;
243
- Debug . Assert ( _pendingTasks . Count == 0 ) ;
256
+ throw new InvalidOperationException ( "The specified component is not a root component" ) ;
244
257
}
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 )
246
264
{
247
- _pendingTasks = null ;
265
+ _rootComponentsLatestParameters ? . Remove ( componentId ) ;
248
266
}
267
+
268
+ ProcessRenderQueue ( ) ;
249
269
}
250
270
251
271
/// <summary>
@@ -254,21 +274,43 @@ protected async Task RenderRootComponentAsync(int componentId, ParameterView ini
254
274
/// <param name="exception">The <see cref="Exception"/>.</param>
255
275
protected abstract void HandleException ( Exception exception ) ;
256
276
257
- private async Task ProcessAsynchronousWork ( )
277
+ private async Task WaitForQuiescence ( )
258
278
{
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 )
262
281
{
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
+ }
265
285
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 ( ) ;
268
309
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
+ }
272
314
}
273
315
}
274
316
@@ -544,7 +586,17 @@ private void ProcessRenderQueue()
544
586
{
545
587
if ( _batchBuilder . ComponentRenderQueue . Count == 0 )
546
588
{
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
+ }
548
600
}
549
601
550
602
// Process render queue until empty
@@ -714,9 +766,13 @@ private void RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
714
766
HandleExceptionViaErrorBoundary ( renderFragmentException , componentState ) ;
715
767
}
716
768
717
- List < Exception > exceptions = null ;
718
-
719
769
// 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 ;
720
776
while ( _batchBuilder . ComponentDisposalQueue . Count > 0 )
721
777
{
722
778
var disposeComponentId = _batchBuilder . ComponentDisposalQueue . Dequeue ( ) ;
0 commit comments