Improve Components error handling (#7165)

* 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 https://github.com/aspnet/AspNetCore/issues/4964
This commit is contained in:
Pranav K 2019-02-13 14:22:46 -08:00 committed by GitHub
parent 758ba235fa
commit cddbc2e888
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1088 additions and 498 deletions

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Blazor.Rendering;
using Microsoft.AspNetCore.Components.Builder;
@ -35,13 +36,13 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
Entries.Add((componentType, domElementSelector));
}
public WebAssemblyRenderer CreateRenderer()
public async Task<WebAssemblyRenderer> CreateRendererAsync()
{
var renderer = new WebAssemblyRenderer(Services);
for (var i = 0; i < Entries.Count; i++)
{
var entry = Entries[i];
renderer.AddComponent(entry.componentType, entry.domElementSelector);
var (componentType, domElementSelector) = Entries[i];
await renderer.AddComponentAsync(componentType, domElementSelector);
}
return renderer;

View File

@ -42,6 +42,11 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
JSRuntime.SetCurrentJSRuntime(_runtime);
SetBrowserHttpMessageHandlerAsDefault();
return StartAsyncAwaited();
}
private async Task StartAsyncAwaited()
{
var scopeFactory = Services.GetRequiredService<IServiceScopeFactory>();
_scope = scopeFactory.CreateScope();
@ -61,7 +66,7 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
var builder = new WebAssemblyBlazorApplicationBuilder(_scope.ServiceProvider);
startup.Configure(builder, _scope.ServiceProvider);
_renderer = builder.CreateRenderer();
_renderer = await builder.CreateRendererAsync();
}
catch
{
@ -76,9 +81,6 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
throw;
}
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken = default)

View File

@ -1,14 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Browser;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.JSInterop;
using Mono.WebAssembly.Interop;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Blazor.Rendering
{
@ -24,7 +23,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
/// Constructs an instance of <see cref="WebAssemblyRenderer"/>.
/// </summary>
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> to use when initializing components.</param>
public WebAssemblyRenderer(IServiceProvider serviceProvider): base(serviceProvider)
public WebAssemblyRenderer(IServiceProvider serviceProvider) : base(serviceProvider)
{
// The browser renderer registers and unregisters itself with the static
// registry. This works well with the WebAssembly runtime, and is simple for the
@ -38,11 +37,13 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
/// </summary>
/// <typeparam name="TComponent">The type of the component.</typeparam>
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
public void AddComponent<TComponent>(string domElementSelector)
where TComponent: IComponent
{
AddComponent(typeof(TComponent), domElementSelector);
}
/// <returns>A <see cref="Task"/> that represents the asynchronous rendering of the added component.</returns>
/// <remarks>
/// Callers of this method may choose to ignore the returned <see cref="Task"/> if they do not
/// want to await the rendering of the added component.
/// </remarks>
public Task AddComponentAsync<TComponent>(string domElementSelector) where TComponent : IComponent
=> AddComponentAsync(typeof(TComponent), domElementSelector);
/// <summary>
/// Associates the <see cref="IComponent"/> with the <see cref="WebAssemblyRenderer"/>,
@ -50,7 +51,12 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
/// </summary>
/// <param name="componentType">The type of the component.</param>
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
public void AddComponent(Type componentType, string domElementSelector)
/// <returns>A <see cref="Task"/> that represents the asynchronous rendering of the added component.</returns>
/// <remarks>
/// Callers of this method may choose to ignore the returned <see cref="Task"/> if they do not
/// want to await the rendering of the added component.
/// </remarks>
public Task AddComponentAsync(Type componentType, string domElementSelector)
{
var component = InstantiateComponent(componentType);
var componentId = AssignRootComponentId(component);
@ -66,7 +72,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
domElementSelector,
componentId);
RenderRootComponent(componentId);
return RenderRootComponentAsync(componentId);
}
/// <inheritdoc />
@ -93,5 +99,22 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
throw new NotImplementedException($"{nameof(WebAssemblyRenderer)} is supported only with in-process JS runtimes.");
}
}
/// <inheritdoc />
protected override void HandleException(Exception exception)
{
Console.Error.WriteLine($"Unhandled exception rendering component:");
if (exception is AggregateException aggregateException)
{
foreach (var innerException in aggregateException.Flatten().InnerExceptions)
{
Console.Error.WriteLine(innerException);
}
}
else
{
Console.Error.WriteLine(exception);
}
}
}
}

View File

@ -443,6 +443,11 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
public void AttachComponent(IComponent component)
=> AssignRootComponentId(component);
protected override void HandleException(Exception exception)
{
throw new NotImplementedException();
}
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
{
LatestBatchReferenceFrames = renderBatch.ReferenceFrames.ToArray();

View File

@ -1546,14 +1546,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -1573,14 +1571,12 @@
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
@ -1697,8 +1693,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@ -1724,7 +1719,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -1732,14 +1726,12 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@ -1758,7 +1750,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}

View File

