@@ -157,7 +157,7 @@ protected Task Invoke(Action workItem)
157
157
protected Task InvokeAsync ( Func < Task > workItem )
158
158
=> _renderHandle . InvokeAsync ( workItem ) ;
159
159
160
- void IComponent . Init ( RenderHandle renderHandle )
160
+ void IComponent . Configure ( RenderHandle renderHandle )
161
161
{
162
162
// This implicitly means a ComponentBase can only be associated with a single
163
163
// renderer. That's the only use case we have right now. If there was ever a need,
@@ -174,26 +174,106 @@ void IComponent.Init(RenderHandle renderHandle)
174
174
/// Method invoked to apply initial or updated parameters to the component.
175
175
/// </summary>
176
176
/// <param name="parameters">The parameters to apply.</param>
177
- public virtual void SetParameters ( ParameterCollection parameters )
177
+ public virtual Task SetParametersAsync ( ParameterCollection parameters )
178
178
{
179
179
parameters . SetParameterProperties ( this ) ;
180
-
181
180
if ( ! _hasCalledInit )
182
181
{
183
- _hasCalledInit = true ;
184
- OnInit ( ) ;
182
+ return RunInitAndSetParameters ( ) ;
183
+ }
184
+ else
185
+ {
186
+ OnParametersSet ( ) ;
187
+ // If you override OnInitAsync or OnParametersSetAsync and return a noncompleted task,
188
+ // then by default we automatically re-render once each of those tasks completes.
189
+ var isAsync = false ;
190
+ Task parametersTask = null ;
191
+ ( isAsync , parametersTask ) = ProcessLifeCycletask ( OnParametersSetAsync ( ) ) ;
192
+ StateHasChanged ( ) ;
193
+ // We call StateHasChanged here so that we render after OnParametersSet and after the
194
+ // synchronous part of OnParametersSetAsync has run, and in case there is async work
195
+ // we trigger another render.
196
+ if ( isAsync )
197
+ {
198
+ return parametersTask ;
199
+ }
185
200
186
- // If you override OnInitAsync and return a noncompleted task, then by default
187
- // we automatically re-render once that task completes.
188
- var initTask = OnInitAsync ( ) ;
189
- ContinueAfterLifecycleTask ( initTask ) ;
201
+ return Task . CompletedTask ;
190
202
}
203
+ }
191
204
192
- OnParametersSet ( ) ;
193
- var parametersTask = OnParametersSetAsync ( ) ;
194
- ContinueAfterLifecycleTask ( parametersTask ) ;
205
+ private async Task RunInitAndSetParameters ( )
206
+ {
207
+ _hasCalledInit = true ;
208
+ var initIsAsync = false ;
209
+
210
+ OnInit ( ) ;
211
+ Task initTask = null ;
212
+ ( initIsAsync , initTask ) = ProcessLifeCycletask ( OnInitAsync ( ) ) ;
213
+ if ( initIsAsync )
214
+ {
215
+ // Call state has changed here so that we render after the sync part of OnInitAsync has run
216
+ // and wait for it to finish before we continue. If no async work has been done yet, we want
217
+ // to defer calling StateHasChanged up until the first bit of async code happens or until
218
+ // the end.
219
+ StateHasChanged ( ) ;
220
+ await initTask ;
221
+ }
195
222
223
+ OnParametersSet ( ) ;
224
+ Task parametersTask = null ;
225
+ var setParametersIsAsync = false ;
226
+ ( setParametersIsAsync , parametersTask ) = ProcessLifeCycletask ( OnParametersSetAsync ( ) ) ;
227
+ // We always call StateHasChanged here as we want to trigger a rerender after OnParametersSet and
228
+ // the synchronous part of OnParametersSetAsync has run, triggering another re-render in case there
229
+ // is additional async work.
196
230
StateHasChanged ( ) ;
231
+ if ( setParametersIsAsync )
232
+ {
233
+ await parametersTask ;
234
+ }
235
+ }
236
+
237
+ private ( bool isAsync , Task asyncTask ) ProcessLifeCycletask ( Task task )
238
+ {
239
+ if ( task == null )
240
+ {
241
+ throw new ArgumentNullException ( nameof ( task ) ) ;
242
+ }
243
+
244
+ switch ( task . Status )
245
+ {
246
+ // If it's already completed synchronously, no need to await and no
247
+ // need to issue a further render (we already rerender synchronously).
248
+ // Just need to make sure we propagate any errors.
249
+ case TaskStatus . RanToCompletion :
250
+ case TaskStatus . Canceled :
251
+ return ( false , null ) ;
252
+ case TaskStatus . Faulted :
253
+ HandleException ( task . Exception ) ;
254
+ return ( false , null ) ;
255
+ // For incomplete tasks, automatically re-render on successful completion
256
+ default :
257
+ return ( true , ReRenderAsyncTask ( task ) ) ;
258
+ }
259
+ }
260
+
261
+ private async Task ReRenderAsyncTask ( Task task )
262
+ {
263
+ try
264
+ {
265
+ await task ;
266
+ StateHasChanged ( ) ;
267
+ }
268
+ catch ( Exception ex )
269
+ {
270
+ // Either the task failed, or it was cancelled, or StateHasChanged threw.
271
+ // We want to report task failure or StateHasChanged exceptions only.
272
+ if ( ! task . IsCanceled )
273
+ {
274
+ HandleException ( ex ) ;
275
+ }
276
+ }
197
277
}
198
278
199
279
private async void ContinueAfterLifecycleTask ( Task task )
@@ -260,19 +340,24 @@ void IHandleAfterRender.OnAfterRender()
260
340
var onAfterRenderTask = OnAfterRenderAsync ( ) ;
261
341
if ( onAfterRenderTask != null && onAfterRenderTask . Status != TaskStatus . RanToCompletion )
262
342
{
263
- onAfterRenderTask . ContinueWith ( task =>
264
- {
265
343
// Note that we don't call StateHasChanged to trigger a render after
266
344
// handling this, because that would be an infinite loop. The only
267
345
// reason we have OnAfterRenderAsync is so that the developer doesn't
268
346
// have to use "async void" and do their own exception handling in
269
347
// the case where they want to start an async task.
348
+ var taskWithHandledException = HandleAfterRenderException ( onAfterRenderTask ) ;
349
+ }
350
+ }
270
351
271
- if ( task . Exception != null )
272
- {
273
- HandleException ( task . Exception ) ;
274
- }
275
- } ) ;
352
+ private async Task HandleAfterRenderException ( Task parentTask )
353
+ {
354
+ try
355
+ {
356
+ await parentTask ;
357
+ }
358
+ catch ( Exception e )
359
+ {
360
+ HandleException ( e ) ;
276
361
}
277
362
}
278
363
}
0 commit comments