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", "" }; + 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", ">", "80", "" + }; + + 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);