@ -1,9 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.JSInterop;
using System;
namespace Microsoft.AspNetCore.Components.Browser
{
@ -16,12 +17,13 @@ namespace Microsoft.AspNetCore.Components.Browser
/// For framework use only.
/// </summary>
[JSInvokable(nameof(DispatchEvent))]
public static void DispatchEvent(
public static Task DispatchEvent(
BrowserEventDescriptor eventDescriptor, string eventArgsJson)
{
var eventArgs = ParseEventArgsJson(eventDescriptor.EventArgsType, eventArgsJson);
var renderer = RendererRegistry.Current.Find(eventDescriptor.BrowserRendererId);
renderer.DispatchEvent(
return renderer.DispatchEventAsync(
eventDescriptor.ComponentId,
eventDescriptor.EventHandlerId,
eventArgs);

View File

@ -91,6 +91,11 @@ namespace Microsoft.AspNetCore.Components.Performance
{
}
protected override void HandleException(Exception exception)
{
throw new NotImplementedException();
}
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
=> Task.CompletedTask;
}

View File

@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Components
private readonly RenderFragment _renderFragment;
private RenderHandle _renderHandle;
private bool _hasCalledInit;
private bool _initialized;
private bool _hasNeverRendered = true;
private bool _hasPendingQueuedRender;
@ -177,188 +177,111 @@ namespace Microsoft.AspNetCore.Components
public virtual Task SetParametersAsync(ParameterCollection parameters)
{
parameters.SetParameterProperties(this);
if (!_hasCalledInit)
if (!_initialized)
{
return RunInitAndSetParameters();
_initialized = true;
return RunInitAndSetParametersAsync();
}
else
{
OnParametersSet();
// If you override OnInitAsync or OnParametersSetAsync and return a noncompleted task,
// then by default we automatically re-render once each of those tasks completes.
var isAsync = false;
Task parametersTask = null;
(isAsync, parametersTask) = ProcessLifeCycletask(OnParametersSetAsync());
StateHasChanged();
// We call StateHasChanged here so that we render after OnParametersSet and after the
// synchronous part of OnParametersSetAsync has run, and in case there is async work
// we trigger another render.
if (isAsync)
{
return parametersTask;
}
return Task.CompletedTask;
return CallOnParametersSetAsync();
}
}
private async Task RunInitAndSetParameters()
private async Task RunInitAndSetParametersAsync()
{
_hasCalledInit = true;
var initIsAsync = false;
OnInit();
Task initTask = null;
(initIsAsync, initTask) = ProcessLifeCycletask(OnInitAsync());
if (initIsAsync)
var task = OnInitAsync();
if (task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled)
{
// Call state has changed here so that we render after the sync part of OnInitAsync has run
// and wait for it to finish before we continue. If no async work has been done yet, we want
// to defer calling StateHasChanged up until the first bit of async code happens or until
// the end.
// the end. Additionally, we want to avoid calling StateHasChanged if no
// async work is to be performed.
StateHasChanged();
await initTask;
try
{
await task;
}
catch when (task.IsCanceled)
{
// Ignore exceptions from task cancelletions.
// Awaiting a canceled task may produce either an OperationCanceledException (if produced as a consequence of
// CancellationToken.ThrowIfCancellationRequested()) or a TaskCanceledException (produced as a consequence of awaiting Task.FromCanceled).
// It's much easier to check the state of the Task (i.e. Task.IsCanceled) rather than catch two distinct exceptions.
}
// Don't call StateHasChanged here. CallOnParametersSetAsync should handle that for us.
}
OnParametersSet();
Task parametersTask = null;
var setParametersIsAsync = false;
(setParametersIsAsync, parametersTask) = ProcessLifeCycletask(OnParametersSetAsync());
// We always call StateHasChanged here as we want to trigger a rerender after OnParametersSet and
// the synchronous part of OnParametersSetAsync has run, triggering another re-render in case there
// is additional async work.
StateHasChanged();
if (setParametersIsAsync)
{
await parametersTask;
}
await CallOnParametersSetAsync();
}
private (bool isAsync, Task asyncTask) ProcessLifeCycletask(Task task)
private Task CallOnParametersSetAsync()
{
if (task == null)
{
throw new ArgumentNullException(nameof(task));
}
OnParametersSet();
var task = OnParametersSetAsync();
// If no async work is to be performed, i.e. the task has already ran to completion
// or was canceled by the time we got to inspect it, avoid going async and re-invoking
// StateHasChanged at the culmination of the async work.
var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
task.Status != TaskStatus.Canceled;
switch (task.Status)
{
// If it's already completed synchronously, no need to await and no
// need to issue a further render (we already rerender synchronously).
// Just need to make sure we propagate any errors.
case TaskStatus.RanToCompletion:
case TaskStatus.Canceled:
return (false, null);
case TaskStatus.Faulted:
HandleException(task.Exception);
return (false, null);
// For incomplete tasks, automatically re-render on successful completion
default:
return (true, ReRenderAsyncTask(task));
}
// We always call StateHasChanged here as we want to trigger a rerender after OnParametersSet and
// the synchronous part of OnParametersSetAsync has run.
StateHasChanged();
return shouldAwaitTask ?
CallStateHasChangedOnAsyncCompletion(task) :
Task.CompletedTask;
}
private async Task ReRenderAsyncTask(Task task)
private async Task CallStateHasChangedOnAsyncCompletion(Task task)
{
try
{
await task;
StateHasChanged();
}
catch (Exception ex)
catch when (task.IsCanceled)
{
// Either the task failed, or it was cancelled, or StateHasChanged threw.
// We want to report task failure or StateHasChanged exceptions only.
if (!task.IsCanceled)
{
HandleException(ex);
}
// Ignore exceptions from task cancelletions, but don't bother issuing a state change.
return;
}
StateHasChanged();
}
private async void ContinueAfterLifecycleTask(Task task)
{
switch (task == null ? TaskStatus.RanToCompletion : task.Status)
{
// If it's already completed synchronously, no need to await and no
// need to issue a further render (we already rerender synchronously).
// Just need to make sure we propagate any errors.
case TaskStatus.RanToCompletion:
case TaskStatus.Canceled:
break;
case TaskStatus.Faulted:
HandleException(task.Exception);
break;
// For incomplete tasks, automatically re-render on successful completion
default:
try
{
await task;
StateHasChanged();
}
catch (Exception ex)
{
// Either the task failed, or it was cancelled, or StateHasChanged threw.
// We want to report task failure or StateHasChanged exceptions only.
if (!task.IsCanceled)
{
HandleException(ex);
}
}
break;
}
}
private static void HandleException(Exception ex)
{
if (ex is AggregateException && ex.InnerException != null)
{
ex = ex.InnerException; // It's more useful
}
// TODO: Need better global exception handling
Console.Error.WriteLine($"[{ex.GetType().FullName}] {ex.Message}\n{ex.StackTrace}");
}
void IHandleEvent.HandleEvent(EventHandlerInvoker binding, UIEventArgs args)
Task IHandleEvent.HandleEventAsync(EventHandlerInvoker binding, UIEventArgs args)
{
var task = binding.Invoke(args);
ContinueAfterLifecycleTask(task);
var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
task.Status != TaskStatus.Canceled;
// After each event, we synchronously re-render (unless !ShouldRender())
// This just saves the developer the trouble of putting "StateHasChanged();"
// at the end of every event callback.
StateHasChanged();
return shouldAwaitTask ?
CallStateHasChangedOnAsyncCompletion(task) :
Task.CompletedTask;
}
void IHandleAfterRender.OnAfterRender()
Task IHandleAfterRender.OnAfterRenderAsync()
{
OnAfterRender();
var onAfterRenderTask = OnAfterRenderAsync();
if (onAfterRenderTask != null && onAfterRenderTask.Status != TaskStatus.RanToCompletion)
{
// Note that we don't call StateHasChanged to trigger a render after
// handling this, because that would be an infinite loop. The only
// reason we have OnAfterRenderAsync is so that the developer doesn't
// have to use "async void" and do their own exception handling in
// the case where they want to start an async task.
var taskWithHandledException = HandleAfterRenderException(onAfterRenderTask);
}
}
return OnAfterRenderAsync();
private async Task HandleAfterRenderException(Task parentTask)
{
try
{
await parentTask;
}
catch (Exception e)
{
HandleException(e);
}
// Note that we don't call StateHasChanged to trigger a render after
// handling this, because that would be an infinite loop. The only
// reason we have OnAfterRenderAsync is so that the developer doesn't
// have to use "async void" and do their own exception handling in
// the case where they want to start an async task.
}
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
@ -11,6 +13,7 @@ namespace Microsoft.AspNetCore.Components
/// <summary>
/// Notifies the component that it has been rendered.
/// </summary>
void OnAfterRender();
/// <returns>A <see cref="Task"/> that represents the asynchronous event handling operation.</returns>
Task OnAfterRenderAsync();
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
@ -13,6 +15,7 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
/// <param name="binding">The event binding.</param>
/// <param name="args">Arguments for the event handler.</param>
void HandleEvent(EventHandlerInvoker binding, UIEventArgs args);
/// <returns>A <see cref="Task"/> that represents the asynchronous event handling operation.</returns>
Task HandleEventAsync(EventHandlerInvoker binding, UIEventArgs args);
}
}

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;
namespace Microsoft.AspNetCore.Components.Rendering
@ -16,22 +15,13 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// </summary>
internal class ComponentState
{
private readonly int _componentId; // TODO: Change the type to 'long' when the Mono runtime has more complete support for passing longs in .NET->JS calls
private readonly ComponentState _parentComponentState;
private readonly IComponent _component;
private readonly Renderer _renderer;
private readonly IReadOnlyList<CascadingParameterState> _cascadingParameters;
private readonly bool _hasAnyCascadingParameterSubscriptions;
private RenderTreeBuilder _renderTreeBuilderCurrent;
private RenderTreeBuilder _renderTreeBuilderPrevious;
private ArrayBuilder<RenderTreeFrame> _latestDirectParametersSnapshot; // Lazily instantiated
private bool _componentWasDisposed;
public int ComponentId => _componentId;
public IComponent Component => _component;
public ComponentState ParentComponentState => _parentComponentState;
public RenderTreeBuilder CurrrentRenderTree => _renderTreeBuilderCurrent;
/// <summary>
/// Constructs an instance of <see cref="ComponentState"/>.
/// </summary>
@ -41,12 +31,12 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// <param name="parentComponentState">The <see cref="ComponentState"/> for the parent component, or null if this is a root component.</param>
public ComponentState(Renderer renderer, int componentId, IComponent component, ComponentState parentComponentState)
{
_componentId = componentId;
_parentComponentState = parentComponentState;
_component = component ?? throw new ArgumentNullException(nameof(component));
ComponentId = componentId;
ParentComponentState = parentComponentState;
Component = component ?? throw new ArgumentNullException(nameof(component));
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
_cascadingParameters = CascadingParameterState.FindCascadingParameters(this);
_renderTreeBuilderCurrent = new RenderTreeBuilder(renderer);
CurrrentRenderTree = new RenderTreeBuilder(renderer);
_renderTreeBuilderPrevious = new RenderTreeBuilder(renderer);
if (_cascadingParameters != null)
@ -55,6 +45,12 @@ namespace Microsoft.AspNetCore.Components.Rendering
}
}
// TODO: Change the type to 'long' when the Mono runtime has more complete support for passing longs in .NET->JS calls
public int ComponentId { get; }
public IComponent Component { get; }
public ComponentState ParentComponentState { get; }
public RenderTreeBuilder CurrrentRenderTree { get; private set; }
public void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment)
{
// A component might be in the render queue already before getting disposed by an
@ -65,31 +61,31 @@ namespace Microsoft.AspNetCore.Components.Rendering
}
// Swap the old and new tree builders
(_renderTreeBuilderCurrent, _renderTreeBuilderPrevious) = (_renderTreeBuilderPrevious, _renderTreeBuilderCurrent);
(CurrrentRenderTree, _renderTreeBuilderPrevious) = (_renderTreeBuilderPrevious, CurrrentRenderTree);
_renderTreeBuilderCurrent.Clear();
renderFragment(_renderTreeBuilderCurrent);
CurrrentRenderTree.Clear();
renderFragment(CurrrentRenderTree);
var diff = RenderTreeDiffBuilder.ComputeDiff(
_renderer,
batchBuilder,
_componentId,
ComponentId,
_renderTreeBuilderPrevious.GetFrames(),
_renderTreeBuilderCurrent.GetFrames());
CurrrentRenderTree.GetFrames());
batchBuilder.UpdatedComponentDiffs.Append(diff);
}
public void DisposeInBatch(RenderBatchBuilder batchBuilder)
{
_componentWasDisposed = true;
// TODO: Handle components throwing during dispose. Shouldn't break the whole render batch.
if (_component is IDisposable disposable)
if (Component is IDisposable disposable)
{
disposable.Dispose();
}
RenderTreeDiffBuilder.DisposeFrames(batchBuilder, _renderTreeBuilderCurrent.GetFrames());
RenderTreeDiffBuilder.DisposeFrames(batchBuilder, CurrrentRenderTree.GetFrames());
if (_hasAnyCascadingParameterSubscriptions)
{
@ -97,22 +93,29 @@ namespace Microsoft.AspNetCore.Components.Rendering
}
}
public void DispatchEvent(EventHandlerInvoker binding, UIEventArgs eventArgs)
public Task DispatchEventAsync(EventHandlerInvoker binding, UIEventArgs eventArgs)
{
if (_component is IHandleEvent handleEventComponent)
if (Component is IHandleEvent handleEventComponent)
{
handleEventComponent.HandleEvent(binding, eventArgs);
return handleEventComponent.HandleEventAsync(binding, eventArgs);
}
else
{
throw new InvalidOperationException(
$"The component of type {_component.GetType().FullName} cannot receive " +
$"The component of type {Component.GetType().FullName} cannot receive " +
$"events because it does not implement {typeof(IHandleEvent).FullName}.");
}
}
public void NotifyRenderCompleted()
=> (_component as IHandleAfterRender)?.OnAfterRender();
public Task NotifyRenderCompletedAsync()
{
if (Component is IHandleAfterRender handlerAfterRender)
{
return handlerAfterRender.OnAfterRenderAsync();
}
return Task.CompletedTask;
}
public void SetDirectParameters(ParameterCollection parameters)
{
@ -167,7 +170,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
hasSubscription = true;
}
}
return hasSubscription;
}

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.RenderTree;
@ -39,49 +40,13 @@ namespace Microsoft.AspNetCore.Components.Rendering
return Task.CompletedTask;
}
/// <summary>
/// Renders a component into a sequence of <see cref="string"/> fragments that represent the textual representation
/// of the HTML produced by the component.
/// </summary>
/// <typeparam name="T">The type of the <see cref="IComponent"/>.</typeparam>
/// <param name="initialParameters">A <see cref="ParameterCollection"/> with the initial parameters to render the component.</param>
/// <returns>A sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
public IEnumerable<string> RenderComponent<T>(ParameterCollection initialParameters) where T : IComponent
{
return RenderComponent(typeof(T), initialParameters);
}
/// <summary>
/// Renders a component into a sequence of <see cref="string"/> fragments that represent the textual representation
/// of the HTML produced by the component.
/// </summary>
/// <param name="componentType">The type of the <see cref="IComponent"/>.</param>
/// <param name="initialParameters">A <see cref="ParameterCollection"/> with the initial parameters to render the component.</param>
/// <returns>A sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
private IEnumerable<string> RenderComponent(Type componentType, ParameterCollection initialParameters)
{
var frames = CreateInitialRender(componentType, initialParameters);
if (frames.Count == 0)
{
return Array.Empty<string>();
}
else
{
var result = new List<string>();
var newPosition = RenderFrames(result, frames, 0, frames.Count);
Debug.Assert(newPosition == frames.Count);
return result;
}
}
/// <summary>
/// Renders a component into a sequence of <see cref="string"/> fragments that represent the textual representation
/// of the HTML produced by the component.
/// </summary>
/// <param name="componentType">The type of the <see cref="IComponent"/>.</param>
/// <param name="initialParameters">A <see cref="ParameterCollection"/> with the initial parameters to render the component.</param>
/// <returns>A sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
/// <returns>A <see cref="Task"/> that on completion returns a sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
public async Task<IEnumerable<string>> RenderComponentAsync(Type componentType, ParameterCollection initialParameters)
{
var frames = await CreateInitialRenderAsync(componentType, initialParameters);
@ -103,14 +68,18 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// Renders a component into a sequence of <see cref="string"/> fragments that represent the textual representation
/// of the HTML produced by the component.
/// </summary>
/// <typeparam name="T">The type of the <see cref="IComponent"/>.</typeparam>
/// <typeparam name="TComponent">The type of the <see cref="IComponent"/>.</typeparam>
/// <param name="initialParameters">A <see cref="ParameterCollection"/> with the initial parameters to render the component.</param>
/// <returns>A sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
public Task<IEnumerable<string>> RenderComponentAsync<T>(ParameterCollection initialParameters) where T : IComponent
/// <returns>A <see cref="Task"/> that on completion returns a sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
public Task<IEnumerable<string>> RenderComponentAsync<TComponent>(ParameterCollection initialParameters) where TComponent : IComponent
{
return RenderComponentAsync(typeof(T), initialParameters);
return RenderComponentAsync(typeof(TComponent), initialParameters);
}
/// <inheritdoc />
protected override void HandleException(Exception exception)
=> ExceptionDispatchInfo.Capture(exception).Throw();
private int RenderFrames(List<string> result, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
{
var nextPosition = position;
@ -258,16 +227,6 @@ namespace Microsoft.AspNetCore.Components.Rendering
return position + maxElements;
}
private ArrayRange<RenderTreeFrame> CreateInitialRender(Type componentType, ParameterCollection initialParameters)
{
var component = InstantiateComponent(componentType);
var componentId = AssignRootComponentId(component);
RenderRootComponent(componentId, initialParameters);
return GetCurrentRenderTreeFrames(componentId);
}
private async Task<ArrayRange<RenderTreeFrame>> CreateInitialRenderAsync(Type componentType, ParameterCollection initialParameters)
{
var component = InstantiateComponent(componentType);

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.RenderTree;
@ -100,80 +99,6 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// <returns>The <see cref="RenderTreeBuilder"/> representing the current render tree.</returns>
private protected ArrayRange<RenderTreeFrame> GetCurrentRenderTreeFrames(int componentId) => GetRequiredComponentState(componentId).CurrrentRenderTree.GetFrames();
/// <summary>
/// Performs the first render for a root component. After this, the root component
/// makes its own decisions about when to re-render, so there is no need to call
/// this more than once.
/// </summary>
/// <param name="componentId">The ID returned by <see cref="AssignRootComponentId(IComponent)"/>.</param>
protected void RenderRootComponent(int componentId)
{
RenderRootComponent(componentId, ParameterCollection.Empty);
}
/// <summary>
/// Performs the first render for a root component. After this, the root component
/// makes its own decisions about when to re-render, so there is no need to call
/// this more than once.
/// </summary>
/// <param name="componentId">The ID returned by <see cref="AssignRootComponentId(IComponent)"/>.</param>
/// <param name="initialParameters">The <see cref="ParameterCollection"/>with the initial parameters to use for rendering.</param>
protected void RenderRootComponent(int componentId, ParameterCollection initialParameters)
{
ReportAsyncExceptions(RenderRootComponentAsync(componentId, initialParameters));
}
private async void ReportAsyncExceptions(Task task)
{
switch (task.Status)
{
// If it's already completed synchronously, no need to await and no
// need to issue a further render (we already rerender synchronously).
// Just need to make sure we propagate any errors.
case TaskStatus.RanToCompletion:
case TaskStatus.Canceled:
_pendingTasks = null;
break;
case TaskStatus.Faulted:
_pendingTasks = null;
HandleException(task.Exception);
break;
default:
try
{
await task;
}
catch (Exception ex)
{
// Either the task failed, or it was cancelled.
// We want to report task failure exceptions only.
if (!task.IsCanceled)
{
HandleException(ex);
}
}
finally
{
// Clear the list after we are done rendering the root component or an async exception has ocurred.
_pendingTasks = null;
}
break;
}
}
private static void HandleException(Exception ex)
{
if (ex is AggregateException && ex.InnerException != null)
{
ex = ex.InnerException; // It's more useful
}
// TODO: Need better global exception handling
Console.Error.WriteLine($"[{ex.GetType().FullName}] {ex.Message}\n{ex.StackTrace}");
}
/// <summary>
/// Performs the first render for a root component, waiting for this component and all
/// children components to finish rendering in case there is any asynchronous work being
@ -182,6 +107,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// this more than once.
/// </summary>
/// <param name="componentId">The ID returned by <see cref="AssignRootComponentId(IComponent)"/>.</param>
/// <remarks>
/// Rendering a root component is an asynchronous operation. Clients may choose to not await the returned task to
/// start, but not wait for the entire render to complete.
/// </remarks>
protected Task RenderRootComponentAsync(int componentId)
{
return RenderRootComponentAsync(componentId, ParameterCollection.Empty);
@ -196,13 +125,17 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// </summary>
/// <param name="componentId">The ID returned by <see cref="AssignRootComponentId(IComponent)"/>.</param>
/// <param name="initialParameters">The <see cref="ParameterCollection"/>with the initial parameters to use for rendering.</param>
/// <remarks>
/// Rendering a root component is an asynchronous operation. Clients may choose to not await the returned task to
/// start, but not wait for the entire render to complete.
/// </remarks>
protected async Task RenderRootComponentAsync(int componentId, ParameterCollection initialParameters)
{
if (_pendingTasks != null)
if (Interlocked.CompareExchange(ref _pendingTasks, new List<Task>(), null) != null)
{
throw new InvalidOperationException("There is an ongoing rendering in progress.");
}
_pendingTasks = new List<Task>();
// During the rendering process we keep a list of components performing work in _pendingTasks.
// _renderer.AddToPendingTasks will be called by ComponentState.SetDirectParameters to add the
// the Task produced by Component.SetParametersAsync to _pendingTasks in order to track the
@ -211,8 +144,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
// work to finish as it will simply trigger new renders that will be handled afterwards.
// During the asynchronous rendering process we want to wait up untill al components have
// finished rendering so that we can produce the complete output.
GetRequiredComponentState(componentId)
.SetDirectParameters(initialParameters);
var componentState = GetRequiredComponentState(componentId);
componentState.SetDirectParameters(initialParameters);
try
{
@ -225,15 +158,20 @@ namespace Microsoft.AspNetCore.Components.Rendering
}
}
/// <summary>
/// Allows derived types to handle exceptions during rendering. Defaults to rethrowing the original exception.
/// </summary>
/// <param name="exception">The <see cref="Exception"/>.</param>
protected abstract void HandleException(Exception exception);
private async Task ProcessAsynchronousWork()
{
// Child components SetParametersAsync are stored in the queue of pending tasks,
// which might trigger further renders.
while (_pendingTasks.Count > 0)
{
Task pendingWork;
// Create a Task that represents the remaining ongoing work for the rendering process
pendingWork = Task.WhenAll(_pendingTasks);
var pendingWork = Task.WhenAll(_pendingTasks);
// Clear all pending work.
_pendingTasks.Clear();
@ -241,7 +179,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
// new work might be added before we check again as a result of waiting for all
// the child components to finish executing SetParametersAsync
await pendingWork;
};
}
}
private ComponentState AttachAndInitComponent(IComponent component, int parentComponentId)
@ -267,7 +205,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// <param name="componentId">The unique identifier for the component within the scope of this <see cref="Renderer"/>.</param>
/// <param name="eventHandlerId">The <see cref="RenderTreeFrame.AttributeEventHandlerId"/> value from the original event attribute.</param>
/// <param name="eventArgs">Arguments to be passed to the event handler.</param>
public void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs eventArgs)
/// <returns>A <see cref="Task"/> representing the asynchronous execution operation.</returns>
public Task DispatchEventAsync(int componentId, int eventHandlerId, UIEventArgs eventArgs)
{
EnsureSynchronizationContext();
@ -275,16 +214,20 @@ namespace Microsoft.AspNetCore.Components.Rendering
{
// The event handler might request multiple renders in sequence. Capture them
// all in a single batch.
var componentState = GetRequiredComponentState(componentId);
Task task = null;
try
{
_isBatchInProgress = true;
GetRequiredComponentState(componentId).DispatchEvent(binding, eventArgs);
task = componentState.DispatchEventAsync(binding, eventArgs);
}
finally
{
_isBatchInProgress = false;
ProcessRenderQueue();
}
return GetErrorHandledTask(task);
}
else
{
@ -329,8 +272,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
// This is for example when we run on a system with a single thread, like WebAssembly.
if (_dispatcher == null)
{
workItem();
return Task.CompletedTask;
return workItem();
}
if (SynchronizationContext.Current == _dispatcher)
@ -373,12 +315,12 @@ namespace Microsoft.AspNetCore.Components.Rendering
case TaskStatus.Canceled:
break;
case TaskStatus.Faulted:
// We want to throw immediately if the task failed synchronously instead of
// We want to immediately handle exceptions if the task failed synchronously instead of
// waiting for it to throw later. This can happen if the task is produced by
// an 'async' state machine (the ones generated using async/await) where even
// the synchronous exceptions will get captured and converted into a faulted
// task.
ExceptionDispatchInfo.Capture(task.Exception.InnerException).Throw();
HandleException(task.Exception.GetBaseException());
break;
default:
// We are not in rendering the root component.
@ -386,7 +328,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
{
return;
}
_pendingTasks.Add(task);
_pendingTasks.Add(GetErrorHandledTask(task));
break;
}
}
@ -472,7 +415,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
var batch = _batchBuilder.ToBatch();
updateDisplayTask = UpdateDisplayAsync(batch);
InvokeRenderCompletedCalls(batch.UpdatedComponents);
// Fire off the execution of OnAfterRenderAsync, but don't wait for it
// if there is async work to be done.
_ = InvokeRenderCompletedCalls(batch.UpdatedComponents);
}
finally
{
@ -482,15 +428,45 @@ namespace Microsoft.AspNetCore.Components.Rendering
}
}
private void InvokeRenderCompletedCalls(ArrayRange<RenderTreeDiff> updatedComponents)
private Task InvokeRenderCompletedCalls(ArrayRange<RenderTreeDiff> updatedComponents)
{
List<Task> batch = null;
var array = updatedComponents.Array;
for (var i = 0; i < updatedComponents.Count; i++)
{
// The component might be rendered and disposed in the same batch (if its parent
// was rendered later in the batch, and removed the child from the tree).
GetOptionalComponentState(array[i].ComponentId)?.NotifyRenderCompleted();
var componentState = GetOptionalComponentState(array[i].ComponentId);
if (componentState != null)
{
// The component might be rendered and disposed in the same batch (if its parent
// was rendered later in the batch, and removed the child from the tree).
var task = componentState.NotifyRenderCompletedAsync();
// We want to avoid allocations per rendering. Avoid allocating a state machine or an accumulator
// unless we absolutely have to.
if (task.IsCompleted)
{
if (task.Status == TaskStatus.RanToCompletion || task.Status == TaskStatus.Canceled)
{
// Nothing to do here.
continue;
}
else if (task.Status == TaskStatus.Faulted)
{
HandleException(task.Exception);
continue;
}
}
// The Task is incomplete.
// Queue up the task and we can inspect it later.
batch = batch ?? new List<Task>();
batch.Add(GetErrorHandledTask(task));
}
}
return batch != null ?
Task.WhenAll(batch) :
Task.CompletedTask;
}
private void RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
@ -536,14 +512,28 @@ namespace Microsoft.AspNetCore.Components.Rendering
}
}
private async Task GetErrorHandledTask(Task taskToHandle)
{
try
{
await taskToHandle;
}
catch (Exception ex)
{
if (!taskToHandle.IsCanceled)
{
// Ignore errors due to task cancellations.
HandleException(ex);
}
}
}
/// <summary>
/// Releases all resources currently used by this <see cref="Renderer"/> instance.
/// </summary>
/// <param name="disposing"><see langword="true"/> if this method is being invoked by <see cref="IDisposable.Dispose"/>, otherwise <see langword="false"/>.</param>
protected virtual void Dispose(bool disposing)
{
List<Exception> exceptions = null;
foreach (var componentState in _componentStateById.Values)
{
if (componentState.Component is IDisposable disposable)
@ -554,17 +544,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
}
catch (Exception exception)
{
// Capture exceptions thrown by individual components and rethrow as an aggregate.
exceptions = exceptions ?? new List<Exception>();
exceptions.Add(exception);
HandleException(exception);
}
}
}
if (exceptions != null)
{
throw new AggregateException(exceptions);
}
}
/// <summary>

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
@ -18,7 +19,7 @@ namespace Microsoft.AspNetCore.Components.Test
var renderer = new TestRenderer();
var component = new TestComponent();
int onInitRuns = 0;
var onInitRuns = 0;
component.OnInitLogic = c => onInitRuns++;
// Act
@ -36,7 +37,7 @@ namespace Microsoft.AspNetCore.Components.Test
var renderer = new TestRenderer();
var component = new TestComponent();
int onInitAsyncRuns = 0;
var onInitAsyncRuns = 0;
component.RunsBaseOnInitAsync = false;
component.OnInitAsyncLogic = c =>
{
@ -60,7 +61,7 @@ namespace Microsoft.AspNetCore.Components.Test
var renderer = new TestRenderer();
var component = new TestComponent();
int onInitAsyncRuns = 0;
var onInitAsyncRuns = 0;
component.RunsBaseOnInitAsync = true;
component.OnInitAsyncLogic = c =>
{
@ -84,7 +85,7 @@ namespace Microsoft.AspNetCore.Components.Test
var renderer = new TestRenderer();
var component = new TestComponent();
int onParametersSetRuns = 0;
var onParametersSetRuns = 0;
component.OnParametersSetLogic = c => onParametersSetRuns++;
// Act
@ -103,7 +104,7 @@ namespace Microsoft.AspNetCore.Components.Test
var renderer = new TestRenderer();
var component = new TestComponent();
int onParametersSetAsyncRuns = 0;
var onParametersSetAsyncRuns = 0;
component.RunsBaseOnParametersSetAsync = false;
component.OnParametersSetAsyncLogic = c =>
{
@ -127,7 +128,7 @@ namespace Microsoft.AspNetCore.Components.Test
var renderer = new TestRenderer();
var component = new TestComponent();
int onParametersSetAsyncRuns = 0;
var onParametersSetAsyncRuns = 0;
component.RunsBaseOnParametersSetAsync = true;
component.OnParametersSetAsyncLogic = c =>
{
@ -145,7 +146,7 @@ namespace Microsoft.AspNetCore.Components.Test
}
[Fact]
public void RendersAfterParametersSetAsyncTaskIsCompleted()
public async Task RendersAfterParametersSetAsyncTaskIsCompleted()
{
// Arrange
var renderer = new TestRenderer();
@ -158,7 +159,7 @@ namespace Microsoft.AspNetCore.Components.Test
// Act
var componentId = renderer.AssignRootComponentId(component);
renderer.RenderRootComponent(componentId);
var renderTask = renderer.RenderRootComponentAsync(componentId);
// Assert
Assert.Single(renderer.Batches);
@ -167,12 +168,14 @@ namespace Microsoft.AspNetCore.Components.Test
component.Counter = 2;
parametersSetTask.SetResult(true);
await renderTask;
// Component should be rendered again
Assert.Equal(2, renderer.Batches.Count);
}
[Fact]
public void RendersAfterParametersSetAndInitAsyncTasksAreCompleted()
public async Task RendersAfterParametersSetAndInitAsyncTasksAreCompleted()
{
// Arrange
var renderer = new TestRenderer();
@ -188,31 +191,32 @@ namespace Microsoft.AspNetCore.Components.Test
// Act
var componentId = renderer.AssignRootComponentId(component);
renderer.RenderRootComponent(componentId);
var renderTask = renderer.RenderRootComponentAsync(componentId);
// Assert
// A rendering should have happened after the synchronous execution of Init
Assert.Single(renderer.Batches);
// Completes task started by OnInitAsync
component.Counter = 2;
initTask.SetResult(true);
// Component should be rendered again 2 times
// after on init async
// after set parameters
Assert.Equal(3, renderer.Batches.Count);
// Component should be rendered once, after set parameters
Assert.Equal(2, renderer.Batches.Count);
// Completes task started by OnParametersSetAsync
component.Counter = 3;
parametersSetTask.SetResult(false);
await renderTask;
// Component should be rendered again
// after the async part of onparameterssetasync completes
Assert.Equal(4, renderer.Batches.Count);
Assert.Equal(3, renderer.Batches.Count);
}
[Fact]
public void DoesNotRenderAfterOnInitAsyncTaskIsCancelled()
public async Task DoesNotRenderAfterOnInitAsyncTaskIsCancelled()
{
// Arrange
var renderer = new TestRenderer();
@ -222,22 +226,49 @@ namespace Microsoft.AspNetCore.Components.Test
// Act
var componentId = renderer.AssignRootComponentId(component);
renderer.RenderRootComponent(componentId);
var renderTask = renderer.RenderRootComponentAsync(componentId);
// Assert
Assert.False(renderTask.IsCompleted);
Assert.Single(renderer.Batches);
// Cancel task started by OnInitAsync
component.Counter = 2;
initTask.SetCanceled();
await renderTask;
// Component should only be rendered again due to
// the call to StateHasChanged after SetParametersAsync
Assert.Equal(2,renderer.Batches.Count);
Assert.Equal(2, renderer.Batches.Count);
}
[Fact]
public void DoesNotRenderAfterOnParametersSetAsyncTaskIsCancelled()
public async Task DoesNotRenderAfterOnInitAsyncTaskIsCancelledUsingCancellationToken()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent() { Counter = 1 };
var cts = new CancellationTokenSource();
cts.Cancel();
component.OnInitAsyncLogic = async _ =>
{
await Task.Yield();
cts.Token.ThrowIfCancellationRequested();
};
// Act
var componentId = renderer.AssignRootComponentId(component);
await renderer.RenderRootComponentAsync(componentId);
// Assert
// At least one call to StateHasChanged depending on how OnInitAsyncLogic gets scheduled.
Assert.NotEmpty(renderer.Batches);
}
[Fact]
public async Task DoesNotRenderAfterOnParametersSetAsyncTaskIsCanceled()
{
// Arrange
var renderer = new TestRenderer();
@ -247,7 +278,7 @@ namespace Microsoft.AspNetCore.Components.Test
// Act
var componentId = renderer.AssignRootComponentId(component);
renderer.RenderRootComponent(componentId);
var renderTask = renderer.RenderRootComponentAsync(componentId);
// Assert
Assert.Single(renderer.Batches);
@ -256,8 +287,75 @@ namespace Microsoft.AspNetCore.Components.Test
component.Counter = 2;
onParametersSetTask.SetCanceled();
await renderTask;
// Component should not be rendered again
Assert.Single(renderer.Batches);
}
[Fact]
public async Task RenderRootComponentAsync_ReportsErrorDuringOnInit()
{
// Arrange
var expected = new TimeZoneNotFoundException();
var renderer = new TestRenderer();
var component = new TestComponent { OnInitLogic = _ => throw expected };
// Act & Assert
var componentId = renderer.AssignRootComponentId(component);
var actual = await Assert.ThrowsAsync<TimeZoneNotFoundException>(() => renderer.RenderRootComponentAsync(componentId));
// Assert
Assert.Same(expected, actual);
}
[Fact]
public async Task RenderRootComponentAsync_ReportsErrorDuringOnInitAsync()
{
// Arrange
var expected = new TimeZoneNotFoundException();
var renderer = new TestRenderer();
var component = new TestComponent { OnInitAsyncLogic = _ => Task.FromException(expected) };
// Act & Assert
var componentId = renderer.AssignRootComponentId(component);
var actual = await Assert.ThrowsAsync<TimeZoneNotFoundException>(() => renderer.RenderRootComponentAsync(componentId));
// Assert
Assert.Same(expected, actual);
}
[Fact]
public async Task RenderRootComponentAsync_ReportsErrorDuringOnParameterSet()
{
// Arrange
var expected = new TimeZoneNotFoundException();
var renderer = new TestRenderer();
var component = new TestComponent { OnParametersSetLogic = _ => throw expected };
// Act & Assert
var componentId = renderer.AssignRootComponentId(component);
var actual = await Assert.ThrowsAsync<TimeZoneNotFoundException>(() => renderer.RenderRootComponentAsync(componentId));
// Assert
Assert.Same(expected, actual);
}
[Fact]
public async Task RenderRootComponentAsync_ReportsErrorDuringOnParameterSetAsync()
{
// Arrange
var expected = new TimeZoneNotFoundException();
var renderer = new TestRenderer();
var component = new TestComponent { OnParametersSetAsyncLogic = _ => Task.FromException(expected) };
// Act & Assert
var componentId = renderer.AssignRootComponentId(component);
var actual = await Assert.ThrowsAsync<TimeZoneNotFoundException>(() => renderer.RenderRootComponentAsync(componentId));
// Assert
Assert.Same(expected, actual);
}
private class TestComponent : ComponentBase
@ -305,7 +403,10 @@ namespace Microsoft.AspNetCore.Components.Test
await base.OnInitAsync();
}
await OnInitAsyncLogic?.Invoke(this);
if (OnInitAsyncLogic != null)
{
await OnInitAsyncLogic.Invoke(this);
}
}
protected override void OnParametersSet()
@ -325,7 +426,10 @@ namespace Microsoft.AspNetCore.Components.Test
await base.OnParametersSetAsync();
}
await OnParametersSetAsyncLogic?.Invoke(this);
if (OnParametersSetAsyncLogic != null)
{
await OnParametersSetAsyncLogic(this);
}
}
}
}

View File

@ -1061,6 +1061,9 @@ namespace Microsoft.AspNetCore.Components.Test
{
}
protected override void HandleException(Exception exception)
=> throw new NotImplementedException();
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
=> throw new NotImplementedException();
}

View File

@ -1,14 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
using Xunit;
namespace Microsoft.AspNetCore.Components.Test
@ -1531,6 +1530,9 @@ namespace Microsoft.AspNetCore.Components.Test
{
}
protected override void HandleException(Exception exception)
=> throw new NotImplementedException();
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
=> Task.CompletedTask;
}

View File

@ -6,10 +6,12 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
using Microsoft.AspNetCore.Testing;
using Microsoft.AspNetCore.Testing.xunit;
using Xunit;
@ -173,13 +175,17 @@ namespace Microsoft.AspNetCore.Components.Test
{
// Arrange
var renderer = new TestRenderer();
var component = new AsyncComponent(5); // Triggers n renders, the first one creating <p>n</p> and the n-1 renders asynchronously update the value.
var tcs = new TaskCompletionSource<int>();
var component = new AsyncComponent(tcs.Task, 5); // Triggers n renders, the first one creating <p>n</p> and the n-1 renders asynchronously update the value.
// Act
var componentId = renderer.AssignRootComponentId(component);
await renderer.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId));
var renderTask = renderer.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId));
// Assert
Assert.False(renderTask.IsCompleted);
tcs.SetResult(0);
await renderTask;
Assert.Equal(5, renderer.Batches.Count);
// First render
@ -195,7 +201,7 @@ namespace Microsoft.AspNetCore.Components.Test
AssertFrame.Text(create.ReferenceFrames[1], "5");
// Second render
for (int i = 1; i < 5; i++)
for (var i = 1; i < 5; i++)
{
var update = renderer.Batches[i];
@ -413,6 +419,36 @@ namespace Microsoft.AspNetCore.Components.Test
AssertStream(3, logForThirdChild);
}
[Fact]
public void DispatchingEventsWithoutAsyncWorkShouldCompleteSynchronously()
{
// Arrange: Render a component with an event handler
var renderer = new TestRenderer();
UIEventArgs receivedArgs = null;
var component = new EventComponent
{
OnTest = args => { receivedArgs = args; }
};
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var eventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.First(frame => frame.AttributeValue != null)
.AttributeEventHandlerId;
// Assert: Event not yet fired
Assert.Null(receivedArgs);
// Act/Assert: Event can be fired
var eventArgs = new UIEventArgs();
var task = renderer.DispatchEventAsync(componentId, eventHandlerId, eventArgs);
// This should always be run synchronously
Assert.True(task.IsCompletedSuccessfully);
}
[Fact]
public void CanDispatchEventsToTopLevelComponents()
{
@ -437,7 +473,9 @@ namespace Microsoft.AspNetCore.Components.Test
// Act/Assert: Event can be fired
var eventArgs = new UIEventArgs();
renderer.DispatchEvent(componentId, eventHandlerId, eventArgs);
var renderTask = renderer.DispatchEventAsync(componentId, eventHandlerId, eventArgs);
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Same(eventArgs, receivedArgs);
}
@ -465,7 +503,9 @@ namespace Microsoft.AspNetCore.Components.Test
// Act/Assert: Event can be fired
var eventArgs = new UIMouseEventArgs();
renderer.DispatchEvent(componentId, eventHandlerId, eventArgs);
var renderTask = renderer.DispatchEventAsync(componentId, eventHandlerId, eventArgs);
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Same(eventArgs, receivedArgs);
}
@ -493,7 +533,9 @@ namespace Microsoft.AspNetCore.Components.Test
// Act/Assert: Event can be fired
var eventArgs = new UIMouseEventArgs();
renderer.DispatchEvent(componentId, eventHandlerId, eventArgs);
var renderTask = renderer.DispatchEventAsync(componentId, eventHandlerId, eventArgs);
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.NotNull(receivedArgs);
}
@ -532,7 +574,9 @@ namespace Microsoft.AspNetCore.Components.Test
// Act/Assert: Event can be fired
var eventArgs = new UIEventArgs();
renderer.DispatchEvent(nestedComponentId, eventHandlerId, eventArgs);
var renderTask = renderer.DispatchEventAsync(nestedComponentId, eventHandlerId, eventArgs);
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Same(eventArgs, receivedArgs);
}
@ -559,7 +603,11 @@ namespace Microsoft.AspNetCore.Components.Test
var eventArgs = new UIEventArgs();
// Act/Assert
var ex = Assert.Throws<InvalidOperationException>(() => renderer.DispatchEvent(componentId, eventHandlerId, eventArgs));
var ex = Assert.Throws<InvalidOperationException>(() =>
{
// Verifies that the exception is thrown synchronously.
_ = renderer.DispatchEventAsync(componentId, eventHandlerId, eventArgs);
});
Assert.Equal($"The component of type {typeof(TestComponent).FullName} cannot receive " +
$"events because it does not implement {typeof(IHandleEvent).FullName}.", ex.Message);
}
@ -571,7 +619,11 @@ namespace Microsoft.AspNetCore.Components.Test
var renderer = new TestRenderer();
// Act/Assert
Assert.Throws<ArgumentException>(() => renderer.DispatchEvent(123, 0, new UIEventArgs()));
Assert.Throws<ArgumentException>(() =>
{
// Intentionally written this way to verify that the exception is thrown synchronously.
_ = renderer.DispatchEventAsync(123, 0, new UIEventArgs());
});
}
[Fact]
@ -790,7 +842,8 @@ namespace Microsoft.AspNetCore.Components.Test
// Act/Assert 1: Event handler fires when we trigger it
Assert.Equal(0, eventCount);
renderer.DispatchEvent(componentId, origEventHandlerId, args: null);
var renderTask = renderer.DispatchEventAsync(componentId, origEventHandlerId, args: null);
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Equal(1, eventCount);
// Now change the attribute value
@ -799,10 +852,16 @@ namespace Microsoft.AspNetCore.Components.Test
component.TriggerRender();
// Act/Assert 2: Can no longer fire the original event, but can fire the new event
Assert.Throws<ArgumentException>(() => renderer.DispatchEvent(componentId, origEventHandlerId, args: null));
Assert.Throws<ArgumentException>(() =>
{
// Verifies that the exception is thrown synchronously.
_ = renderer.DispatchEventAsync(componentId, origEventHandlerId, args: null);
});
Assert.Equal(1, eventCount);
Assert.Equal(0, newEventCount);
renderer.DispatchEvent(componentId, origEventHandlerId + 1, args: null);
renderTask = renderer.DispatchEventAsync(componentId, origEventHandlerId + 1, args: null);
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Equal(1, newEventCount);
}
@ -824,7 +883,8 @@ namespace Microsoft.AspNetCore.Components.Test
// Act/Assert 1: Event handler fires when we trigger it
Assert.Equal(0, eventCount);
renderer.DispatchEvent(componentId, origEventHandlerId, args: null);
var renderTask = renderer.DispatchEventAsync(componentId, origEventHandlerId, args: null);
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Equal(1, eventCount);
// Now remove the event attribute
@ -832,7 +892,11 @@ namespace Microsoft.AspNetCore.Components.Test
component.TriggerRender();
// Act/Assert 2: Can no longer fire the original event
Assert.Throws<ArgumentException>(() => renderer.DispatchEvent(componentId, origEventHandlerId, args: null));
Assert.Throws<ArgumentException>(() =>
{
// Verifies that the exception is thrown synchronously.
_ = renderer.DispatchEventAsync(componentId, origEventHandlerId, args: null);
});
Assert.Equal(1, eventCount);
}
@ -870,7 +934,8 @@ namespace Microsoft.AspNetCore.Components.Test
// Act/Assert 1: Event handler fires when we trigger it
Assert.Equal(0, eventCount);
renderer.DispatchEvent(childComponentId, eventHandlerId, args: null);
var renderTask = renderer.DispatchEventAsync(childComponentId, eventHandlerId, args: null);
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Equal(1, eventCount);
// Now remove the EventComponent
@ -878,7 +943,11 @@ namespace Microsoft.AspNetCore.Components.Test
component.TriggerRender();
// Act/Assert 2: Can no longer fire the original event
Assert.Throws<ArgumentException>(() => renderer.DispatchEvent(eventHandlerId, eventHandlerId, args: null));
Assert.Throws<ArgumentException>(() =>
{
// Verifies that the exception is thrown synchronously.
_ = renderer.DispatchEventAsync(eventHandlerId, eventHandlerId, args: null);
});
Assert.Equal(1, eventCount);
}
@ -900,7 +969,8 @@ namespace Microsoft.AspNetCore.Components.Test
// Act/Assert 1: Event handler fires when we trigger it
Assert.Equal(0, eventCount);
renderer.DispatchEvent(componentId, origEventHandlerId, args: null);
var renderTask = renderer.DispatchEventAsync(componentId, origEventHandlerId, args: null);
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Equal(1, eventCount);
// Now remove the ancestor element
@ -908,7 +978,11 @@ namespace Microsoft.AspNetCore.Components.Test
component.TriggerRender();
// Act/Assert 2: Can no longer fire the original event
Assert.Throws<ArgumentException>(() => renderer.DispatchEvent(componentId, origEventHandlerId, args: null));
Assert.Throws<ArgumentException>(() =>
{
// Verifies that the exception is thrown synchronously.
_ = renderer.DispatchEventAsync(componentId, origEventHandlerId, args: null);
});
Assert.Equal(1, eventCount);
}
@ -947,9 +1021,10 @@ namespace Microsoft.AspNetCore.Components.Test
Assert.Single(renderer.Batches);
// Act
renderer.DispatchEvent(childComponentId, origEventHandlerId, args: null);
var renderTask = renderer.DispatchEventAsync(childComponentId, origEventHandlerId, args: null);
// Assert
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Equal(2, renderer.Batches.Count);
var batch = renderer.Batches.Last();
Assert.Collection(batch.DiffsInOrder,
@ -1137,9 +1212,10 @@ namespace Microsoft.AspNetCore.Components.Test
// Act
// The fact that there's no error here is the main thing we're testing
renderer.DispatchEvent(childComponentId, origEventHandlerId, args: null);
var renderTask = renderer.DispatchEventAsync(childComponentId, origEventHandlerId, args: null);
// Assert: correct render result
Assert.True(renderTask.IsCompletedSuccessfully);
var newBatch = renderer.Batches.Skip(1).Single();
Assert.Equal(1, newBatch.DisposedComponentIDs.Count);
Assert.Equal(1, newBatch.DiffsByComponentId.Count);
@ -1168,7 +1244,9 @@ namespace Microsoft.AspNetCore.Components.Test
// Act: Toggle the checkbox
var eventArgs = new UIChangeEventArgs { Value = true };
renderer.DispatchEvent(componentId, checkboxChangeEventHandlerId, eventArgs);
var renderTask = renderer.DispatchEventAsync(componentId, checkboxChangeEventHandlerId, eventArgs);
Assert.True(renderTask.IsCompletedSuccessfully);
var latestBatch = renderer.Batches.Last();
var latestDiff = latestBatch.DiffsInOrder.Single();
var referenceFrames = latestBatch.ReferenceFrames;
@ -1349,28 +1427,444 @@ namespace Microsoft.AspNetCore.Components.Test
// Act/Assert 1: Event can be fired for the first time
var render1TCS = new TaskCompletionSource<object>();
renderer.NextUpdateDisplayReturnTask = render1TCS.Task;
renderer.DispatchEvent(componentId, eventHandlerId, new UIEventArgs());
await renderer.DispatchEventAsync(componentId, eventHandlerId, new UIEventArgs());
Assert.Equal(1, numEventsFired);
// Act/Assert 2: *Same* event handler ID can be reused prior to completion of
// preceding UI update
var render2TCS = new TaskCompletionSource<object>();
renderer.NextUpdateDisplayReturnTask = render2TCS.Task;
renderer.DispatchEvent(componentId, eventHandlerId, new UIEventArgs());
await renderer.DispatchEventAsync(componentId, eventHandlerId, new UIEventArgs());
Assert.Equal(2, numEventsFired);
// Act/Assert 3: After we complete the first UI update in which a given
// event handler ID is disposed, we can no longer reuse that event handler ID
render1TCS.SetResult(null);
await Task.Delay(500); // From here we can't see when the async disposal is completed. Just give it plenty of time (Task.Yield isn't enough).
var ex = Assert.Throws<ArgumentException>(() =>
{
renderer.DispatchEvent(componentId, eventHandlerId, new UIEventArgs());
});
var ex = await Assert.ThrowsAsync<ArgumentException>(() =>
renderer.DispatchEventAsync(componentId, eventHandlerId, new UIEventArgs()));
Assert.Equal($"There is no event handler with ID {eventHandlerId}", ex.Message);
Assert.Equal(2, numEventsFired);
}
[Fact]
public void ExceptionsThrownSynchronouslyCanBeHandledSynchronously()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new NestedAsyncComponent();
var exception = new InvalidTimeZoneException();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var task = renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
{
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = new[]
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
EventAction = () => throw exception,
},
}
},
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(Array.Empty<int>()),
},
}));
Assert.True(task.IsCompletedSuccessfully);
Assert.Equal(new[] { exception }, renderer.HandledExceptions);
}
[Fact]
public void ExceptionsThrownSynchronouslyCanBeHandled()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new NestedAsyncComponent();
var exception = new InvalidTimeZoneException();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
{
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = new[]
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
EventAction = () => throw exception,
},
}
},
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(Array.Empty<int>()),
},
}));
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Equal(new[] { exception }, renderer.HandledExceptions);
}
[Fact]
public void ExceptionsReturnedUsingTaskFromExceptionCanBeHandled()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new NestedAsyncComponent();
var exception = new InvalidTimeZoneException();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
{
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = new[]
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
EventAction = () => Task.FromException<(int, NestedAsyncComponent.EventType)>(exception),
},
}
},
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(Array.Empty<int>()),
},
}));
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Equal(new[] { exception }, renderer.HandledExceptions);
}
[Fact]
public async Task ExceptionsThrownAsynchronouslyCanBeHandled()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new NestedAsyncComponent();
var tcs = new TaskCompletionSource<int>();
var exception = new InvalidTimeZoneException();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
{
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = new[]
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
EventAction = async () =>
{
await tcs.Task;
throw exception;
}
},
}
},
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(Array.Empty<int>()),
},
}));
Assert.False(renderTask.IsCompleted);
tcs.SetResult(0);
await renderTask;
Assert.Same(exception, Assert.Single(renderer.HandledExceptions).GetBaseException());
}
[Fact]
public async Task ExceptionsThrownAsynchronouslyFromMultipleComponentsCanBeHandled()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new NestedAsyncComponent();
var exception1 = new InvalidTimeZoneException();
var exception2 = new UriFormatException();
var tcs = new TaskCompletionSource<int>();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
{
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = Array.Empty<NestedAsyncComponent.ExecutionAction>(),
[1] = new List<NestedAsyncComponent.ExecutionAction>
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
EventAction = async () =>
{
await tcs.Task;
throw exception1;
}
},
},
[2] = new List<NestedAsyncComponent.ExecutionAction>
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
EventAction = async () =>
{
await tcs.Task;
throw exception2;
}
},
},
},
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(new[] { 1, 2, }),
[1] = CreateRenderFactory(Array.Empty<int>()),
[2] = CreateRenderFactory(Array.Empty<int>()),
},
}));
Assert.False(renderTask.IsCompleted);
tcs.SetResult(0);
await renderTask;
Assert.Equal(2, renderer.HandledExceptions.Count);
Assert.Contains(exception1, renderer.HandledExceptions);
Assert.Contains(exception2, renderer.HandledExceptions);
}
[Fact]
public void ExceptionsThrownSynchronouslyFromMultipleComponentsCanBeHandled()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new NestedAsyncComponent();
var exception1 = new InvalidTimeZoneException();
var exception2 = new UriFormatException();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
{
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = Array.Empty<NestedAsyncComponent.ExecutionAction>(),
[1] = new List<NestedAsyncComponent.ExecutionAction>
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
EventAction = () =>
{
throw exception1;
}
},
},
[2] = new List<NestedAsyncComponent.ExecutionAction>
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
EventAction = () =>
{
throw exception2;
}
},
},
},
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(new[] { 1, 2, }),
[1] = CreateRenderFactory(Array.Empty<int>()),
[2] = CreateRenderFactory(Array.Empty<int>()),
},
}));
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Equal(2, renderer.HandledExceptions.Count);
Assert.Contains(exception1, renderer.HandledExceptions);
Assert.Contains(exception2, renderer.HandledExceptions);
}
[Fact]
public async Task ExceptionsThrownFromHandleAfterRender_AreHandled()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new NestedAsyncComponent();
var exception = new InvalidTimeZoneException();
var taskCompletionSource = new TaskCompletionSource<int>();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
{
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = new[]
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnAfterRenderAsync,
EventAction = () =>
{
throw exception;
},
}
},
[1] = new[]
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnAfterRenderAsync,
EventAction = async () =>
{
await Task.Yield();
taskCompletionSource.TrySetResult(0);
return (1, NestedAsyncComponent.EventType.OnAfterRenderAsync);
},
}
}
},
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(new[] { 1 }),
[1] = CreateRenderFactory(Array.Empty<int>()),
},
}));
Assert.True(renderTask.IsCompletedSuccessfully);
// OnAfterRenderAsync happens in the background. Make it more predictable, by gating it until we're ready to capture exceptions.
await taskCompletionSource.Task.TimeoutAfter(TimeSpan.FromSeconds(10));
Assert.Same(exception, Assert.Single(renderer.HandledExceptions).GetBaseException());
}
[Fact]
public void SynchronousCancelledTasks_HandleAfterRender_Works()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new NestedAsyncComponent();
var tcs = new TaskCompletionSource<(int, NestedAsyncComponent.EventType)>();
tcs.TrySetCanceled();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
{
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = new[]
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnAfterRenderAsync,
EventAction = () => tcs.Task,
}
},
},
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(Array.Empty<int>()),
},
}));
// Rendering should finish synchronously
Assert.True(renderTask.IsCompletedSuccessfully);
Assert.Empty(renderer.HandledExceptions);
}
[Fact]
public void AsynchronousCancelledTasks_HandleAfterRender_Works()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new NestedAsyncComponent();
var tcs = new TaskCompletionSource<(int, NestedAsyncComponent.EventType)>();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
{
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = new[]
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnAfterRenderAsync,
EventAction = () => tcs.Task,
}
},
},
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(Array.Empty<int>()),
},
}));
// Rendering should be complete.
Assert.True(renderTask.IsCompletedSuccessfully);
tcs.TrySetCanceled();
Assert.Empty(renderer.HandledExceptions);
}
[Fact]
public async Task CanceledTasksInHandleAfterRender_AreIgnored()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var component = new NestedAsyncComponent();
var taskCompletionSource = new TaskCompletionSource<int>();
var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.Cancel();
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
await renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
{
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
{
[0] = new[]
{
new NestedAsyncComponent.ExecutionAction
{
Event = NestedAsyncComponent.EventType.OnAfterRenderAsync,
EventAction = () =>
{
taskCompletionSource.TrySetResult(0);
cancellationTokenSource.Token.ThrowIfCancellationRequested();
return default;
},
}
},
},
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
{
[0] = CreateRenderFactory(Array.Empty<int>()),
},
}));
await taskCompletionSource.Task.TimeoutAfter(TimeSpan.FromSeconds(10));
Assert.Empty(renderer.HandledExceptions);
}
[Fact]
public void DisposingRenderer_DisposesTopLevelComponents()
{
@ -1416,7 +1910,7 @@ namespace Microsoft.AspNetCore.Components.Test
public void DisposingRenderer_CapturesExceptionsFromAllRegisteredComponents()
{
// Arrange
var renderer = new TestRenderer();
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var exception1 = new Exception();
var exception2 = new Exception();
var component = new TestComponent(builder =>
@ -1434,13 +1928,13 @@ namespace Microsoft.AspNetCore.Components.Test
component.TriggerRender();
// Act &A Assert
var aggregate = Assert.Throws<AggregateException>(renderer.Dispose);
renderer.Dispose();
// All components must be disposed even if some throw as part of being diposed.
Assert.True(component.Disposed);
Assert.Equal(2, aggregate.InnerExceptions.Count);
Assert.Contains(exception1, aggregate.InnerExceptions);
Assert.Contains(exception2, aggregate.InnerExceptions);
Assert.Equal(2, renderer.HandledExceptions.Count);
Assert.Contains(exception1, renderer.HandledExceptions);
Assert.Contains(exception2, renderer.HandledExceptions);
}
private class NoOpRenderer : Renderer
@ -1452,6 +1946,9 @@ namespace Microsoft.AspNetCore.Components.Test
public new int AssignRootComponentId(IComponent component)
=> base.AssignRootComponentId(component);
protected override void HandleException(Exception exception)
=> throw new NotImplementedException();
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
=> Task.CompletedTask;
}
@ -1572,9 +2069,10 @@ namespace Microsoft.AspNetCore.Components.Test
builder.AddContent(6, $"Render count: {++renderCount}");
}
public void HandleEvent(EventHandlerInvoker binding, UIEventArgs args)
public Task HandleEventAsync(EventHandlerInvoker binding, UIEventArgs args)
{
binding.Invoke(args);
return Task.CompletedTask;
}
}
@ -1642,9 +2140,9 @@ namespace Microsoft.AspNetCore.Components.Test
return Task.CompletedTask;
}
public void HandleEvent(EventHandlerInvoker binding, UIEventArgs args)
public async Task HandleEventAsync(EventHandlerInvoker binding, UIEventArgs args)
{
var task = binding.Invoke(args);
await binding.Invoke(args);
Render();
}
@ -1687,10 +2185,11 @@ namespace Microsoft.AspNetCore.Components.Test
public bool CheckboxEnabled;
public string SomeStringProperty;
public void HandleEvent(EventHandlerInvoker binding, UIEventArgs args)
public Task HandleEventAsync(EventHandlerInvoker binding, UIEventArgs args)
{
binding.Invoke(args);
TriggerRender();
return Task.CompletedTask;
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
@ -1712,9 +2211,10 @@ namespace Microsoft.AspNetCore.Components.Test
{
public int OnAfterRenderCallCount { get; private set; }
public void OnAfterRender()
public Task OnAfterRenderAsync()
{
OnAfterRenderCallCount++;
return Task.CompletedTask;
}
Task IComponent.SetParametersAsync(ParameterCollection parameters)
@ -1761,11 +2261,14 @@ namespace Microsoft.AspNetCore.Components.Test
{
private RenderHandle _renderHandler;
public AsyncComponent(int number)
public AsyncComponent(Task taskToAwait, int number)
{
_taskToAwait = taskToAwait;
Number = number;
}
private readonly Task _taskToAwait;
public int Number { get; set; }
public void Configure(RenderHandle renderHandle)
@ -1781,7 +2284,7 @@ namespace Microsoft.AspNetCore.Components.Test
n = Number;
_renderHandler.Render(CreateFragment);
Number--;
await Task.Yield();
await _taskToAwait;
};
// Cheap closure
@ -1833,7 +2336,7 @@ namespace Microsoft.AspNetCore.Components.Test
return component => builder =>
{
int s = 0;
var s = 0;
builder.OpenElement(s++, "div");
builder.AddContent(s++, $"Id: {component.TestId} BuildRenderTree, {Guid.NewGuid()}");
foreach (var child in childrenToRender)
@ -1852,13 +2355,6 @@ namespace Microsoft.AspNetCore.Components.Test
private class NestedAsyncComponent : ComponentBase
{
private RenderHandle _renderHandle;
public void Configure(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}
[Parameter] public IDictionary<int, IList<ExecutionAction>> EventActions { get; set; }
[Parameter] public IDictionary<int, Func<NestedAsyncComponent, RenderFragment>> WhatToRender { get; set; }
@ -1924,6 +2420,15 @@ namespace Microsoft.AspNetCore.Components.Test
renderFactory(this)(builder);
}
protected override async Task OnAfterRenderAsync()
{
if (TryGetEntry(EventType.OnAfterRenderAsync, out var entry))
{
var result = await entry.EventAction();
LogResult(result);
}
}
private bool TryGetEntry(EventType eventType, out ExecutionAction entry)
{
var entries = EventActions[TestId];
@ -1937,7 +2442,7 @@ namespace Microsoft.AspNetCore.Components.Test
private void LogResult((int, EventType) entry)
{
Log.Enqueue(entry);
Log?.Enqueue(entry);
}
public class ExecutionAction
@ -1977,7 +2482,8 @@ namespace Microsoft.AspNetCore.Components.Test
OnInitAsyncAsync,
OnParametersSet,
OnParametersSetAsyncSync,
OnParametersSetAsyncAsync
OnParametersSetAsyncAsync,
OnAfterRenderAsync,
}
}
}

View File

@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
private static readonly Func<string, string> _encoder = (string t) => HtmlEncoder.Default.Encode(t);
[Fact]
public void RenderComponent_CanRenderEmptyElement()
public void RenderComponentAsync_CanRenderEmptyElement()
{
// Arrange
var dispatcher = Renderer.CreateDefaultDispatcher();
@ -30,14 +30,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
// Act
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_CanRenderSimpleComponent()
public void RenderComponentAsync_CanRenderSimpleComponent()
{
// Arrange
var dispatcher = Renderer.CreateDefaultDispatcher();
@ -51,14 +51,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
// Act
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_HtmlEncodesContent()
public void RenderComponentAsync_HtmlEncodesContent()
{
// Arrange
var dispatcher = Renderer.CreateDefaultDispatcher();
@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
// Act
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
[Fact]
public void RenderComponent_DoesNotEncodeMarkup()
public void RenderComponentAsync_DoesNotEncodeMarkup()
{
// Arrange
var dispatcher = Renderer.CreateDefaultDispatcher();
@ -94,7 +94,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
// Act
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
@ -102,7 +102,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
[Fact]
public void RenderComponent_CanRenderWithAttributes()
public void RenderComponentAsync_CanRenderWithAttributes()
{
// Arrange
var dispatcher = Renderer.CreateDefaultDispatcher();
@ -118,14 +118,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
// Act
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_HtmlEncodesAttributeValues()
public void RenderComponentAsync_HtmlEncodesAttributeValues()
{
// Arrange
var dispatcher = Renderer.CreateDefaultDispatcher();
@ -141,14 +141,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
// Act
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_CanRenderBooleanAttributes()
public void RenderComponentAsync_CanRenderBooleanAttributes()
{
// Arrange
var dispatcher = Renderer.CreateDefaultDispatcher();
@ -163,14 +163,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
// Act
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_DoesNotRenderBooleanAttributesWhenValueIsFalse()
public void RenderComponentAsync_DoesNotRenderBooleanAttributesWhenValueIsFalse()
{
// Arrange
var dispatcher = Renderer.CreateDefaultDispatcher();
@ -185,14 +185,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
// Act
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_CanRenderWithChildren()
public void RenderComponentAsync_CanRenderWithChildren()
{
// Arrange
var dispatcher = Renderer.CreateDefaultDispatcher();
@ -209,14 +209,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
// Act
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_CanRenderWithMultipleChildren()
public void RenderComponentAsync_CanRenderWithMultipleChildren()
{
// Arrange
var dispatcher = Renderer.CreateDefaultDispatcher();
@ -240,14 +240,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
// Act
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_CanRenderComponentWithChildrenComponents()
public void RenderComponentAsync_CanRenderComponentAsyncWithChildrenComponents()
{
// Arrange
var dispatcher = Renderer.CreateDefaultDispatcher();
@ -270,14 +270,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
// Act
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_ComponentReferenceNoops()
public void RenderComponentAsync_ComponentReferenceNoops()
{
// Arrange
var dispatcher = Renderer.CreateDefaultDispatcher();
@ -301,14 +301,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
// Act
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_CanPassParameters()
public void RenderComponentAsync_CanPassParameters()
{
// Arrange
var dispatcher = Renderer.CreateDefaultDispatcher();
@ -333,7 +333,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
Action<UIChangeEventArgs> change = (UIChangeEventArgs changeArgs) => throw new InvalidOperationException();
// Act
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<ComponentWithParameters>(
var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<ComponentWithParameters>(
new ParameterCollection(new[] {
RenderTreeFrame.Element(0,string.Empty),
RenderTreeFrame.Attribute(1,"update",change),
@ -345,7 +345,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
}
[Fact]
public void RenderComponent_CanRenderComponentWithRenderFragmentContent()
public void RenderComponentAsync_CanRenderComponentAsyncWithRenderFragmentContent()
{
// Arrange
var dispatcher = Renderer.CreateDefaultDispatcher();
@ -365,14 +365,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
// Act
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);
}
[Fact]
public void RenderComponent_ElementRefsNoops()
public void RenderComponentAsync_ElementRefsNoops()
{
// Arrange
var dispatcher = Renderer.CreateDefaultDispatcher();
@ -393,7 +393,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
// Act
var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
// Assert
Assert.Equal(expectedHtml, result);

View File

@ -100,7 +100,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
for (var i = 0; i < builder.Entries.Count; i++)
{
var (componentType, domElementSelector) = builder.Entries[i];
Renderer.AddComponent(componentType, domElementSelector);
await Renderer.AddComponentAsync(componentType, domElementSelector);
}
for (var i = 0; i < _circuitHandlers.Length; i++)

View File

@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Components.Server.Circuits
@ -17,10 +18,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly DefaultCircuitFactoryOptions _options;
private readonly ILoggerFactory _loggerFactory;
public DefaultCircuitFactory(
IServiceScopeFactory scopeFactory,
IOptions<DefaultCircuitFactoryOptions> options)
IOptions<DefaultCircuitFactoryOptions> options,
ILoggerFactory loggerFactory)
{
if (options == null)
{
@ -29,6 +32,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
_options = options.Value;
_loggerFactory = loggerFactory;
}
public override CircuitHost CreateCircuitHost(HttpContext httpContext, IClientProxy client)
@ -48,7 +52,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
rendererRegistry,
jsRuntime,
client,
dispatcher);
dispatcher,
_loggerFactory.CreateLogger<RemoteRenderer>());
var circuitHandlers = scope.ServiceProvider.GetServices<CircuitHandler>()
.OrderBy(h => h.Order)

View File

@ -7,8 +7,10 @@ using System.Threading;
using System.Threading.Tasks;
using MessagePack;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Components.Browser.Rendering
@ -25,6 +27,7 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
private readonly RendererRegistry _rendererRegistry;
private readonly ConcurrentDictionary<long, AutoCancelTaskCompletionSource<object>> _pendingRenders
= new ConcurrentDictionary<long, AutoCancelTaskCompletionSource<object>>();
private readonly ILogger _logger;
private long _nextRenderId = 1;
/// <summary>
@ -45,7 +48,8 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
RendererRegistry rendererRegistry,
IJSRuntime jsRuntime,
IClientProxy client,
IDispatcher dispatcher)
IDispatcher dispatcher,
ILogger logger)
: base(serviceProvider, dispatcher)
{
_rendererRegistry = rendererRegistry;
@ -53,18 +57,7 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
_client = client;
_id = _rendererRegistry.Add(this);
}
/// <summary>
/// Attaches a new root component to the renderer,
/// causing it to be displayed in the specified DOM element.
/// </summary>
/// <typeparam name="TComponent">The type of the component.</typeparam>
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
public void AddComponent<TComponent>(string domElementSelector)
where TComponent : IComponent
{
AddComponent(typeof(TComponent), domElementSelector);
_logger = logger;
}
/// <summary>
@ -73,7 +66,7 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
/// </summary>
/// <param name="componentType">The type of the component.</param>
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
public void AddComponent(Type componentType, string domElementSelector)
public Task AddComponentAsync(Type componentType, string domElementSelector)
{
var component = InstantiateComponent(componentType);
var componentId = AssignRootComponentId(component);
@ -85,7 +78,23 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
componentId);
CaptureAsyncExceptions(attachComponentTask);
RenderRootComponent(componentId);
return RenderRootComponentAsync(componentId);
}
/// <inheritdoc />
protected override void HandleException(Exception exception)
{
if (exception is AggregateException aggregateException)
{
foreach (var innerException in aggregateException.Flatten().InnerExceptions)
{
_logger.UnhandledExceptionRenderingComponent(innerException);
}
}
else
{
_logger.UnhandledExceptionRenderingComponent(exception);
}
}
/// <inheritdoc />

View File

@ -0,0 +1,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Components.Server
{
internal static class LoggerExtensions
{
private static readonly Action<ILogger, string, Exception> _unhandledExceptionRenderingComponent;
static LoggerExtensions()
{
_unhandledExceptionRenderingComponent = LoggerMessage.Define<string>(
LogLevel.Warning,
new EventId(1, "ExceptionRenderingComponent"),
"Unhandled exception rendering component: {Message}");
}
public static void UnhandledExceptionRenderingComponent(this ILogger logger, Exception exception)
{
_unhandledExceptionRenderingComponent(
logger,
exception.Message,
exception);
}
}
}

View File

@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Components.Browser;
using Microsoft.AspNetCore.Components.Browser.Rendering;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.JSInterop;
using Moq;
using Xunit;
@ -155,7 +156,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
private class TestRemoteRenderer : RemoteRenderer
{
public TestRemoteRenderer(IServiceProvider serviceProvider, RendererRegistry rendererRegistry, IJSRuntime jsRuntime, IClientProxy client)
: base(serviceProvider, rendererRegistry, jsRuntime, client, CreateDefaultDispatcher())
: base(serviceProvider, rendererRegistry, jsRuntime, client, CreateDefaultDispatcher(), NullLogger.Instance)
{
}

View File

@ -377,6 +377,11 @@ namespace Microsoft.AspNetCore.Components.Server
{
}
protected override void HandleException(Exception exception)
{
throw new NotImplementedException();
}
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
=> throw new NotImplementedException();
}

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Xunit;
@ -31,11 +30,18 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
public List<CapturedBatch> Batches { get; }
= new List<CapturedBatch>();
public List<Exception> HandledExceptions { get; } = new List<Exception>();
public bool ShouldHandleExceptions { get; set; }
public new int AssignRootComponentId(IComponent component)
=> base.AssignRootComponentId(component);
public new void RenderRootComponent(int componentId)
=> Invoke(() => base.RenderRootComponent(componentId));
public void RenderRootComponent(int componentId, ParameterCollection? parameters = default)
{
var task = InvokeAsync(() => base.RenderRootComponentAsync(componentId, parameters ?? ParameterCollection.Empty));
UnwrapTask(task);
}
public new Task RenderRootComponentAsync(int componentId)
=> InvokeAsync(() => base.RenderRootComponentAsync(componentId));
@ -43,25 +49,43 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
public new Task RenderRootComponentAsync(int componentId, ParameterCollection parameters)
=> InvokeAsync(() => base.RenderRootComponentAsync(componentId, parameters));
public new void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs args)
public new Task DispatchEventAsync(int componentId, int eventHandlerId, UIEventArgs args)
{
var task = InvokeAsync(() => base.DispatchEventAsync(componentId, eventHandlerId, args));
return UnwrapTask(task);
}
private static Task UnwrapTask(Task task)
{
var t = Invoke(() => base.DispatchEvent(componentId, eventHandlerId, args));
// This should always be run synchronously
Assert.True(t.IsCompleted);
if (t.IsFaulted)
Assert.True(task.IsCompleted);
if (task.IsFaulted)
{
var exception = t.Exception.Flatten().InnerException;
var exception = task.Exception.Flatten().InnerException;
while (exception is AggregateException e)
{
exception = e.InnerException;
}
ExceptionDispatchInfo.Capture(exception).Throw();
}
return task;
}
public T InstantiateComponent<T>() where T : IComponent
=> (T)InstantiateComponent(typeof(T));
protected override void HandleException(Exception exception)
{
if (!ShouldHandleExceptions)
{
throw exception;
}
HandledExceptions.Add(exception);
}
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
{
OnUpdateDisplay?.Invoke(renderBatch);

View File

@ -107,14 +107,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure
{
}
await Task.Delay(1000);
await Task.Delay(1000);
}
});
try
{
waitForStart.TimeoutAfter(Timeout).Wait();
waitForStart.TimeoutAfter(Timeout).Wait(1000);
}
catch (Exception ex)
{

View File

@ -23,7 +23,7 @@ namespace Interop.FunctionalTests
SkipReason = "Missing Windows ALPN support: https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation#Support")]
public class H2SpecTests : LoggedTest
{
[ConditionalTheory]
[ConditionalTheory(Skip = "https://github.com/aspnet/AspNetCore/issues/6691")]
[MemberData(nameof(H2SpecTestCases))]
[SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/7299
public async Task RunIndividualTestCase(H2SpecTestCase testCase)