Skip to content

Commit 41d8dda

Browse files
committed
Improve Components error handling
* Change event handlers IHandleEvent, IHandleAfterEvent to be async. * Return faulted tasks to Renderer instead of handling exceptions in ComponentBase * Use ILogger in RemoteRenderer, and log to console in WebAssemblyRenderer * Cleaning up touched files Fixes #4964
1 parent ef1fc9e commit 41d8dda

16 files changed

+694
-347
lines changed

src/Components/Blazor/Blazor/src/Rendering/WebAssemblyRenderer.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
5+
using System.Threading.Tasks;
46
using Microsoft.AspNetCore.Components;
57
using Microsoft.AspNetCore.Components.Browser;
68
using Microsoft.AspNetCore.Components.Rendering;
79
using Microsoft.JSInterop;
810
using Mono.WebAssembly.Interop;
9-
using System;
10-
using System.Threading.Tasks;
1111

1212
namespace Microsoft.AspNetCore.Blazor.Rendering
1313
{
@@ -31,9 +31,6 @@ public WebAssemblyRenderer(IServiceProvider serviceProvider): base(serviceProvid
3131
_webAssemblyRendererId = RendererRegistry.Current.Add(this);
3232
}
3333

34-
internal void DispatchBrowserEvent(int componentId, int eventHandlerId, UIEventArgs eventArgs)
35-
=> DispatchEvent(componentId, eventHandlerId, eventArgs);
36-
3734
/// <summary>
3835
/// Attaches a new root component to the renderer,
3936
/// causing it to be displayed in the specified DOM element.
@@ -95,5 +92,24 @@ protected override Task UpdateDisplayAsync(in RenderBatch batch)
9592
throw new NotImplementedException($"{nameof(WebAssemblyRenderer)} is supported only with in-process JS runtimes.");
9693
}
9794
}
95+
96+
/// <inheritdoc />
97+
protected override bool HandleException(int componentId, IComponent component, Exception exception)
98+
{
99+
Console.Error.WriteLine($"Unhandled exception executing component {component.GetType().FullName} ({componentId})");
100+
if (exception is AggregateException aggregateException)
101+
{
102+
foreach (var innerException in aggregateException.Flatten().InnerExceptions)
103+
{
104+
Console.Error.WriteLine(innerException);
105+
}
106+
}
107+
else
108+
{
109+
Console.Error.WriteLine(exception);
110+
}
111+
112+
return true;
113+
}
98114
}
99115
}

src/Components/Browser/src/RendererRegistryEventDispatcher.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
5+
using System.Threading.Tasks;
46
using Microsoft.AspNetCore.Components.Rendering;
57
using Microsoft.JSInterop;
6-
using System;
78

