[Components] Support for prerrendering asynchronous components.
* Updates the IComponent interface to rename Init into Configure * Updates the IComponent interface to change SetParameters for SetParametersAsync and make it return a Task that represents when the component is done applying the parameters and potentially triggering one or more renders. * Updates ComponentBase SetParametersAsync to ensure that OnInit(Async) runs before OnParametersSet(Async). * Introduces ParameterCollection.FromDictionary to generate a parameter collection from a dictionary of key value pairs. * Introduces RenderComponentAsync on HtmlRenderer to support prerrendering of async components. * Introduces RenderRootComponentAsync on the renderer to allow for asynchronous prerrendering of the root component.
This commit is contained in:
parent
749092c3f3
commit
19b543e45f
|
|
@ -62,14 +62,16 @@ namespace Test
|
|||
// Arrange
|
||||
AdditionalSyntaxTrees.Add(Parse(@"
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
public class MyComponent : ComponentBase, IComponent
|
||||
{
|
||||
void IComponent.SetParameters(ParameterCollection parameters)
|
||||
Task IComponent.SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}"));
|
||||
|
|
@ -136,14 +138,16 @@ namespace Test
|
|||
// Arrange
|
||||
AdditionalSyntaxTrees.Add(Parse(@"
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
public class MyComponent : ComponentBase, IComponent
|
||||
{
|
||||
void IComponent.SetParameters(ParameterCollection parameters)
|
||||
Task IComponent.SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}"));
|
||||
|
|
|
|||
|
|
@ -162,14 +162,16 @@ namespace Test
|
|||
{
|
||||
// Arrange
|
||||
AdditionalSyntaxTrees.Add(Parse(@"
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
public class MyComponent : ComponentBase, IComponent
|
||||
{
|
||||
void IComponent.SetParameters(ParameterCollection parameters)
|
||||
Task IComponent.SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Layouts;
|
||||
using Microsoft.AspNetCore.Components.Test.Helpers;
|
||||
|
|
@ -149,12 +150,13 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
[Parameter]
|
||||
RenderFragment Body { get; set; }
|
||||
|
||||
public void Init(RenderHandle renderHandle)
|
||||
public void Configure(RenderHandle renderHandle)
|
||||
{
|
||||
}
|
||||
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
public Task SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
|
@ -376,7 +377,13 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
{
|
||||
var renderer = new TestRenderer();
|
||||
renderer.AttachComponent(component);
|
||||
component.SetParameters(ParameterCollection.Empty);
|
||||
var task = component.SetParametersAsync(ParameterCollection.Empty);
|
||||
// we will have to change this method if we add a test that does actual async work.
|
||||
Assert.True(task.Status.HasFlag(TaskStatus.RanToCompletion) || task.Status.HasFlag(TaskStatus.Faulted));
|
||||
if (task.IsFaulted)
|
||||
{
|
||||
ExceptionDispatchInfo.Capture(task.Exception.InnerException).Throw();
|
||||
}
|
||||
return renderer.LatestBatchReferenceFrames;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
// 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.Rendering;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
|
|
@ -49,13 +50,13 @@ namespace Microsoft.AspNetCore.Components
|
|||
bool ICascadingValueComponent.CurrentValueIsFixed => IsFixed;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Init(RenderHandle renderHandle)
|
||||
public void Configure(RenderHandle renderHandle)
|
||||
{
|
||||
_renderHandle = renderHandle;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
public Task SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
// Implementing the parameter binding manually, instead of just calling
|
||||
// parameters.SetParameterProperties(this), is just a very slight perf optimization
|
||||
|
|
@ -129,6 +130,8 @@ namespace Microsoft.AspNetCore.Components
|
|||
{
|
||||
NotifySubscribers();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
bool ICascadingValueComponent.CanSupplyValue(Type requestedType, string requestedName)
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
protected Task InvokeAsync(Func<Task> workItem)
|
||||
=> _renderHandle.InvokeAsync(workItem);
|
||||
|
||||
void IComponent.Init(RenderHandle renderHandle)
|
||||
void IComponent.Configure(RenderHandle renderHandle)
|
||||
{
|
||||
// This implicitly means a ComponentBase can only be associated with a single
|
||||
// renderer. That's the only use case we have right now. If there was ever a need,
|
||||
|
|
@ -174,26 +174,106 @@ namespace Microsoft.AspNetCore.Components
|
|||
/// Method invoked to apply initial or updated parameters to the component.
|
||||
/// </summary>
|
||||
/// <param name="parameters">The parameters to apply.</param>
|
||||
public virtual void SetParameters(ParameterCollection parameters)
|
||||
public virtual Task SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
|
||||
if (!_hasCalledInit)
|
||||
{
|
||||
_hasCalledInit = true;
|
||||
OnInit();
|
||||
return RunInitAndSetParameters();
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// If you override OnInitAsync and return a noncompleted task, then by default
|
||||
// we automatically re-render once that task completes.
|
||||
var initTask = OnInitAsync();
|
||||
ContinueAfterLifecycleTask(initTask);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunInitAndSetParameters()
|
||||
{
|
||||
_hasCalledInit = true;
|
||||
var initIsAsync = false;
|
||||
|
||||
OnInit();
|
||||
Task initTask = null;
|
||||
(initIsAsync, initTask) = ProcessLifeCycletask(OnInitAsync());
|
||||
if (initIsAsync)
|
||||
{
|
||||
// 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.
|
||||
StateHasChanged();
|
||||
await initTask;
|
||||
}
|
||||
|
||||
OnParametersSet();
|
||||
var parametersTask = OnParametersSetAsync();
|
||||
ContinueAfterLifecycleTask(parametersTask);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private (bool isAsync, Task asyncTask) ProcessLifeCycletask(Task task)
|
||||
{
|
||||
if (task == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(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:
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReRenderAsyncTask(Task task)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void ContinueAfterLifecycleTask(Task task)
|
||||
|
|
@ -260,19 +340,24 @@ namespace Microsoft.AspNetCore.Components
|
|||
var onAfterRenderTask = OnAfterRenderAsync();
|
||||
if (onAfterRenderTask != null && onAfterRenderTask.Status != TaskStatus.RanToCompletion)
|
||||
{
|
||||
onAfterRenderTask.ContinueWith(task =>
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
if (task.Exception != null)
|
||||
{
|
||||
HandleException(task.Exception);
|
||||
}
|
||||
});
|
||||
private async Task HandleAfterRenderException(Task parentTask)
|
||||
{
|
||||
try
|
||||
{
|
||||
await parentTask;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
HandleException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -12,12 +14,13 @@ namespace Microsoft.AspNetCore.Components
|
|||
/// Initializes the component.
|
||||
/// </summary>
|
||||
/// <param name="renderHandle">A <see cref="RenderHandle"/> that allows the component to be rendered.</param>
|
||||
void Init(RenderHandle renderHandle);
|
||||
void Configure(RenderHandle renderHandle);
|
||||
|
||||
/// <summary>
|
||||
/// Sets parameters supplied by the component's parent in the render tree.
|
||||
/// </summary>
|
||||
/// <param name="parameters">The parameters.</param>
|
||||
void SetParameters(ParameterCollection parameters);
|
||||
/// <returns>A <see cref="Task"/> that completes when the component has finished updating and rendering itself.</returns>
|
||||
Task SetParametersAsync(ParameterCollection parameters);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
|
||||
|
|
@ -34,16 +35,17 @@ namespace Microsoft.AspNetCore.Components.Layouts
|
|||
IDictionary<string, object> PageParameters { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Init(RenderHandle renderHandle)
|
||||
public void Configure(RenderHandle renderHandle)
|
||||
{
|
||||
_renderHandle = renderHandle;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
public Task SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
Render();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void Render()
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
/// </summary>
|
||||
public readonly struct ParameterCollection
|
||||
{
|
||||
private const string GeneratedParameterCollectionElementName = "__ARTIFICIAL_PARAMETER_COLLECTION";
|
||||
private static readonly RenderTreeFrame[] _emptyCollectionFrames = new RenderTreeFrame[]
|
||||
{
|
||||
RenderTreeFrame.Element(0, string.Empty).WithComponentSubtreeLength(1)
|
||||
|
|
@ -196,5 +197,25 @@ namespace Microsoft.AspNetCore.Components
|
|||
builder.Append(_frames, _ownerIndex + 1, numEntries);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ParameterCollection"/> from the given <see cref="IDictionary{TKey, TValue}"/>.
|
||||
/// </summary>
|
||||
/// <param name="parameters">The <see cref="IDictionary{TKey, TValue}"/> with the parameters.</param>
|
||||
/// <returns>A <see cref="ParameterCollection"/>.</returns>
|
||||
public static ParameterCollection FromDictionary(IDictionary<string, object> parameters)
|
||||
{
|
||||
var frames = new RenderTreeFrame[parameters.Count + 1];
|
||||
frames[0] = RenderTreeFrame.Element(0, GeneratedParameterCollectionElementName)
|
||||
.WithElementSubtreeLength(frames.Length);
|
||||
|
||||
var i = 0;
|
||||
foreach (var kvp in parameters)
|
||||
{
|
||||
frames[++i] = RenderTreeFrame.Attribute(i, kvp.Key, kvp.Value);
|
||||
}
|
||||
|
||||
return new ParameterCollection(frames, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
|
||||
|
|
@ -139,7 +140,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
parameters = parameters.WithCascadingParameters(_cascadingParameters);
|
||||
}
|
||||
|
||||
Component.SetParameters(parameters);
|
||||
_renderer.AddToPendingTasks(Component.SetParametersAsync(parameters));
|
||||
}
|
||||
|
||||
public void NotifyCascadingValueChanged()
|
||||
|
|
@ -148,7 +149,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
? new ParameterCollection(_latestDirectParametersSnapshot.Buffer, 0)
|
||||
: ParameterCollection.Empty;
|
||||
var allParams = directParams.WithCascadingParameters(_cascadingParameters);
|
||||
Component.SetParameters(allParams);
|
||||
var task = Component.SetParametersAsync(allParams);
|
||||
_renderer.AddToPendingTasks(task);
|
||||
}
|
||||
|
||||
private bool AddCascadingParameterSubscriptions()
|
||||
|
|
|
|||
|
|
@ -73,6 +73,42 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public async Task<IEnumerable<string>> RenderComponentAsync(Type componentType, ParameterCollection initialParameters)
|
||||
{
|
||||
var frames = await CreateInitialRenderAsync(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>
|
||||
/// <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 Task<IEnumerable<string>> RenderComponentAsync<T>(ParameterCollection initialParameters) where T : IComponent
|
||||
{
|
||||
return RenderComponentAsync(typeof(T), initialParameters);
|
||||
}
|
||||
|
||||
private int RenderFrames(List<string> result, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
|
||||
{
|
||||
var nextPosition = position;
|
||||
|
|
@ -229,6 +265,16 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
|
||||
return GetCurrentRenderTreeFrames(componentId);
|
||||
}
|
||||
|
||||
private async Task<ArrayRange<RenderTreeFrame>> CreateInitialRenderAsync(Type componentType, ParameterCollection initialParameters)
|
||||
{
|
||||
var component = InstantiateComponent(componentType);
|
||||
var componentId = AssignRootComponentId(component);
|
||||
|
||||
await RenderRootComponentAsync(componentId, initialParameters);
|
||||
|
||||
return GetCurrentRenderTreeFrames(componentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
// 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.RenderTree;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Rendering
|
||||
{
|
||||
|
|
@ -22,6 +24,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it
|
||||
private bool _isBatchInProgress;
|
||||
private int _lastEventHandlerId = 0;
|
||||
private List<Task> _pendingTasks;
|
||||
|
||||
// We need to introduce locking as we don't know if we are executing
|
||||
// under a synchronization context that limits the ammount of concurrency
|
||||
// that can happen when async callbacks are executed.
|
||||
// As a result, we have to protect the _pendingTask list and the
|
||||
// _batchBuilder render queue from concurrent modifications.
|
||||
private object _asyncWorkLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an instance of <see cref="Renderer"/>.
|
||||
|
|
@ -64,8 +74,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="componentId">The ID returned by <see cref="AssignRootComponentId(IComponent)"/>.</param>
|
||||
protected void RenderRootComponent(int componentId)
|
||||
{
|
||||
GetRequiredComponentState(componentId)
|
||||
.SetDirectParameters(ParameterCollection.Empty);
|
||||
RenderRootComponent(componentId, ParameterCollection.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -77,8 +86,131 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <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
|
||||
/// done by any of the components. 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 Task RenderRootComponentAsync(int componentId)
|
||||
{
|
||||
return RenderRootComponentAsync(componentId, ParameterCollection.Empty);
|
||||
}
|
||||
|
||||
/// <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
|
||||
/// done by any of the components. 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 async Task RenderRootComponentAsync(int componentId, ParameterCollection initialParameters)
|
||||
{
|
||||
if (_pendingTasks != 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
|
||||
// remaining work.
|
||||
// During the synchronous rendering process we don't wait for the pending asynchronous
|
||||
// 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);
|
||||
|
||||
try
|
||||
{
|
||||
await ProcessAsynchronousWork();
|
||||
Debug.Assert(_pendingTasks.Count == 0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_pendingTasks = null;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
lock (_asyncWorkLock)
|
||||
{
|
||||
// Create a Task that represents the remaining ongoing work for the rendering process
|
||||
pendingWork = Task.WhenAll(_pendingTasks);
|
||||
|
||||
// Clear all pending work.
|
||||
_pendingTasks.Clear();
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
|
@ -87,7 +219,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
var parentComponentState = GetOptionalComponentState(parentComponentId);
|
||||
var componentState = new ComponentState(this, componentId, component, parentComponentState);
|
||||
_componentStateById.Add(componentId, componentState);
|
||||
component.Init(new RenderHandle(this, componentId));
|
||||
component.Configure(new RenderHandle(this, componentId));
|
||||
return componentState;
|
||||
}
|
||||
|
||||
|
|
@ -167,6 +299,38 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
frame = frame.WithComponent(newComponentState);
|
||||
}
|
||||
|
||||
internal void AddToPendingTasks(Task task)
|
||||
{
|
||||
switch (task == null ? TaskStatus.RanToCompletion : task.Status)
|
||||
{
|
||||
// If it's already completed synchronously, no need to add it to the list of
|
||||
// pending Tasks as no further render (we already rerender synchronously) will.
|
||||
// happen.
|
||||
case TaskStatus.RanToCompletion:
|
||||
case TaskStatus.Canceled:
|
||||
break;
|
||||
case TaskStatus.Faulted:
|
||||
// We want to throw immediately 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();
|
||||
break;
|
||||
default:
|
||||
// We are not in rendering the root component.
|
||||
if (_pendingTasks == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
lock (_asyncWorkLock)
|
||||
{
|
||||
_pendingTasks.Add(task);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
internal void AssignEventHandlerId(ref RenderTreeFrame frame)
|
||||
{
|
||||
var id = ++_lastEventHandlerId;
|
||||
|
|
@ -195,8 +359,11 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
return;
|
||||
}
|
||||
|
||||
_batchBuilder.ComponentRenderQueue.Enqueue(
|
||||
new RenderQueueEntry(componentState, renderFragment));
|
||||
lock (_asyncWorkLock)
|
||||
{
|
||||
_batchBuilder.ComponentRenderQueue.Enqueue(
|
||||
new RenderQueueEntry(componentState, renderFragment));
|
||||
}
|
||||
|
||||
if (!_isBatchInProgress)
|
||||
{
|
||||
|
|
@ -222,9 +389,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
try
|
||||
{
|
||||
// Process render queue until empty
|
||||
while (_batchBuilder.ComponentRenderQueue.Count > 0)
|
||||
while (TryDequeueRenderQueueEntry(out var nextToRender))
|
||||
{
|
||||
var nextToRender = _batchBuilder.ComponentRenderQueue.Dequeue();
|
||||
RenderInExistingBatch(nextToRender);
|
||||
}
|
||||
|
||||
|
|
@ -240,6 +406,23 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
}
|
||||
}
|
||||
|
||||
private bool TryDequeueRenderQueueEntry(out RenderQueueEntry entry)
|
||||
{
|
||||
lock (_asyncWorkLock)
|
||||
{
|
||||
if (_batchBuilder.ComponentRenderQueue.Count > 0)
|
||||
{
|
||||
entry = _batchBuilder.ComponentRenderQueue.Dequeue();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
entry = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InvokeRenderCompletedCalls(ArrayRange<RenderTreeDiff> updatedComponents)
|
||||
{
|
||||
var array = updatedComponents.Array;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Components.Services;
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
|
|
@ -50,7 +51,7 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
[Inject] private IUriHelper UriHelper { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Init(RenderHandle renderHandle)
|
||||
public void Configure(RenderHandle renderHandle)
|
||||
{
|
||||
_renderHandle = renderHandle;
|
||||
|
||||
|
|
@ -59,7 +60,7 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
public Task SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
// Capture the parameters we want to do special things with, plus all as a dictionary
|
||||
parameters.TryGetValue(RenderTreeBuilder.ChildContent, out _childContent);
|
||||
|
|
@ -73,6 +74,7 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
_hrefAbsolute = href == null ? null : UriHelper.ToAbsoluteUri(href).AbsoluteUri;
|
||||
_isActive = ShouldMatch(UriHelper.GetAbsoluteUri());
|
||||
_renderHandle.Render(Render);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Layouts;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.AspNetCore.Components.Services;
|
||||
|
|
@ -38,7 +39,7 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
private RouteTable Routes { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Init(RenderHandle renderHandle)
|
||||
public void Configure(RenderHandle renderHandle)
|
||||
{
|
||||
_renderHandle = renderHandle;
|
||||
_baseUri = UriHelper.GetBaseUri();
|
||||
|
|
@ -47,12 +48,13 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
public Task SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
var types = ComponentResolver.ResolveComponents(AppAssembly);
|
||||
Routes = RouteTable.Create(types);
|
||||
Refresh();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Components.Test.Helpers;
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Test
|
||||
|
|
@ -372,7 +373,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
static CascadingValue<T> CreateCascadingValueComponent<T>(T value, string name = null)
|
||||
{
|
||||
var supplier = new CascadingValue<T>();
|
||||
supplier.Init(new RenderHandle(new TestRenderer(), 0));
|
||||
supplier.Configure(new RenderHandle(new TestRenderer(), 0));
|
||||
|
||||
var supplierParams = new Dictionary<string, object>
|
||||
{
|
||||
|
|
@ -422,10 +423,10 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
|
||||
class TestComponentBase : IComponent
|
||||
{
|
||||
public void Init(RenderHandle renderHandle)
|
||||
public void Configure(RenderHandle renderHandle)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
public Task SetParametersAsync(ParameterCollection parameters)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Components.RenderTree;
|
|||
using Microsoft.AspNetCore.Components.Test.Helpers;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Test
|
||||
|
|
@ -76,7 +77,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
var firstBatch = renderer.Batches.Single();
|
||||
var nestedComponent = FindComponent<CascadingParameterConsumerComponent<string>>(firstBatch, out var nestedComponentId);
|
||||
Assert.Equal(1, nestedComponent.NumRenders);
|
||||
|
||||
|
||||
// Act 2: Render again with updated regular parameter
|
||||
regularParameterValue = "Changed value";
|
||||
component.TriggerRender();
|
||||
|
|
@ -374,10 +375,10 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
[CascadingParameter] T CascadingParameter { get; set; }
|
||||
[Parameter] string RegularParameter { get; set; }
|
||||
|
||||
public override void SetParameters(ParameterCollection parameters)
|
||||
public override async Task SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
NumSetParametersCalls++;
|
||||
base.SetParameters(parameters);
|
||||
await base.SetParametersAsync(parameters);
|
||||
}
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
|
|
|
|||
|
|
@ -193,23 +193,22 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
// Assert
|
||||
Assert.Single(renderer.Batches);
|
||||
|
||||
// Completes task started by OnParametersSetAsync
|
||||
// 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);
|
||||
|
||||
// Completes task started by OnParametersSetAsync
|
||||
component.Counter = 3;
|
||||
parametersSetTask.SetResult(false);
|
||||
|
||||
// Component should be rendered again
|
||||
Assert.Equal(2, renderer.Batches.Count);
|
||||
|
||||
// Completes task started by OnInitAsync
|
||||
// NOTE: We will probably change this behavior. It would make more sense for the base class
|
||||
// to wait until InitAsync is completed before proceeding with SetParametersAsync, rather
|
||||
// that running the two lifecycle methods in parallel. This will come up as a requirement
|
||||
// when implementing async server-side prerendering.
|
||||
component.Counter = 3;
|
||||
initTask.SetResult(true);
|
||||
|
||||
// Component should be rendered again
|
||||
Assert.Equal(3, renderer.Batches.Count);
|
||||
// after the async part of onparameterssetasync completes
|
||||
Assert.Equal(4, renderer.Batches.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -232,8 +231,9 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
component.Counter = 2;
|
||||
initTask.SetCanceled();
|
||||
|
||||
// Component should not be rendered again
|
||||
Assert.Single(renderer.Batches);
|
||||
// Component should only be rendered again due to
|
||||
// the call to StateHasChanged after SetParametersAsync
|
||||
Assert.Equal(2,renderer.Batches.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
// 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 Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Test.Helpers;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Test
|
||||
|
|
@ -193,10 +194,10 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
// not throw, then be sure also to add a test to verify that injection
|
||||
// occurs before lifecycle methods.
|
||||
|
||||
public void Init(RenderHandle renderHandle)
|
||||
public void Configure(RenderHandle renderHandle)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
public Task SetParametersAsync(ParameterCollection parameters)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.AspNetCore.Components.Test.Helpers;
|
||||
|
|
@ -298,10 +299,10 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
|
||||
class FakeComponent : IComponent
|
||||
{
|
||||
public void Init(RenderHandle renderHandle)
|
||||
public void Configure(RenderHandle renderHandle)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
public Task SetParametersAsync(ParameterCollection parameters)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
// 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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Test
|
||||
|
|
@ -239,6 +239,37 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromDictionary_CanBeInitializedWithEmptyDictionary()
|
||||
{
|
||||
// Arrange
|
||||
var dictionary = new Dictionary<string, object>();
|
||||
|
||||
// Act
|
||||
var collection = ParameterCollection.FromDictionary(dictionary);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(collection.ToDictionary());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromDictionary_RoundTrips()
|
||||
{
|
||||
// Arrange
|
||||
var dictionary = new Dictionary<string, object>
|
||||
{
|
||||
["IntValue"] = 1,
|
||||
["StringValue"] = "String"
|
||||
};
|
||||
|
||||
// Act
|
||||
var collection = ParameterCollection.FromDictionary(dictionary);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(dictionary, collection.ToDictionary());
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void CanConvertToReadOnlyDictionary()
|
||||
{
|
||||
|
|
@ -311,10 +342,10 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
|
||||
private class FakeComponent : IComponent
|
||||
{
|
||||
public void Init(RenderHandle renderHandle)
|
||||
public void Configure(RenderHandle renderHandle)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
public Task SetParametersAsync(ParameterCollection parameters)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1049,9 +1049,9 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
|
||||
private class TestComponent : IComponent
|
||||
{
|
||||
public void Init(RenderHandle renderHandle) { }
|
||||
public void Configure(RenderHandle renderHandle) { }
|
||||
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
public Task SetParametersAsync(ParameterCollection parameters)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1554,35 +1554,35 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
|
||||
public string NonParameterProperty { get; set; }
|
||||
|
||||
public void Init(RenderHandle renderHandle) { }
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
public void Configure(RenderHandle renderHandle) { }
|
||||
public Task SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeComponent2 : IComponent
|
||||
{
|
||||
public void Init(RenderHandle renderHandle)
|
||||
public void Configure(RenderHandle renderHandle)
|
||||
{
|
||||
}
|
||||
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
{
|
||||
}
|
||||
public Task SetParametersAsync(ParameterCollection parameters) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
private class CaptureSetParametersComponent : IComponent
|
||||
{
|
||||
public int SetParametersCallCount { get; private set; }
|
||||
|
||||
public void Init(RenderHandle renderHandle)
|
||||
public void Configure(RenderHandle renderHandle)
|
||||
{
|
||||
}
|
||||
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
public Task SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
SetParametersCallCount++;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1591,16 +1591,16 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
public int DisposalCount { get; private set; }
|
||||
public void Dispose() => DisposalCount++;
|
||||
|
||||
public void Init(RenderHandle renderHandle) { }
|
||||
public void Configure(RenderHandle renderHandle) { }
|
||||
|
||||
public void SetParameters(ParameterCollection parameters) { }
|
||||
public Task SetParametersAsync(ParameterCollection parameters) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
private class NonDisposableComponent : IComponent
|
||||
{
|
||||
public void Init(RenderHandle renderHandle) { }
|
||||
public void Configure(RenderHandle renderHandle) { }
|
||||
|
||||
public void SetParameters(ParameterCollection parameters) { }
|
||||
public Task SetParametersAsync(ParameterCollection parameters) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static void AssertEdit(
|
||||
|
|
|
|||
|
|
@ -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.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -161,6 +162,251 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanRenderAsyncTopLevelComponents()
|
||||
{
|
||||
// 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.
|
||||
|
||||
// Act
|
||||
var componentId = renderer.AssignRootComponentId(component);
|
||||
await renderer.RenderRootComponentAsync(componentId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(5, renderer.Batches.Count);
|
||||
|
||||
// First render
|
||||
var create = renderer.Batches[0];
|
||||
var diff = create.DiffsByComponentId[componentId].Single();
|
||||
Assert.Collection(diff.Edits,
|
||||
edit =>
|
||||
{
|
||||
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
||||
Assert.Equal(0, edit.ReferenceFrameIndex);
|
||||
});
|
||||
AssertFrame.Element(create.ReferenceFrames[0], "p", 2);
|
||||
AssertFrame.Text(create.ReferenceFrames[1], "5");
|
||||
|
||||
// Second render
|
||||
for (int i = 1; i < 5; i++)
|
||||
{
|
||||
|
||||
var update = renderer.Batches[i];
|
||||
var updateDiff = update.DiffsByComponentId[componentId].Single();
|
||||
Assert.Collection(updateDiff.Edits,
|
||||
edit =>
|
||||
{
|
||||
Assert.Equal(RenderTreeEditType.StepIn, edit.Type);
|
||||
},
|
||||
edit =>
|
||||
{
|
||||
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
|
||||
},
|
||||
edit =>
|
||||
{
|
||||
Assert.Equal(RenderTreeEditType.StepOut, edit.Type);
|
||||
});
|
||||
AssertFrame.Text(update.ReferenceFrames[0], (5 - i).ToString());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanRenderAsyncNestedComponents()
|
||||
{
|
||||
// Arrange
|
||||
var renderer = new TestRenderer();
|
||||
var component = new NestedAsyncComponent();
|
||||
|
||||
// Act/Assert
|
||||
var componentId = renderer.AssignRootComponentId(component);
|
||||
var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
|
||||
await renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
|
||||
{
|
||||
[0] = new List<NestedAsyncComponent.ExecutionAction>
|
||||
{
|
||||
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInit),
|
||||
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
|
||||
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSet),
|
||||
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async: true),
|
||||
},
|
||||
[1] = new List<NestedAsyncComponent.ExecutionAction>
|
||||
{
|
||||
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInit),
|
||||
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
|
||||
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSet),
|
||||
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async: true),
|
||||
}
|
||||
},
|
||||
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
|
||||
{
|
||||
[0] = CreateRenderFactory(new[] { 1 }),
|
||||
[1] = CreateRenderFactory(Array.Empty<int>())
|
||||
},
|
||||
[nameof(NestedAsyncComponent.Log)] = log
|
||||
}));
|
||||
|
||||
var logForParent = log.Where(l => l.id == 0).ToArray();
|
||||
var logForChild = log.Where(l => l.id == 1).ToArray();
|
||||
|
||||
AssertStream(0, logForParent);
|
||||
AssertStream(1, logForChild);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanRenderAsyncComponentsWithSyncChildComponents()
|
||||
{
|
||||
// Arrange
|
||||
var renderer = new TestRenderer();
|
||||
var component = new NestedAsyncComponent();
|
||||
|
||||
// Act/Assert
|
||||
var componentId = renderer.AssignRootComponentId(component);
|
||||
var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
|
||||
await renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
|
||||
{
|
||||
[0] = new List<NestedAsyncComponent.ExecutionAction>
|
||||
{
|
||||
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInit),
|
||||
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
|
||||
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSet),
|
||||
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async: true),
|
||||
},
|
||||
[1] = new List<NestedAsyncComponent.ExecutionAction>
|
||||
{
|
||||
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInit),
|
||||
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInitAsyncAsync),
|
||||
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSet),
|
||||
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync),
|
||||
}
|
||||
},
|
||||
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
|
||||
{
|
||||
[0] = CreateRenderFactory(new[] { 1 }),
|
||||
[1] = CreateRenderFactory(Array.Empty<int>())
|
||||
},
|
||||
[nameof(NestedAsyncComponent.Log)] = log
|
||||
}));
|
||||
|
||||
var logForParent = log.Where(l => l.id == 0).ToArray();
|
||||
var logForChild = log.Where(l => l.id == 1).ToArray();
|
||||
|
||||
AssertStream(0, logForParent);
|
||||
AssertStream(1, logForChild);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanRenderAsyncComponentsWithAsyncChildInit()
|
||||
{
|
||||
// Arrange
|
||||
var renderer = new TestRenderer();
|
||||
var component = new NestedAsyncComponent();
|
||||
|
||||
// Act/Assert
|
||||
var componentId = renderer.AssignRootComponentId(component);
|
||||
var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
|
||||
await renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
|
||||
{
|
||||
[0] = new List<NestedAsyncComponent.ExecutionAction>
|
||||
{
|
||||
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInit),
|
||||
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
|
||||
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSet),
|
||||
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async: true),
|
||||
},
|
||||
[1] = new List<NestedAsyncComponent.ExecutionAction>
|
||||
{
|
||||
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInit),
|
||||
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
|
||||
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSet),
|
||||
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync),
|
||||
}
|
||||
},
|
||||
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
|
||||
{
|
||||
[0] = CreateRenderFactory(new[] { 1 }),
|
||||
[1] = CreateRenderFactory(Array.Empty<int>())
|
||||
},
|
||||
[nameof(NestedAsyncComponent.Log)] = log
|
||||
}));
|
||||
|
||||
var logForParent = log.Where(l => l.id == 0).ToArray();
|
||||
var logForChild = log.Where(l => l.id == 1).ToArray();
|
||||
|
||||
AssertStream(0, logForParent);
|
||||
AssertStream(1, logForChild);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanRenderAsyncComponentsWithMultipleAsyncChildren()
|
||||
{
|
||||
// Arrange
|
||||
var renderer = new TestRenderer();
|
||||
var component = new NestedAsyncComponent();
|
||||
|
||||
// Act/Assert
|
||||
var componentId = renderer.AssignRootComponentId(component);
|
||||
var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
|
||||
await renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
|
||||
{
|
||||
[0] = new List<NestedAsyncComponent.ExecutionAction>
|
||||
{
|
||||
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInit),
|
||||
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
|
||||
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSet),
|
||||
NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async: true),
|
||||
},
|
||||
[1] = new List<NestedAsyncComponent.ExecutionAction>
|
||||
{
|
||||
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInit),
|
||||
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
|
||||
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSet),
|
||||
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async:true),
|
||||
},
|
||||
[2] = new List<NestedAsyncComponent.ExecutionAction>
|
||||
{
|
||||
NestedAsyncComponent.ExecutionAction.On(2, NestedAsyncComponent.EventType.OnInit),
|
||||
NestedAsyncComponent.ExecutionAction.On(2, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
|
||||
NestedAsyncComponent.ExecutionAction.On(2, NestedAsyncComponent.EventType.OnParametersSet),
|
||||
NestedAsyncComponent.ExecutionAction.On(2, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async:true),
|
||||
},
|
||||
[3] = new List<NestedAsyncComponent.ExecutionAction>
|
||||
{
|
||||
NestedAsyncComponent.ExecutionAction.On(3, NestedAsyncComponent.EventType.OnInit),
|
||||
NestedAsyncComponent.ExecutionAction.On(3, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
|
||||
NestedAsyncComponent.ExecutionAction.On(3, NestedAsyncComponent.EventType.OnParametersSet),
|
||||
NestedAsyncComponent.ExecutionAction.On(3, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async:true),
|
||||
}
|
||||
},
|
||||
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
|
||||
{
|
||||
[0] = CreateRenderFactory(new[] { 1, 2 }),
|
||||
[1] = CreateRenderFactory(new[] { 3 }),
|
||||
[2] = CreateRenderFactory(Array.Empty<int>()),
|
||||
[3] = CreateRenderFactory(Array.Empty<int>())
|
||||
},
|
||||
[nameof(NestedAsyncComponent.Log)] = log
|
||||
}));
|
||||
|
||||
var logForParent = log.Where(l => l.id == 0).ToArray();
|
||||
var logForFirstChild = log.Where(l => l.id == 1).ToArray();
|
||||
var logForSecondChild = log.Where(l => l.id == 2).ToArray();
|
||||
var logForThirdChild = log.Where(l => l.id == 3).ToArray();
|
||||
|
||||
AssertStream(0, logForParent);
|
||||
AssertStream(1, logForFirstChild);
|
||||
AssertStream(2, logForSecondChild);
|
||||
AssertStream(3, logForThirdChild);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanDispatchEventsToTopLevelComponents()
|
||||
{
|
||||
|
|
@ -1233,13 +1479,16 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
_renderFragment = renderFragment;
|
||||
}
|
||||
|
||||
public void Init(RenderHandle renderHandle)
|
||||
public void Configure(RenderHandle renderHandle)
|
||||
{
|
||||
_renderHandle = renderHandle;
|
||||
}
|
||||
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
=> TriggerRender();
|
||||
public Task SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
TriggerRender();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void TriggerRender()
|
||||
=> _renderHandle.Render(_renderFragment);
|
||||
|
|
@ -1273,11 +1522,14 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
|
||||
public RenderHandle RenderHandle { get; private set; }
|
||||
|
||||
public void Init(RenderHandle renderHandle)
|
||||
public void Configure(RenderHandle renderHandle)
|
||||
=> RenderHandle = renderHandle;
|
||||
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
=> parameters.SetParameterProperties(this);
|
||||
public Task SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private class EventComponent : AutoRenderComponent, IComponent, IHandleEvent
|
||||
|
|
@ -1337,7 +1589,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.AddContent(0, "Parent here");
|
||||
|
||||
|
||||
if (IncludeChild)
|
||||
{
|
||||
builder.OpenComponent<T>(1);
|
||||
|
|
@ -1353,7 +1605,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class ReRendersParentComponent : AutoRenderComponent
|
||||
{
|
||||
[Parameter]
|
||||
|
|
@ -1380,13 +1632,14 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
|
||||
private RenderHandle _renderHandle;
|
||||
|
||||
public void Init(RenderHandle renderHandle)
|
||||
public void Configure(RenderHandle renderHandle)
|
||||
=> _renderHandle = renderHandle;
|
||||
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
public Task SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
Render();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void HandleEvent(EventHandlerInvoker binding, UIEventArgs args)
|
||||
|
|
@ -1409,11 +1662,12 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
private readonly List<RenderHandle> _renderHandles
|
||||
= new List<RenderHandle>();
|
||||
|
||||
public void Init(RenderHandle renderHandle)
|
||||
public void Configure(RenderHandle renderHandle)
|
||||
=> _renderHandles.Add(renderHandle);
|
||||
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
public Task SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void TriggerRender()
|
||||
|
|
@ -1463,9 +1717,10 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
OnAfterRenderCallCount++;
|
||||
}
|
||||
|
||||
void IComponent.SetParameters(ParameterCollection parameters)
|
||||
Task IComponent.SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
TriggerRender();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
|
|
@ -1501,5 +1756,229 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
return NextUpdateDisplayReturnTask;
|
||||
}
|
||||
}
|
||||
|
||||
private class AsyncComponent : IComponent
|
||||
{
|
||||
private RenderHandle _renderHandler;
|
||||
|
||||
public AsyncComponent(int number)
|
||||
{
|
||||
Number = number;
|
||||
}
|
||||
|
||||
public int Number { get; set; }
|
||||
|
||||
public void Configure(RenderHandle renderHandle)
|
||||
{
|
||||
_renderHandler = renderHandle;
|
||||
}
|
||||
|
||||
public async Task SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
int n;
|
||||
while (Number > 0)
|
||||
{
|
||||
n = Number;
|
||||
_renderHandler.Render(CreateFragment);
|
||||
Number--;
|
||||
await Task.Yield();
|
||||
};
|
||||
|
||||
// Cheap closure
|
||||
void CreateFragment(RenderTreeBuilder builder)
|
||||
{
|
||||
var s = 0;
|
||||
builder.OpenElement(s++, "p");
|
||||
builder.AddContent(s++, n);
|
||||
builder.CloseElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AssertStream(int expectedId, (int id, NestedAsyncComponent.EventType @event)[] logStream)
|
||||
{
|
||||
// OnInit runs first
|
||||
Assert.Equal((expectedId, NestedAsyncComponent.EventType.OnInit), logStream[0]);
|
||||
|
||||
// OnInit async completes
|
||||
Assert.Single(logStream.Skip(1),
|
||||
e => e == (expectedId, NestedAsyncComponent.EventType.OnInitAsyncAsync) || e == (expectedId, NestedAsyncComponent.EventType.OnInitAsyncSync));
|
||||
|
||||
var parametersSetEvent = logStream.Where(le => le == (expectedId, NestedAsyncComponent.EventType.OnParametersSet)).ToArray();
|
||||
// OnParametersSet gets called at least once
|
||||
Assert.NotEmpty(parametersSetEvent);
|
||||
|
||||
var parametersSetAsyncEvent = logStream
|
||||
.Where(le => le == (expectedId, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync) ||
|
||||
le == (expectedId, NestedAsyncComponent.EventType.OnParametersSetAsyncSync))
|
||||
.ToArray();
|
||||
// OnParametersSetAsync async gets called at least once
|
||||
Assert.NotEmpty(parametersSetAsyncEvent);
|
||||
|
||||
// The same number of OnParametersSet and OnParametersSetAsync get produced
|
||||
Assert.Equal(parametersSetEvent.Length, parametersSetAsyncEvent.Length);
|
||||
|
||||
// The log ends with an OnParametersSetAsync event
|
||||
Assert.True(logStream.Last() == (expectedId, NestedAsyncComponent.EventType.OnParametersSetAsyncSync) ||
|
||||
logStream.Last() == (expectedId, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync));
|
||||
}
|
||||
|
||||
private Func<NestedAsyncComponent, RenderFragment> CreateRenderFactory(int[] childrenToRender)
|
||||
{
|
||||
// For some reason nameof doesn't work inside a nested lambda, so capturing the value here.
|
||||
var eventActionsName = nameof(NestedAsyncComponent.EventActions);
|
||||
var whatToRenderName = nameof(NestedAsyncComponent.WhatToRender);
|
||||
var testIdName = nameof(NestedAsyncComponent.TestId);
|
||||
var logName = nameof(NestedAsyncComponent.Log);
|
||||
|
||||
return component => builder =>
|
||||
{
|
||||
int s = 0;
|
||||
builder.OpenElement(s++, "div");
|
||||
builder.AddContent(s++, $"Id: {component.TestId} BuildRenderTree, {Guid.NewGuid()}");
|
||||
foreach (var child in childrenToRender)
|
||||
{
|
||||
builder.OpenComponent<NestedAsyncComponent>(s++);
|
||||
builder.AddAttribute(s++, eventActionsName, component.EventActions);
|
||||
builder.AddAttribute(s++, whatToRenderName, component.WhatToRender);
|
||||
builder.AddAttribute(s++, testIdName, child);
|
||||
builder.AddAttribute(s++, logName, component.Log);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
builder.CloseElement();
|
||||
};
|
||||
}
|
||||
|
||||
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; }
|
||||
|
||||
[Parameter] public int TestId { get; set; }
|
||||
|
||||
[Parameter] public ConcurrentQueue<(int testId, EventType @event)> Log { get; set; }
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
if (TryGetEntry(EventType.OnInit, out var entry))
|
||||
{
|
||||
var result = entry.EventAction();
|
||||
LogResult(result.Result);
|
||||
}
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
protected override async Task OnInitAsync()
|
||||
{
|
||||
if (TryGetEntry(EventType.OnInitAsyncSync, out var entrySync))
|
||||
{
|
||||
var result = await entrySync.EventAction();
|
||||
LogResult(result);
|
||||
}
|
||||
else if (TryGetEntry(EventType.OnInitAsyncAsync, out var entryAsync))
|
||||
{
|
||||
var result = await entryAsync.EventAction();
|
||||
LogResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (TryGetEntry(EventType.OnParametersSet, out var entry))
|
||||
{
|
||||
var result = entry.EventAction();
|
||||
LogResult(result.Result);
|
||||
}
|
||||
base.OnParametersSet();
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (TryGetEntry(EventType.OnParametersSetAsyncSync, out var entrySync))
|
||||
{
|
||||
var result = await entrySync.EventAction();
|
||||
LogResult(result);
|
||||
|
||||
await entrySync.EventAction();
|
||||
}
|
||||
else if (TryGetEntry(EventType.OnParametersSetAsyncAsync, out var entryAsync))
|
||||
{
|
||||
var result = await entryAsync.EventAction();
|
||||
LogResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
base.BuildRenderTree(builder);
|
||||
var renderFactory = WhatToRender[TestId];
|
||||
renderFactory(this)(builder);
|
||||
}
|
||||
|
||||
private bool TryGetEntry(EventType eventType, out ExecutionAction entry)
|
||||
{
|
||||
var entries = EventActions[TestId];
|
||||
if (entries == null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to find entries for component with Id: " + TestId);
|
||||
}
|
||||
entry = entries.FirstOrDefault(e => e.Event == eventType);
|
||||
return entry != null;
|
||||
}
|
||||
|
||||
private void LogResult((int, EventType) entry)
|
||||
{
|
||||
Log.Enqueue(entry);
|
||||
}
|
||||
|
||||
public class ExecutionAction
|
||||
{
|
||||
public EventType Event { get; set; }
|
||||
public Func<Task<(int id, EventType @event)>> EventAction { get; set; }
|
||||
|
||||
public static ExecutionAction On(int id, EventType @event, bool async = false)
|
||||
{
|
||||
if (!async)
|
||||
{
|
||||
return new ExecutionAction
|
||||
{
|
||||
Event = @event,
|
||||
EventAction = () => Task.FromResult((id, @event))
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ExecutionAction
|
||||
{
|
||||
Event = @event,
|
||||
EventAction = async () =>
|
||||
{
|
||||
await Task.Yield();
|
||||
return (id, @event);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum EventType
|
||||
{
|
||||
OnInit,
|
||||
OnInitAsyncSync,
|
||||
OnInitAsyncAsync,
|
||||
OnParametersSet,
|
||||
OnParametersSetAsyncSync,
|
||||
OnParametersSetAsyncAsync
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit;
|
||||
|
|
@ -385,7 +387,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
{
|
||||
public RenderHandle RenderHandle { get; private set; }
|
||||
|
||||
public void Init(RenderHandle renderHandle)
|
||||
public void Configure(RenderHandle renderHandle)
|
||||
{
|
||||
RenderHandle = renderHandle;
|
||||
}
|
||||
|
|
@ -393,9 +395,106 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
[Inject]
|
||||
Func<ParameterCollection, RenderFragment> CreateRenderFragment { get; set; }
|
||||
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
public Task SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
RenderHandle.Render(CreateRenderFragment(parameters));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanRender_AsyncComponent()
|
||||
{
|
||||
// Arrange
|
||||
var expectedHtml = new[] {
|
||||
"<", "p", ">", "20", "</", "p", ">" };
|
||||
var serviceProvider = new ServiceCollection().AddSingleton<AsyncComponent>().BuildServiceProvider();
|
||||
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||
|
||||
// Act
|
||||
var result = await htmlRenderer.RenderComponentAsync<AsyncComponent>(ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
["Value"] = 10
|
||||
}));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedHtml, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanRender_NestedAsyncComponents()
|
||||
{
|
||||
// Arrange
|
||||
var expectedHtml = new[] {
|
||||
"<", "p", ">", "20", "</", "p", ">",
|
||||
"<", "p", ">", "80", "</", "p", ">"
|
||||
};
|
||||
|
||||
var serviceProvider = new ServiceCollection().AddSingleton<AsyncComponent>().BuildServiceProvider();
|
||||
|
||||
var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
|
||||
|
||||
// Act
|
||||
var result = await htmlRenderer.RenderComponentAsync<NestedAsyncComponent>(ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
["Nested"] = false,
|
||||
["Value"] = 10
|
||||
}));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedHtml, result);
|
||||
}
|
||||
|
||||
|
||||
private class NestedAsyncComponent : ComponentBase
|
||||
{
|
||||
[Parameter] public bool Nested { get; set; }
|
||||
[Parameter] public int Value { get; set; }
|
||||
|
||||
protected override async Task OnInitAsync()
|
||||
{
|
||||
Value = Value * 2;
|
||||
await Task.Yield();
|
||||
}
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
base.BuildRenderTree(builder);
|
||||
builder.OpenElement(0, "p");
|
||||
builder.AddContent(1, Value.ToString());
|
||||
builder.CloseElement();
|
||||
if (!Nested)
|
||||
{
|
||||
builder.OpenComponent<NestedAsyncComponent>(2);
|
||||
builder.AddAttribute(3, "Nested", true);
|
||||
builder.AddAttribute(4, "Value", Value * 2);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class AsyncComponent : ComponentBase
|
||||
{
|
||||
public AsyncComponent()
|
||||
{
|
||||
}
|
||||
|
||||
[Parameter]
|
||||
public int Value { get; set; }
|
||||
|
||||
protected override async Task OnInitAsync()
|
||||
{
|
||||
Value = Value * 2;
|
||||
await Task.Delay(Value * 100);
|
||||
}
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
base.BuildRenderTree(builder);
|
||||
builder.OpenElement(0, "p");
|
||||
builder.AddContent(1, Value.ToString());
|
||||
builder.CloseElement();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -403,14 +502,15 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
{
|
||||
private RenderHandle _renderHandle;
|
||||
|
||||
public void Init(RenderHandle renderHandle)
|
||||
public void Configure(RenderHandle renderHandle)
|
||||
{
|
||||
_renderHandle = renderHandle;
|
||||
}
|
||||
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
public Task SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
_renderHandle.Render(CreateRenderFragment(parameters));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private RenderFragment CreateRenderFragment(ParameterCollection parameters)
|
||||
|
|
@ -433,14 +533,15 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
[Inject]
|
||||
public RenderFragment Fragment { get; set; }
|
||||
|
||||
public void Init(RenderHandle renderHandle)
|
||||
public void Configure(RenderHandle renderHandle)
|
||||
{
|
||||
_renderHandle = renderHandle;
|
||||
}
|
||||
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
public Task SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
_renderHandle.Render(Fragment);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -363,10 +363,10 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
|
||||
class FakeComponent : IComponent
|
||||
{
|
||||
public void Init(RenderHandle renderHandle)
|
||||
public void Configure(RenderHandle renderHandle)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
public Task SetParametersAsync(ParameterCollection parameters)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// 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;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
|
||||
|
|
@ -10,15 +11,16 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
|
|||
{
|
||||
private RenderHandle _renderHandle;
|
||||
|
||||
public void Init(RenderHandle renderHandle)
|
||||
public void Configure(RenderHandle renderHandle)
|
||||
{
|
||||
_renderHandle = renderHandle;
|
||||
}
|
||||
|
||||
public virtual void SetParameters(ParameterCollection parameters)
|
||||
public virtual Task SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
TriggerRender();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void TriggerRender()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
// 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 Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Test.Helpers
|
||||
{
|
||||
|
|
@ -13,7 +14,7 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
|
|||
this IComponent component,
|
||||
Dictionary<string, object> parameters)
|
||||
{
|
||||
component.SetParameters(DictionaryToParameterCollection(parameters));
|
||||
component.SetParametersAsync(DictionaryToParameterCollection(parameters));
|
||||
}
|
||||
|
||||
private static ParameterCollection DictionaryToParameterCollection(
|
||||
|
|
@ -32,8 +33,8 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
|
|||
|
||||
private abstract class AbstractComponent : IComponent
|
||||
{
|
||||
public abstract void Init(RenderHandle renderHandle);
|
||||
public abstract void SetParameters(ParameterCollection parameters);
|
||||
public abstract void Configure(RenderHandle renderHandle);
|
||||
public abstract Task SetParametersAsync(ParameterCollection parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,12 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
|
|||
public new void RenderRootComponent(int componentId)
|
||||
=> base.RenderRootComponent(componentId);
|
||||
|
||||
public new Task RenderRootComponentAsync(int componentId)
|
||||
=> base.RenderRootComponentAsync(componentId);
|
||||
|
||||
public new Task RenderRootComponentAsync(int componentId, ParameterCollection parameters)
|
||||
=> base.RenderRootComponentAsync(componentId, parameters);
|
||||
|
||||
public new void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs args)
|
||||
=> base.DispatchEvent(componentId, eventHandlerId, args);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue