diff --git a/src/Components/Blazor/Build/test/BindRazorIntegrationTest.cs b/src/Components/Blazor/Build/test/BindRazorIntegrationTest.cs
index 0f217f4e1e..7e001e6c30 100644
--- a/src/Components/Blazor/Build/test/BindRazorIntegrationTest.cs
+++ b/src/Components/Blazor/Build/test/BindRazorIntegrationTest.cs
@@ -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;
}
}
}"));
diff --git a/src/Components/Blazor/Build/test/ComponentRenderingRazorIntegrationTest.cs b/src/Components/Blazor/Build/test/ComponentRenderingRazorIntegrationTest.cs
index 0226c74ba4..2078a3e1df 100644
--- a/src/Components/Blazor/Build/test/ComponentRenderingRazorIntegrationTest.cs
+++ b/src/Components/Blazor/Build/test/ComponentRenderingRazorIntegrationTest.cs
@@ -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;
}
}
}
diff --git a/src/Components/Blazor/Build/test/DirectiveRazorIntegrationTest.cs b/src/Components/Blazor/Build/test/DirectiveRazorIntegrationTest.cs
index 14485e1e61..a82da4b755 100644
--- a/src/Components/Blazor/Build/test/DirectiveRazorIntegrationTest.cs
+++ b/src/Components/Blazor/Build/test/DirectiveRazorIntegrationTest.cs
@@ -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;
}
}
diff --git a/src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs b/src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs
index 799f995cde..c3f207e299 100644
--- a/src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs
+++ b/src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs
@@ -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;
}
diff --git a/src/Components/Components/src/CascadingValue.cs b/src/Components/Components/src/CascadingValue.cs
index 53e356ae2e..32323bac3f 100644
--- a/src/Components/Components/src/CascadingValue.cs
+++ b/src/Components/Components/src/CascadingValue.cs
@@ -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;
///
- public void Init(RenderHandle renderHandle)
+ public void Configure(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}
///
- 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)
diff --git a/src/Components/Components/src/ComponentBase.cs b/src/Components/Components/src/ComponentBase.cs
index a4b718761b..e492c81f17 100644
--- a/src/Components/Components/src/ComponentBase.cs
+++ b/src/Components/Components/src/ComponentBase.cs
@@ -157,7 +157,7 @@ namespace Microsoft.AspNetCore.Components
protected Task InvokeAsync(Func 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.
///
/// The parameters to apply.
- 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);
}
}
}
diff --git a/src/Components/Components/src/IComponent.cs b/src/Components/Components/src/IComponent.cs
index 69f20fc7ca..d775efdbd9 100644
--- a/src/Components/Components/src/IComponent.cs
+++ b/src/Components/Components/src/IComponent.cs
@@ -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
{
///
@@ -12,12 +14,13 @@ namespace Microsoft.AspNetCore.Components
/// Initializes the component.
///
/// A that allows the component to be rendered.
- void Init(RenderHandle renderHandle);
+ void Configure(RenderHandle renderHandle);
///
/// Sets parameters supplied by the component's parent in the render tree.
///
/// The parameters.
- void SetParameters(ParameterCollection parameters);
+ /// A that completes when the component has finished updating and rendering itself.
+ Task SetParametersAsync(ParameterCollection parameters);
}
}
diff --git a/src/Components/Components/src/Layouts/LayoutDisplay.cs b/src/Components/Components/src/Layouts/LayoutDisplay.cs
index afd205c40d..cd3baaf165 100644
--- a/src/Components/Components/src/Layouts/LayoutDisplay.cs
+++ b/src/Components/Components/src/Layouts/LayoutDisplay.cs
@@ -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 PageParameters { get; set; }
///
- 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;
}
private void Render()
diff --git a/src/Components/Components/src/ParameterCollection.cs b/src/Components/Components/src/ParameterCollection.cs
index 18915c67db..a929c8071e 100644
--- a/src/Components/Components/src/ParameterCollection.cs
+++ b/src/Components/Components/src/ParameterCollection.cs
@@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Components
///
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);
}
}
+
+ ///
+ /// Creates a new from the given .
+ ///
+ /// The with the parameters.
+ /// A .
+ public static ParameterCollection FromDictionary(IDictionary 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);
+ }
}
}
diff --git a/src/Components/Components/src/Rendering/ComponentState.cs b/src/Components/Components/src/Rendering/ComponentState.cs
index b7d38c6cd4..7dd758055b 100644
--- a/src/Components/Components/src/Rendering/ComponentState.cs
+++ b/src/Components/Components/src/Rendering/ComponentState.cs
@@ -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()
diff --git a/src/Components/Components/src/Rendering/HtmlRenderer.cs b/src/Components/Components/src/Rendering/HtmlRenderer.cs
index 1918e85b44..315c287b81 100644
--- a/src/Components/Components/src/Rendering/HtmlRenderer.cs
+++ b/src/Components/Components/src/Rendering/HtmlRenderer.cs
@@ -73,6 +73,42 @@ namespace Microsoft.AspNetCore.Components.Rendering
}
}
+ ///
+ /// Renders a component into a sequence of fragments that represent the textual representation
+ /// of the HTML produced by the component.
+ ///
+ /// The type of the .
+ /// A with the initial parameters to render the component.
+ /// A sequence of fragments that represent the HTML text of the component.
+ public async Task> RenderComponentAsync(Type componentType, ParameterCollection initialParameters)
+ {
+ var frames = await CreateInitialRenderAsync(componentType, initialParameters);
+
+ if (frames.Count == 0)
+ {
+ return Array.Empty();
+ }
+ else
+ {
+ var result = new List();
+ var newPosition = RenderFrames(result, frames, 0, frames.Count);
+ Debug.Assert(newPosition == frames.Count);
+ return result;
+ }
+ }
+
+ ///
+ /// Renders a component into a sequence of fragments that represent the textual representation
+ /// of the HTML produced by the component.
+ ///
+ /// The type of the .
+ /// A with the initial parameters to render the component.
+ /// A sequence of fragments that represent the HTML text of the component.
+ public Task> RenderComponentAsync(ParameterCollection initialParameters) where T : IComponent
+ {
+ return RenderComponentAsync(typeof(T), initialParameters);
+ }
+
private int RenderFrames(List result, ArrayRange frames, int position, int maxElements)
{
var nextPosition = position;
@@ -229,6 +265,16 @@ namespace Microsoft.AspNetCore.Components.Rendering
return GetCurrentRenderTreeFrames(componentId);
}
+
+ private async Task> CreateInitialRenderAsync(Type componentType, ParameterCollection initialParameters)
+ {
+ var component = InstantiateComponent(componentType);
+ var componentId = AssignRootComponentId(component);
+
+ await RenderRootComponentAsync(componentId, initialParameters);
+
+ return GetCurrentRenderTreeFrames(componentId);
+ }
}
}
diff --git a/src/Components/Components/src/Rendering/Renderer.cs b/src/Components/Components/src/Rendering/Renderer.cs
index 3dde251101..3cf785801d 100644
--- a/src/Components/Components/src/Rendering/Renderer.cs
+++ b/src/Components/Components/src/Rendering/Renderer.cs
@@ -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 _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();
///
/// Constructs an instance of .
@@ -64,8 +74,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// The ID returned by .
protected void RenderRootComponent(int componentId)
{
- GetRequiredComponentState(componentId)
- .SetDirectParameters(ParameterCollection.Empty);
+ RenderRootComponent(componentId, ParameterCollection.Empty);
}
///
@@ -77,8 +86,131 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// The with the initial parameters to use for rendering.
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}");
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The ID returned by .
+ protected Task RenderRootComponentAsync(int componentId)
+ {
+ return RenderRootComponentAsync(componentId, ParameterCollection.Empty);
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The ID returned by .
+ /// The with the initial parameters to use for rendering.
+ protected async Task RenderRootComponentAsync(int componentId, ParameterCollection initialParameters)
+ {
+ if (_pendingTasks != null)
+ {
+ throw new InvalidOperationException("There is an ongoing rendering in progress.");
+ }
+ _pendingTasks = new List();
+ // 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 updatedComponents)
{
var array = updatedComponents.Array;
diff --git a/src/Components/Components/src/Routing/NavLink.cs b/src/Components/Components/src/Routing/NavLink.cs
index f73882e921..e28fb9456b 100644
--- a/src/Components/Components/src/Routing/NavLink.cs
+++ b/src/Components/Components/src/Routing/NavLink.cs
@@ -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; }
///
- public void Init(RenderHandle renderHandle)
+ public void Configure(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
@@ -59,7 +60,7 @@ namespace Microsoft.AspNetCore.Components.Routing
}
///
- 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;
}
///
diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs
index 17b018241c..751ef8b64b 100644
--- a/src/Components/Components/src/Routing/Router.cs
+++ b/src/Components/Components/src/Routing/Router.cs
@@ -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; }
///
- public void Init(RenderHandle renderHandle)
+ public void Configure(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
_baseUri = UriHelper.GetBaseUri();
@@ -47,12 +48,13 @@ namespace Microsoft.AspNetCore.Components.Routing
}
///
- 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;
}
///
diff --git a/src/Components/Components/test/CascadingParameterStateTest.cs b/src/Components/Components/test/CascadingParameterStateTest.cs
index e2b12756dd..9e66a2c35b 100644
--- a/src/Components/Components/test/CascadingParameterStateTest.cs
+++ b/src/Components/Components/test/CascadingParameterStateTest.cs
@@ -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 CreateCascadingValueComponent(T value, string name = null)
{
var supplier = new CascadingValue();
- supplier.Init(new RenderHandle(new TestRenderer(), 0));
+ supplier.Configure(new RenderHandle(new TestRenderer(), 0));
var supplierParams = new Dictionary
{
@@ -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();
}
diff --git a/src/Components/Components/test/CascadingParameterTest.cs b/src/Components/Components/test/CascadingParameterTest.cs
index a24821f721..46711f0606 100644
--- a/src/Components/Components/test/CascadingParameterTest.cs
+++ b/src/Components/Components/test/CascadingParameterTest.cs
@@ -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>(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)
diff --git a/src/Components/Components/test/ComponentBaseTest.cs b/src/Components/Components/test/ComponentBaseTest.cs
index 51834f158d..bca1d60ccf 100644
--- a/src/Components/Components/test/ComponentBaseTest.cs
+++ b/src/Components/Components/test/ComponentBaseTest.cs
@@ -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]
diff --git a/src/Components/Components/test/DependencyInjectionTest.cs b/src/Components/Components/test/DependencyInjectionTest.cs
index ab7a9a789a..b6ccabbe49 100644
--- a/src/Components/Components/test/DependencyInjectionTest.cs
+++ b/src/Components/Components/test/DependencyInjectionTest.cs
@@ -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();
}
}
diff --git a/src/Components/Components/test/ParameterCollectionAssignmentExtensionsTest.cs b/src/Components/Components/test/ParameterCollectionAssignmentExtensionsTest.cs
index a3c4ba3d76..24c2d832c9 100644
--- a/src/Components/Components/test/ParameterCollectionAssignmentExtensionsTest.cs
+++ b/src/Components/Components/test/ParameterCollectionAssignmentExtensionsTest.cs
@@ -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();
}
}
diff --git a/src/Components/Components/test/ParameterCollectionTest.cs b/src/Components/Components/test/ParameterCollectionTest.cs
index 55df5d6eb7..66620132d0 100644
--- a/src/Components/Components/test/ParameterCollectionTest.cs
+++ b/src/Components/Components/test/ParameterCollectionTest.cs
@@ -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();
+
+ // Act
+ var collection = ParameterCollection.FromDictionary(dictionary);
+
+ // Assert
+ Assert.Empty(collection.ToDictionary());
+ }
+
+ [Fact]
+ public void FromDictionary_RoundTrips()
+ {
+ // Arrange
+ var dictionary = new Dictionary
+ {
+ ["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();
}
diff --git a/src/Components/Components/test/RenderTreeBuilderTest.cs b/src/Components/Components/test/RenderTreeBuilderTest.cs
index f5def4fab9..9a44251c30 100644
--- a/src/Components/Components/test/RenderTreeBuilderTest.cs
+++ b/src/Components/Components/test/RenderTreeBuilderTest.cs
@@ -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();
}
diff --git a/src/Components/Components/test/RenderTreeDiffBuilderTest.cs b/src/Components/Components/test/RenderTreeDiffBuilderTest.cs
index 4866be353f..e97170616a 100644
--- a/src/Components/Components/test/RenderTreeDiffBuilderTest.cs
+++ b/src/Components/Components/test/RenderTreeDiffBuilderTest.cs
@@ -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(
diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs
index 57eb16707d..86de53bf6e 100644
--- a/src/Components/Components/test/RendererTest.cs
+++ b/src/Components/Components/test/RendererTest.cs
@@ -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 n
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
+ {
+ [nameof(NestedAsyncComponent.EventActions)] = new Dictionary>
+ {
+ [0] = new List
+ {
+ 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.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>
+ {
+ [0] = CreateRenderFactory(new[] { 1 }),
+ [1] = CreateRenderFactory(Array.Empty())
+ },
+ [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
+ {
+ [nameof(NestedAsyncComponent.EventActions)] = new Dictionary>
+ {
+ [0] = new List
+ {
+ 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.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>
+ {
+ [0] = CreateRenderFactory(new[] { 1 }),
+ [1] = CreateRenderFactory(Array.Empty())
+ },
+ [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
+ {
+ [nameof(NestedAsyncComponent.EventActions)] = new Dictionary>
+ {
+ [0] = new List
+ {
+ 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.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>
+ {
+ [0] = CreateRenderFactory(new[] { 1 }),
+ [1] = CreateRenderFactory(Array.Empty())
+ },
+ [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
+ {
+ [nameof(NestedAsyncComponent.EventActions)] = new Dictionary>
+ {
+ [0] = new List
+ {
+ 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.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.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.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>
+ {
+ [0] = CreateRenderFactory(new[] { 1, 2 }),
+ [1] = CreateRenderFactory(new[] { 3 }),
+ [2] = CreateRenderFactory(Array.Empty()),
+ [3] = CreateRenderFactory(Array.Empty())
+ },
+ [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(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 _renderHandles
= new List();
- 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 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(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> EventActions { get; set; }
+
+ [Parameter] public IDictionary> 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> 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
+ }
+ }
}
}
diff --git a/src/Components/Components/test/Rendering/HtmlRendererTests.cs b/src/Components/Components/test/Rendering/HtmlRendererTests.cs
index 040d0cb414..184f118867 100644
--- a/src/Components/Components/test/Rendering/HtmlRendererTests.cs
+++ b/src/Components/Components/test/Rendering/HtmlRendererTests.cs
@@ -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 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().BuildServiceProvider();
+
+ var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
+
+ // Act
+ var result = await htmlRenderer.RenderComponentAsync(ParameterCollection.FromDictionary(new Dictionary
+ {
+ ["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().BuildServiceProvider();
+
+ var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
+
+ // Act
+ var result = await htmlRenderer.RenderComponentAsync(ParameterCollection.FromDictionary(new Dictionary
+ {
+ ["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(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;
}
}
}
diff --git a/src/Components/Server/test/Circuits/RenderBatchWriterTest.cs b/src/Components/Server/test/Circuits/RenderBatchWriterTest.cs
index c1d4b19148..14305a5834 100644
--- a/src/Components/Server/test/Circuits/RenderBatchWriterTest.cs
+++ b/src/Components/Server/test/Circuits/RenderBatchWriterTest.cs
@@ -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();
}
diff --git a/src/Components/Shared/test/AutoRenderComponent.cs b/src/Components/Shared/test/AutoRenderComponent.cs
index 05e7ad2895..7051d65e65 100644
--- a/src/Components/Shared/test/AutoRenderComponent.cs
+++ b/src/Components/Shared/test/AutoRenderComponent.cs
@@ -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()
diff --git a/src/Components/Shared/test/IComponentExtensions.cs b/src/Components/Shared/test/IComponentExtensions.cs
index 1f7d802392..d70d3017c8 100644
--- a/src/Components/Shared/test/IComponentExtensions.cs
+++ b/src/Components/Shared/test/IComponentExtensions.cs
@@ -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 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);
}
}
}
diff --git a/src/Components/Shared/test/TestRenderer.cs b/src/Components/Shared/test/TestRenderer.cs
index fbf4806dcc..7dd8c9249a 100644
--- a/src/Components/Shared/test/TestRenderer.cs
+++ b/src/Components/Shared/test/TestRenderer.cs
@@ -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);