89
namespace Microsoft.AspNetCore.Components.Browser
910
{
@@ -16,12 +17,13 @@ public static class RendererRegistryEventDispatcher
1617
/// For framework use only.
1718
/// </summary>
1819
[JSInvokable(nameof(DispatchEvent))]
19-
public static void DispatchEvent(
20+
public static Task DispatchEvent(
2021
BrowserEventDescriptor eventDescriptor, string eventArgsJson)
2122
{
2223
var eventArgs = ParseEventArgsJson(eventDescriptor.EventArgsType, eventArgsJson);
2324
var renderer = RendererRegistry.Current.Find(eventDescriptor.BrowserRendererId);
24-
renderer.DispatchEvent(
25+
26+
return renderer.DispatchEventAsync(
2527
eventDescriptor.ComponentId,
2628
eventDescriptor.EventHandlerId,
2729
eventArgs);

src/Components/Components/src/ComponentBase.cs

Lines changed: 44 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public abstract class ComponentBase : IComponent, IHandleEvent, IHandleAfterRend
3232

3333
private readonly RenderFragment _renderFragment;
3434
private RenderHandle _renderHandle;
35-
private bool _hasCalledInit;
35+
private bool _initialized;
3636
private bool _hasNeverRendered = true;
3737
private bool _hasPendingQueuedRender;
3838

@@ -177,188 +177,93 @@ void IComponent.Configure(RenderHandle renderHandle)
177177
public virtual Task SetParametersAsync(ParameterCollection parameters)
178178
{
179179
parameters.SetParameterProperties(this);
180-
if (!_hasCalledInit)
180+
if (!_initialized)
181181
{
182-
return RunInitAndSetParameters();
182+
_initialized = true;
183+
184+
return RunInitAndSetParametersAsync();
183185
}
184186
else
185187
{
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-
}
200-
201-
return Task.CompletedTask;
188+
return SetParametersAsyncCore();
202189
}
203190
}
204191

205-
private async Task RunInitAndSetParameters()
192+
private async Task RunInitAndSetParametersAsync()
206193
{
207-
_hasCalledInit = true;
208-
var initIsAsync = false;
209-
210194
OnInit();
211-
Task initTask = null;
212-
(initIsAsync, initTask) = ProcessLifeCycletask(OnInitAsync());
213-
if (initIsAsync)
195+
var task = OnInitAsync();
196+
197+
if (task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled)
214198
{
215199
// Call state has changed here so that we render after the sync part of OnInitAsync has run
216200
// and wait for it to finish before we continue. If no async work has been done yet, we want
217201
// to defer calling StateHasChanged up until the first bit of async code happens or until
218-
// the end.
202+
// the end. Additionally, we want to avoid calling StateHasChanged if no
203+
// async work is to be performed.
219204
StateHasChanged();
220-
await initTask;
221-
}
222205

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.
230-
StateHasChanged();
231-
if (setParametersIsAsync)
232-
{
233-
await parametersTask;
206+
await ProcessLifecycleTaskAsync(task);
234207
}
208+
209+
await SetParametersAsyncCore();
235210
}
236211

237-
private (bool isAsync, Task asyncTask) ProcessLifeCycletask(Task task)
212+
private Task SetParametersAsyncCore()
238213
{
239-
if (task == null)
240-
{
241-
throw new ArgumentNullException(nameof(task));
242-
}
214+
OnParametersSet();
215+
var task = OnParametersSetAsync();
216+
// If no aync work is to be performed, i.e. the task has already ran to completion
217+
// or was canceled by the time we got to inspect it, avoid going async and re-invoking
218+
// StateHasChanged at the culmination of the async work.
219+
var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
220+
task.Status != TaskStatus.Canceled;
243221

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-
}
222+
// We always call StateHasChanged here as we want to trigger a rerender after OnParametersSet and
223+
// the synchronous part of OnParametersSetAsync has run.
224+
StateHasChanged();
225+
226+
return shouldAwaitTask ?
227+
ProcessLifecycleTaskAsync(task) :
228+
Task.CompletedTask;
259229
}
260230

261-
private async Task ReRenderAsyncTask(Task task)
231+
private async Task ProcessLifecycleTaskAsync(Task task)
262232
{
263233
try
264234
{
265235
await task;
266-
StateHasChanged();
267236
}
268-
catch (Exception ex)
237+
catch when (task.IsCanceled)
269238
{
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-
}
277-
}
278-
279-
private async void ContinueAfterLifecycleTask(Task task)
280-
{
281-
switch (task == null ? TaskStatus.RanToCompletion : task.Status)
282-
{
283-
// If it's already completed synchronously, no need to await and no
284-
// need to issue a further render (we already rerender synchronously).
285-
// Just need to make sure we propagate any errors.
286-
case TaskStatus.RanToCompletion:
287-
case TaskStatus.Canceled:
288-
break;
289-
case TaskStatus.Faulted:
290-
HandleException(task.Exception);
291-
break;
292-
293-
// For incomplete tasks, automatically re-render on successful completion
294-
default:
295-
try
296-
{
297-
await task;
298-
StateHasChanged();
299-
}
300-
catch (Exception ex)
301-
{
302-
// Either the task failed, or it was cancelled, or StateHasChanged threw.
303-
// We want to report task failure or StateHasChanged exceptions only.
304-
if (!task.IsCanceled)
305-
{
306-
HandleException(ex);
307-
}
308-
}
309-
310-
break;
311-
}
312-
}
313-
314-
private static void HandleException(Exception ex)
315-
{
316-
if (ex is AggregateException && ex.InnerException != null)
317-
{
318-
ex = ex.InnerException; // It's more useful
239+
// Ignore task cancelation but don't bother issuing a state change.
240+
return;
319241
}
320242

321-
// TODO: Need better global exception handling
322-
Console.Error.WriteLine($"[{ex.GetType().FullName}] {ex.Message}\n{ex.StackTrace}");
243+
StateHasChanged();
323244
}
324245

325-
void IHandleEvent.HandleEvent(EventHandlerInvoker binding, UIEventArgs args)
246+
async Task IHandleEvent.HandleEventAsync(EventHandlerInvoker binding, UIEventArgs args)
326247
{
327-
var task = binding.Invoke(args);
328-
ContinueAfterLifecycleTask(task);
248+
await binding.Invoke(args);
329249

330250
// After each event, we synchronously re-render (unless !ShouldRender())
331251
// This just saves the developer the trouble of putting "StateHasChanged();"
332252
// at the end of every event callback.
333253
StateHasChanged();
334254
}
335255

336-
void IHandleAfterRender.OnAfterRender()
256+
Task IHandleAfterRender.OnAfterRenderAsync()
337257
{
338258
OnAfterRender();
339259

340-
var onAfterRenderTask = OnAfterRenderAsync();
341-
if (onAfterRenderTask != null && onAfterRenderTask.Status != TaskStatus.RanToCompletion)
342-
{
343-
// Note that we don't call StateHasChanged to trigger a render after
344-
// handling this, because that would be an infinite loop. The only
345-
// reason we have OnAfterRenderAsync is so that the developer doesn't
346-
// have to use "async void" and do their own exception handling in
347-
// the case where they want to start an async task.
348-
var taskWithHandledException = HandleAfterRenderException(onAfterRenderTask);
349-
}
350-
}
260+
return OnAfterRenderAsync();
351261

352-
private async Task HandleAfterRenderException(Task parentTask)
353-
{
354-
try
355-
{
356-
await parentTask;
357-
}
358-
catch (Exception e)
359-
{
360-
HandleException(e);
361-
}
262+
// Note that we don't call StateHasChanged to trigger a render after
263+
// handling this, because that would be an infinite loop. The only
264+
// reason we have OnAfterRenderAsync is so that the developer doesn't
265+
// have to use "async void" and do their own exception handling in
266+
// the case where they want to start an async task.
362267
}
363268
}
364269
}

src/Components/Components/src/IHandleAfterRender.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System.Threading.Tasks;
5+
46
namespace Microsoft.AspNetCore.Components
57
{
68
/// <summary>
@@ -11,6 +13,7 @@ public interface IHandleAfterRender
1113
/// <summary>
1214
/// Notifies the component that it has been rendered.
1315
/// </summary>
14-
void OnAfterRender();
16+
/// <returns>A <see cref="Task"/> that represents the asynchronous event handling operation.</returns>
17+
Task OnAfterRenderAsync();
1518
}
1619
}

src/Components/Components/src/IHandleEvent.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System.Threading.Tasks;
5+
46
namespace Microsoft.AspNetCore.Components
57
{
68
/// <summary>
@@ -13,6 +15,7 @@ public interface IHandleEvent
1315
/// </summary>
1416
/// <param name="binding">The event binding.</param>
1517
/// <param name="args">Arguments for the event handler.</param>
16-
void HandleEvent(EventHandlerInvoker binding, UIEventArgs args);
18+
/// <returns>A <see cref="Task"/> that represents the asynchronous event handling operation.</returns>
19+
Task HandleEventAsync(EventHandlerInvoker binding, UIEventArgs args);
1720
}
1821
}

0 commit comments

Comments
 (0)