@@ -157,7 +157,7 @@ protected Task Invoke(Action workItem)
157157 protected Task InvokeAsync ( Func < Task > workItem )
158158 => _renderHandle . InvokeAsync ( workItem ) ;
159159
160- void IComponent . Init ( RenderHandle renderHandle )
160+ void IComponent . Configure ( RenderHandle renderHandle )
161161 {
162162 // This implicitly means a ComponentBase can only be associated with a single
163163 // 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)
174174 /// Method invoked to apply initial or updated parameters to the component.
175175 /// </summary>
176176 /// <param name="parameters">The parameters to apply.</param>
177- public virtual void SetParameters ( ParameterCollection parameters )
177+ public virtual Task SetParametersAsync ( ParameterCollection parameters )
178178 {
179179 parameters . SetParameterProperties ( this ) ;
180-
181180 if ( ! _hasCalledInit )
182181 {
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+ }
185200
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 ;
190202 }
203+ }
191204
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+ }
195222
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.
196230 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+ }
197277 }
198278
199279 private async void ContinueAfterLifecycleTask ( Task task )
@@ -260,19 +340,24 @@ void IHandleAfterRender.OnAfterRender()
260340 var onAfterRenderTask = OnAfterRenderAsync ( ) ;
261341 if ( onAfterRenderTask != null && onAfterRenderTask . Status != TaskStatus . RanToCompletion )
262342 {
263- onAfterRenderTask . ContinueWith ( task =>
264- {
265343 // Note that we don't call StateHasChanged to trigger a render after
266344 // handling this, because that would be an infinite loop. The only
267345 // reason we have OnAfterRenderAsync is so that the developer doesn't
268346 // have to use "async void" and do their own exception handling in
269347 // the case where they want to start an async task.
348+ var taskWithHandledException = HandleAfterRenderException ( onAfterRenderTask ) ;
349+ }
350+ }
270351
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 ) ;
276361 }
277362 }
278363 }
0 commit comments