Skip to content

[Components] Adds support for rendering async components #6708

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/Components/Blazor/Build/test/BindRazorIntegrationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,16 @@ public void Render_BindToComponent_SpecifiesValue_WithoutMatchingProperties()
// 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;
}
}
}"));
Expand Down Expand Up @@ -136,14 +138,16 @@ public void Render_BindToComponent_SpecifiesValueAndChangeEvent_WithoutMatchingP
// 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;
}
}
}"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,16 @@ public void Render_ChildComponent_WithNonPropertyAttributes()
{
// 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;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -149,12 +150,13 @@ public class TestLayout : IComponent
[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;
}
}

Expand Down
9 changes: 8 additions & 1 deletion src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
Expand Down Expand Up @@ -376,7 +377,13 @@ protected RenderTreeFrame[] GetRenderTree(IComponent component)
{
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;
}

Expand Down
11 changes: 7 additions & 4 deletions src/Components/Components/src/CascadingValue.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -49,13 +50,13 @@ public class CascadingValue<T> : ICascadingValueComponent, IComponent
bool ICascadingValueComponent.CurrentValueIsFixed => IsFixed;

/// <inheritdoc />
public void Init(RenderHandle renderHandle)
public void Configure(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}

/// <inheritdoc />
public void SetParameters(ParameterCollection parameters)
public Task SetParametersAsync(ParameterCollection parameters)
{
// Implementing the parameter binding manually, instead of just calling
// parameters.SetParameterProperties(this), is just a very slight perf optimization
Expand Down Expand Up @@ -129,6 +130,8 @@ public void SetParameters(ParameterCollection parameters)
{
NotifySubscribers();
}

return Task.CompletedTask;
}

bool ICascadingValueComponent.CanSupplyValue(Type requestedType, string requestedName)
Expand Down
123 changes: 104 additions & 19 deletions src/Components/Components/src/ComponentBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ protected Task Invoke(Action workItem)
protected Task InvokeAsync(Func<Task> workItem)
=> _renderHandle.InvokeAsync(workItem);

void IComponent.Init(RenderHandle renderHandle)
void IComponent.Configure(RenderHandle renderHandle)
{
// This implicitly means a ComponentBase can only be associated with a single
// renderer. That's the only use case we have right now. If there was ever a need,
Expand All @@ -174,26 +174,106 @@ void IComponent.Init(RenderHandle renderHandle)
/// Method invoked to apply initial or updated parameters to the component.
/// </summary>
/// <param name="parameters">The parameters to apply.</param>
public virtual void SetParameters(ParameterCollection parameters)
public virtual Task SetParametersAsync(ParameterCollection parameters)
{
parameters.SetParameterProperties(this);

if (!_hasCalledInit)
{
_hasCalledInit = true;
OnInit();
return RunInitAndSetParameters();
}
else
{
OnParametersSet();
// If you override OnInitAsync or OnParametersSetAsync and return a noncompleted task,
// then by default we automatically re-render once each of those tasks completes.
var isAsync = false;
Task parametersTask = null;
(isAsync, parametersTask) = ProcessLifeCycletask(OnParametersSetAsync());
StateHasChanged();
// We call StateHasChanged here so that we render after OnParametersSet and after the
// synchronous part of OnParametersSetAsync has run, and in case there is async work
// we trigger another render.
if (isAsync)
{
return parametersTask;
}

// If you override OnInitAsync and return a noncompleted task, then by default
// we automatically re-render once that task completes.
var initTask = OnInitAsync();
ContinueAfterLifecycleTask(initTask);
return Task.CompletedTask;
}
}

OnParametersSet();
var parametersTask = OnParametersSetAsync();
ContinueAfterLifecycleTask(parametersTask);
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();
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)
Expand Down Expand Up @@ -260,19 +340,24 @@ void IHandleAfterRender.OnAfterRender()
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);
}
}
}
Expand Down
9 changes: 6 additions & 3 deletions src/Components/Components/src/IComponent.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Threading.Tasks;

namespace Microsoft.AspNetCore.Components
{
/// <summary>
Expand All @@ -12,12 +14,13 @@ public interface IComponent
/// Initializes the component.
/// </summary>
/// <param name="renderHandle">A <see cref="RenderHandle"/> that allows the component to be rendered.</param>
void Init(RenderHandle renderHandle);
void Configure(RenderHandle renderHandle);

/// <summary>
/// Sets parameters supplied by the component's parent in the render tree.
/// </summary>
/// <param name="parameters">The parameters.</param>
void SetParameters(ParameterCollection parameters);
/// <returns>A <see cref="Task"/> that completes when the component has finished updating and rendering itself.</returns>
Task SetParametersAsync(ParameterCollection parameters);
}
}
6 changes: 4 additions & 2 deletions src/Components/Components/src/Layouts/LayoutDisplay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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

/// <inheritdoc />
public void Init(RenderHandle renderHandle)
public void Configure(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}

/// <inheritdoc />
public void SetParameters(ParameterCollection parameters)
public Task SetParametersAsync(ParameterCollection parameters)
{
parameters.SetParameterProperties(this);
Render();
return Task.CompletedTask;
}

private void Render()
Expand Down
21 changes: 21 additions & 0 deletions src/Components/Components/src/ParameterCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
public readonly struct ParameterCollection
{
private const string GeneratedParameterCollectionElementName = "__ARTIFICIAL_PARAMETER_COLLECTION";
private static readonly RenderTreeFrame[] _emptyCollectionFrames = new RenderTreeFrame[]
{
RenderTreeFrame.Element(0, string.Empty).WithComponentSubtreeLength(1)
Expand Down Expand Up @@ -196,5 +197,25 @@ internal void CaptureSnapshot(ArrayBuilder<RenderTreeFrame> builder)
builder.Append(_frames, _ownerIndex + 1, numEntries);
}
}

/// <summary>
/// Creates a new <see cref="ParameterCollection"/> from the given <see cref="IDictionary{TKey, TValue}"/>.
/// </summary>
/// <param name="parameters">The <see cref="IDictionary{TKey, TValue}"/> with the parameters.</param>
/// <returns>A <see cref="ParameterCollection"/>.</returns>
public static ParameterCollection FromDictionary(IDictionary<string, object> parameters)
{
var frames = new RenderTreeFrame[parameters.Count + 1];
frames[0] = RenderTreeFrame.Element(0, GeneratedParameterCollectionElementName)
.WithElementSubtreeLength(frames.Length);

var i = 0;
foreach (var kvp in parameters)
{
frames[++i] = RenderTreeFrame.Attribute(i, kvp.Key, kvp.Value);
}

return new ParameterCollection(frames, 0);
}
}
}
Loading