[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:
Javier Calvarro Nelson 2019-01-10 02:37:13 -08:00 committed by Artak
parent 749092c3f3
commit 19b543e45f
28 changed files with 1108 additions and 120 deletions

View File

@ -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;
}
}
}"));

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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)

View File

@ -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);
}
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
@ -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);
}
}

View File

@ -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()

View File

@ -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);
}
}
}

View File

@ -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()

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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 />

View File

@ -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 />

View File

@ -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();
}

View File

@ -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)

View File

@ -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]

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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(

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.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
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}

View File

@ -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()

View File

@ -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);
}
}
}

View File

@ -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);