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:
parent
758ba235fa
commit
cddbc2e888
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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++)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue