Skip to content

Commit 19b543e

Browse files
javiercnmkArtakMSFT
authored andcommitted
[Components] Support for prerrendering asynchronous components.
* Updates the IComponent interface to rename Init into Configure * Updates the IComponent interface to change SetParameters for SetParametersAsync and make it return a Task that represents when the component is done applying the parameters and potentially triggering one or more renders. * Updates ComponentBase SetParametersAsync to ensure that OnInit(Async) runs before OnParametersSet(Async). * Introduces ParameterCollection.FromDictionary to generate a parameter collection from a dictionary of key value pairs. * Introduces RenderComponentAsync on HtmlRenderer to support prerrendering of async components. * Introduces RenderRootComponentAsync on the renderer to allow for asynchronous prerrendering of the root component.
1 parent 749092c commit 19b543e

28 files changed

+1106
-118
lines changed

src/Components/Blazor/Build/test/BindRazorIntegrationTest.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,16 @@ public void Render_BindToComponent_SpecifiesValue_WithoutMatchingProperties()
6262
// Arrange
6363
AdditionalSyntaxTrees.Add(Parse(@"
6464
using System;
65+
using System.Threading.Tasks;
6566
using Microsoft.AspNetCore.Components;
6667
6768
namespace Test
6869
{
6970
public class MyComponent : ComponentBase, IComponent
7071
{
71-
void IComponent.SetParameters(ParameterCollection parameters)
72+
Task IComponent.SetParametersAsync(ParameterCollection parameters)
7273
{
74+
return Task.CompletedTask;
7375
}
7476
}
7577
}"));
@@ -136,14 +138,16 @@ public void Render_BindToComponent_SpecifiesValueAndChangeEvent_WithoutMatchingP
136138
// Arrange
137139
AdditionalSyntaxTrees.Add(Parse(@"
138140
using System;
141+
using System.Threading.Tasks;
139142
using Microsoft.AspNetCore.Components;
140143
141144
namespace Test
142145
{
143146
public class MyComponent : ComponentBase, IComponent
144147
{
145-
void IComponent.SetParameters(ParameterCollection parameters)
148+
Task IComponent.SetParametersAsync(ParameterCollection parameters)
146149
{
150+
return Task.CompletedTask;
147151
}
148152
}
149153
}"));

src/Components/Blazor/Build/test/ComponentRenderingRazorIntegrationTest.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,14 +162,16 @@ public void Render_ChildComponent_WithNonPropertyAttributes()
162162
{
163163
// Arrange
164164
AdditionalSyntaxTrees.Add(Parse(@"
165+
using System.Threading.Tasks;
165166
using Microsoft.AspNetCore.Components;
166167
167168
namespace Test
168169
{
169170
public class MyComponent : ComponentBase, IComponent
170171
{
171-
void IComponent.SetParameters(ParameterCollection parameters)
172+
Task IComponent.SetParametersAsync(ParameterCollection parameters)
172173
{
174+
return Task.CompletedTask;
173175
}
174176
}
175177
}

src/Components/Blazor/Build/test/DirectiveRazorIntegrationTest.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Linq;
66
using System.Reflection;
7+
using System.Threading.Tasks;
78
using Microsoft.AspNetCore.Components;
89
using Microsoft.AspNetCore.Components.Layouts;
910
using Microsoft.AspNetCore.Components.Test.Helpers;
@@ -149,12 +150,13 @@ public class TestLayout : IComponent
149150
[Parameter]
150151
RenderFragment Body { get; set; }
151152

152-
public void Init(RenderHandle renderHandle)
153+
public void Configure(RenderHandle renderHandle)
153154
{
154155
}
155156

156-
public void SetParameters(ParameterCollection parameters)
157+
public Task SetParametersAsync(ParameterCollection parameters)
157158
{
159+
return Task.CompletedTask;
158160
}
159161
}
160162

src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.IO;
77
using System.Linq;
88
using System.Reflection;
9+
using System.Runtime.ExceptionServices;
910
using System.Runtime.InteropServices;
1011
using System.Text;
1112
using System.Threading;
@@ -376,7 +377,13 @@ protected RenderTreeFrame[] GetRenderTree(IComponent component)
376377
{
377378
var renderer = new TestRenderer();
378379
renderer.AttachComponent(component);
379-
component.SetParameters(ParameterCollection.Empty);
380+
var task = component.SetParametersAsync(ParameterCollection.Empty);
381+
// we will have to change this method if we add a test that does actual async work.
382+
Assert.True(task.Status.HasFlag(TaskStatus.RanToCompletion) || task.Status.HasFlag(TaskStatus.Faulted));
383+
if (task.IsFaulted)
384+
{
385+
ExceptionDispatchInfo.Capture(task.Exception.InnerException).Throw();
386+
}
380387
return renderer.LatestBatchReferenceFrames;
381388
}
382389

src/Components/Components/src/CascadingValue.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
using Microsoft.AspNetCore.Components.Rendering;
5-
using Microsoft.AspNetCore.Components.RenderTree;
64
using System;
75
using System.Collections.Generic;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Components.Rendering;
8+
using Microsoft.AspNetCore.Components.RenderTree;
89

910
namespace Microsoft.AspNetCore.Components
1011
{
@@ -49,13 +50,13 @@ public class CascadingValue<T> : ICascadingValueComponent, IComponent
4950
bool ICascadingValueComponent.CurrentValueIsFixed => IsFixed;
5051

5152
/// <inheritdoc />
52-
public void Init(RenderHandle renderHandle)
53+
public void Configure(RenderHandle renderHandle)
5354
{
5455
_renderHandle = renderHandle;
5556
}
5657

5758
/// <inheritdoc />
58-
public void SetParameters(ParameterCollection parameters)
59+
public Task SetParametersAsync(ParameterCollection parameters)
5960
{
6061
// Implementing the parameter binding manually, instead of just calling
6162
// parameters.SetParameterProperties(this), is just a very slight perf optimization
@@ -129,6 +130,8 @@ public void SetParameters(ParameterCollection parameters)
129130
{
130131
NotifySubscribers();
131132
}
133+
134+
return Task.CompletedTask;
132135
}
133136

134137
bool ICascadingValueComponent.CanSupplyValue(Type requestedType, string requestedName)

src/Components/Components/src/ComponentBase.cs

Lines changed: 104 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ protected Task Invoke(Action workItem)
157157
protected Task InvokeAsync(Func<Task> workItem)
158158
=> _renderHandle.InvokeAsync(workItem);
159159

160-
void IComponent.Init(RenderHandle renderHandle)
160+
void IComponent.Configure(RenderHandle renderHandle)
161161
{
162162
// This implicitly means a ComponentBase can only be associated with a single
163163
// renderer. That's the only use case we have right now. If there was ever a need,
@@ -174,26 +174,106 @@ void IComponent.Init(RenderHandle renderHandle)
174174
/// Method invoked to apply initial or updated parameters to the component.
175175
/// </summary>
176176
/// <param name="parameters">The parameters to apply.</param>
177-
public virtual void SetParameters(ParameterCollection parameters)
177+
public virtual Task SetParametersAsync(ParameterCollection parameters)
178178
{
179179
parameters.SetParameterProperties(this);
180-
181180
if (!_hasCalledInit)
182181
{
183-
_hasCalledInit = true;
184-
OnInit();
182+
return RunInitAndSetParameters();
183+
}
184+
else
185+
{
186+
OnParametersSet();
187+
// If you override OnInitAsync or OnParametersSetAsync and return a noncompleted task,
188+
// then by default we automatically re-render once each of those tasks completes.
189+
var isAsync = false;
190+
Task parametersTask = null;
191+
(isAsync, parametersTask) = ProcessLifeCycletask(OnParametersSetAsync());
192+
StateHasChanged();
193+
// We call StateHasChanged here so that we render after OnParametersSet and after the
194+
// synchronous part of OnParametersSetAsync has run, and in case there is async work
195+
// we trigger another render.
196+
if (isAsync)
197+
{
198+
return parametersTask;
199+
}
185200

186-
// If you override OnInitAsync and return a noncompleted task, then by default
187-
// we automatically re-render once that task completes.
188-
var initTask = OnInitAsync();
189-
ContinueAfterLifecycleTask(initTask);
201+
return Task.CompletedTask;
190202
}
203+
}
191204

192-
OnParametersSet();
193-
var parametersTask = OnParametersSetAsync();
194-
ContinueAfterLifecycleTask(parametersTask);
205+
private async Task RunInitAndSetParameters()
206+
{
207+
_hasCalledInit = true;
208+
var initIsAsync = false;
209+
210+
OnInit();
211+
Task initTask = null;
212+
(initIsAsync, initTask) = ProcessLifeCycletask(OnInitAsync());
213+
if (initIsAsync)
214+
{
215+
// Call state has changed here so that we render after the sync part of OnInitAsync has run
216+
// and wait for it to finish before we continue. If no async work has been done yet, we want
217+
// to defer calling StateHasChanged up until the first bit of async code happens or until
218+
// the end.
219+
StateHasChanged();
220+
await initTask;
221+
}
195222

223+
OnParametersSet();
224+
Task parametersTask = null;
225+
var setParametersIsAsync = false;
226+
(setParametersIsAsync, parametersTask) = ProcessLifeCycletask(OnParametersSetAsync());
227+
// We always call StateHasChanged here as we want to trigger a rerender after OnParametersSet and
228+
// the synchronous part of OnParametersSetAsync has run, triggering another re-render in case there
229+
// is additional async work.
196230
StateHasChanged();
231+
if (setParametersIsAsync)
232+
{
233+
await parametersTask;
234+
}
235+
}
236+
237+
private (bool isAsync, Task asyncTask) ProcessLifeCycletask(Task task)
238+
{
239+
if (task == null)
240+
{
241+
throw new ArgumentNullException(nameof(task));
242+
}
243+
244+
switch (task.Status)
245+
{
246+
// If it's already completed synchronously, no need to await and no
247+
// need to issue a further render (we already rerender synchronously).
248+
// Just need to make sure we propagate any errors.
249+
case TaskStatus.RanToCompletion:
250+
case TaskStatus.Canceled:
251+
return (false, null);
252+
case TaskStatus.Faulted:
253+
HandleException(task.Exception);
254+
return (false, null);
255+
// For incomplete tasks, automatically re-render on successful completion
256+
default:
257+
return (true, ReRenderAsyncTask(task));
258+
}
259+
}
260+
261+
private async Task ReRenderAsyncTask(Task task)
262+
{
263+
try
264+
{
265+
await task;
266+
StateHasChanged();
267+
}
268+
catch (Exception ex)
269+
{
270+
// Either the task failed, or it was cancelled, or StateHasChanged threw.
271+
// We want to report task failure or StateHasChanged exceptions only.
272+
if (!task.IsCanceled)
273+
{
274+
HandleException(ex);
275+
}
276+
}
197277
}
198278

199279
private async void ContinueAfterLifecycleTask(Task task)
@@ -260,19 +340,24 @@ void IHandleAfterRender.OnAfterRender()
260340
var onAfterRenderTask = OnAfterRenderAsync();
261341
if (onAfterRenderTask != null && onAfterRenderTask.Status != TaskStatus.RanToCompletion)
262342
{
263-
onAfterRenderTask.ContinueWith(task =>
264-
{
265343
// Note that we don't call StateHasChanged to trigger a render after
266344
// handling this, because that would be an infinite loop. The only
267345
// reason we have OnAfterRenderAsync is so that the developer doesn't
268346
// have to use "async void" and do their own exception handling in
269347
// the case where they want to start an async task.
348+
var taskWithHandledException = HandleAfterRenderException(onAfterRenderTask);
349+
}
350+
}
270351

271-
if (task.Exception != null)
272-
{
273-
HandleException(task.Exception);
274-
}
275-
});
352+
private async Task HandleAfterRenderException(Task parentTask)
353+
{
354+
try
355+
{
356+
await parentTask;
357+
}
358+
catch (Exception e)
359+
{
360+
HandleException(e);
276361
}
277362
}
278363
}

src/Components/Components/src/IComponent.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System.Threading.Tasks;
5+
46
namespace Microsoft.AspNetCore.Components
57
{
68
/// <summary>
@@ -12,12 +14,13 @@ public interface IComponent
1214
/// Initializes the component.
1315
/// </summary>
1416
/// <param name="renderHandle">A <see cref="RenderHandle"/> that allows the component to be rendered.</param>
15-
void Init(RenderHandle renderHandle);
17+
void Configure(RenderHandle renderHandle);
1618

1719
/// <summary>
1820
/// Sets parameters supplied by the component's parent in the render tree.
1921
/// </summary>
2022
/// <param name="parameters">The parameters.</param>
21-
void SetParameters(ParameterCollection parameters);
23+
/// <returns>A <see cref="Task"/> that completes when the component has finished updating and rendering itself.</returns>
24+
Task SetParametersAsync(ParameterCollection parameters);
2225
}
2326
}

src/Components/Components/src/Layouts/LayoutDisplay.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Reflection;
7+
using System.Threading.Tasks;
78
using Microsoft.AspNetCore.Components;
89
using Microsoft.AspNetCore.Components.RenderTree;
910

@@ -34,16 +35,17 @@ public class LayoutDisplay : IComponent
3435
IDictionary<string, object> PageParameters { get; set; }
3536

3637
/// <inheritdoc />
37-
public void Init(RenderHandle renderHandle)
38+
public void Configure(RenderHandle renderHandle)
3839
{
3940
_renderHandle = renderHandle;
4041
}
4142

4243
/// <inheritdoc />
43-
public void SetParameters(ParameterCollection parameters)
44+
public Task SetParametersAsync(ParameterCollection parameters)
4445
{
4546
parameters.SetParameterProperties(this);
4647
Render();
48+
return Task.CompletedTask;
4749
}
4850

4951
private void Render()

src/Components/Components/src/ParameterCollection.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Components
1313
/// </summary>
1414
public readonly struct ParameterCollection
1515
{
16+
private const string GeneratedParameterCollectionElementName = "__ARTIFICIAL_PARAMETER_COLLECTION";
1617
private static readonly RenderTreeFrame[] _emptyCollectionFrames = new RenderTreeFrame[]
1718
{
1819
RenderTreeFrame.Element(0, string.Empty).WithComponentSubtreeLength(1)
@@ -196,5 +197,25 @@ internal void CaptureSnapshot(ArrayBuilder<RenderTreeFrame> builder)
196197
builder.Append(_frames, _ownerIndex + 1, numEntries);
197198
}
198199
}
200+
201+
/// <summary>
202+
/// Creates a new <see cref="ParameterCollection"/> from the given <see cref="IDictionary{TKey, TValue}"/>.
203+
/// </summary>
204+
/// <param name="parameters">The <see cref="IDictionary{TKey, TValue}"/> with the parameters.</param>
205+
/// <returns>A <see cref="ParameterCollection"/>.</returns>
206+
public static ParameterCollection FromDictionary(IDictionary<string, object> parameters)
207+
{
208+
var frames = new RenderTreeFrame[parameters.Count + 1];
209+
frames[0] = RenderTreeFrame.Element(0, GeneratedParameterCollectionElementName)
210+
.WithElementSubtreeLength(frames.Length);
211+
212+
var i = 0;
213+
foreach (var kvp in parameters)
214+
{
215+
frames[++i] = RenderTreeFrame.Attribute(i, kvp.Key, kvp.Value);
216+
}
217+
218+
return new ParameterCollection(frames, 0);
219+
}
199220
}
200221
}

0 commit comments

Comments
 (0)