Ensure ComponentBase async lifecycle methods return non-null task (imported from Blazor PR 1620) (#4790)

This commit is contained in:
Steve Sanderson 2018-12-14 17:06:30 +00:00 committed by GitHub
parent 8a9df6c873
commit e768a78c2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 369 additions and 23 deletions

View File

@ -158,34 +158,51 @@ namespace Microsoft.AspNetCore.Components
_hasCalledInit = true;
OnInit();
// If you override OnInitAsync and return a nonnull task, then by default
// If you override OnInitAsync and return a noncompleted task, then by default
// we automatically re-render once that task completes.
var initTask = OnInitAsync();
if (initTask != null && initTask.Status != TaskStatus.RanToCompletion)
{
initTask.ContinueWith(ContinueAfterLifecycleTask);
}
ContinueAfterLifecycleTask(initTask);
}
OnParametersSet();
var parametersTask = OnParametersSetAsync();
if (parametersTask != null && parametersTask.Status != TaskStatus.RanToCompletion)
{
parametersTask.ContinueWith(ContinueAfterLifecycleTask);
}
ContinueAfterLifecycleTask(parametersTask);
StateHasChanged();
}
private void ContinueAfterLifecycleTask(Task task)
private async void ContinueAfterLifecycleTask(Task task)
{
if (task.Exception == null)
switch (task == null ? TaskStatus.RanToCompletion : task.Status)
{
StateHasChanged();
}
else
{
HandleException(task.Exception);
// 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:
break;
case TaskStatus.Faulted:
HandleException(task.Exception);
break;
// For incomplete tasks, automatically re-render on successful completion
default:
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);
}
}
break;
}
}
@ -203,18 +220,12 @@ namespace Microsoft.AspNetCore.Components
void IHandleEvent.HandleEvent(EventHandlerInvoker binding, UIEventArgs args)
{
var task = binding.Invoke(args);
ContinueAfterLifecycleTask(task);
// After each event, we synchronously re-render (unless !ShouldRender())
// This just saves the developer the trouble of putting "StateHasChanged();"
// at the end of every event callback.
StateHasChanged();
if (task.Status == TaskStatus.RanToCompletion)
{
return;
}
task.ContinueWith(ContinueAfterLifecycleTask);
}
void IHandleAfterRender.OnAfterRender()

View File

@ -0,0 +1,332 @@
// 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;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
using Xunit;
namespace Microsoft.AspNetCore.Components.Test
{
public class ComponentBaseTest
{
[Fact]
public void RunsOnInitWhenRendered()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent();
int onInitRuns = 0;
component.OnInitLogic = c => onInitRuns++;
// Act
var componentId = renderer.AssignRootComponentId(component);
renderer.RenderRootComponent(componentId);
// Assert
Assert.Equal(1, onInitRuns);
}
[Fact]
public void RunsOnInitAsyncWhenRendered()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent();
int onInitAsyncRuns = 0;
component.RunsBaseOnInitAsync = false;
component.OnInitAsyncLogic = c =>
{
onInitAsyncRuns++;
return Task.CompletedTask;
};
// Act
var componentId = renderer.AssignRootComponentId(component);
renderer.RenderRootComponent(componentId);
// Assert
Assert.Equal(1, onInitAsyncRuns);
Assert.Single(renderer.Batches);
}
[Fact]
public void RunsOnInitAsyncAlsoOnBaseClassWhenRendered()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent();
int onInitAsyncRuns = 0;
component.RunsBaseOnInitAsync = true;
component.OnInitAsyncLogic = c =>
{
onInitAsyncRuns++;
return Task.CompletedTask;
};
// Act
var componentId = renderer.AssignRootComponentId(component);
renderer.RenderRootComponent(componentId);
// Assert
Assert.Equal(1, onInitAsyncRuns);
Assert.Single(renderer.Batches);
}
[Fact]
public void RunsOnParametersSetWhenRendered()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent();
int onParametersSetRuns = 0;
component.OnParametersSetLogic = c => onParametersSetRuns++;
// Act
var componentId = renderer.AssignRootComponentId(component);
renderer.RenderRootComponent(componentId);
// Assert
Assert.Equal(1, onParametersSetRuns);
Assert.Single(renderer.Batches);
}
[Fact]
public void RunsOnParametersSetAsyncWhenRendered()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent();
int onParametersSetAsyncRuns = 0;
component.RunsBaseOnParametersSetAsync = false;
component.OnParametersSetAsyncLogic = c =>
{
onParametersSetAsyncRuns++;
return Task.CompletedTask;
};
// Act
var componentId = renderer.AssignRootComponentId(component);
renderer.RenderRootComponent(componentId);
// Assert
Assert.Equal(1, onParametersSetAsyncRuns);
Assert.Single(renderer.Batches);
}
[Fact]
public void RunsOnParametersSetAsyncAlsoOnBaseClassWhenRendered()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent();
int onParametersSetAsyncRuns = 0;
component.RunsBaseOnParametersSetAsync = true;
component.OnParametersSetAsyncLogic = c =>
{
onParametersSetAsyncRuns++;
return Task.CompletedTask;
};
// Act
var componentId = renderer.AssignRootComponentId(component);
renderer.RenderRootComponent(componentId);
// Assert
Assert.Equal(1, onParametersSetAsyncRuns);
Assert.Single(renderer.Batches);
}
[Fact]
public void RendersAfterParametersSetAsyncTaskIsCompleted()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent();
component.Counter = 1;
var parametersSetTask = new TaskCompletionSource<bool>();
component.RunsBaseOnParametersSetAsync = false;
component.OnParametersSetAsyncLogic = c => parametersSetTask.Task;
// Act
var componentId = renderer.AssignRootComponentId(component);
renderer.RenderRootComponent(componentId);
// Assert
Assert.Single(renderer.Batches);
// Completes task started by OnParametersSetAsync
component.Counter = 2;
parametersSetTask.SetResult(true);
// Component should be rendered again
Assert.Equal(2, renderer.Batches.Count);
}
[Fact]
public void RendersAfterParametersSetAndInitAsyncTasksAreCompleted()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent();
component.Counter = 1;
var initTask = new TaskCompletionSource<bool>();
var parametersSetTask = new TaskCompletionSource<bool>();
component.RunsBaseOnInitAsync = true;
component.RunsBaseOnParametersSetAsync = true;
component.OnInitAsyncLogic = c => initTask.Task;
component.OnParametersSetAsyncLogic = c => parametersSetTask.Task;
// Act
var componentId = renderer.AssignRootComponentId(component);
renderer.RenderRootComponent(componentId);
// Assert
Assert.Single(renderer.Batches);
// Completes task started by OnParametersSetAsync
component.Counter = 2;
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);
}
[Fact]
public void DoesNotRenderAfterOnInitAsyncTaskIsCancelled()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent() { Counter = 1 };
var initTask = new TaskCompletionSource<object>();
component.OnInitAsyncLogic = _ => initTask.Task;
// Act
var componentId = renderer.AssignRootComponentId(component);
renderer.RenderRootComponent(componentId);
// Assert
Assert.Single(renderer.Batches);
// Cancel task started by OnInitAsync
component.Counter = 2;
initTask.SetCanceled();
// Component should not be rendered again
Assert.Single(renderer.Batches);
}
[Fact]
public void DoesNotRenderAfterOnParametersSetAsyncTaskIsCancelled()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent() { Counter = 1 };
var onParametersSetTask = new TaskCompletionSource<object>();
component.OnParametersSetAsyncLogic = _ => onParametersSetTask.Task;
// Act
var componentId = renderer.AssignRootComponentId(component);
renderer.RenderRootComponent(componentId);
// Assert
Assert.Single(renderer.Batches);
// Cancel task started by OnParametersSet
component.Counter = 2;
onParametersSetTask.SetCanceled();
// Component should not be rendered again
Assert.Single(renderer.Batches);
}
private class TestComponent : ComponentBase
{
public bool RunsBaseOnInit { get; set; } = true;
public bool RunsBaseOnInitAsync { get; set; } = true;
public bool RunsBaseOnParametersSet { get; set; } = true;
public bool RunsBaseOnParametersSetAsync { get; set; } = true;
public Action<TestComponent> OnInitLogic { get; set; }
public Func<TestComponent, Task> OnInitAsyncLogic { get; set; }
public Action<TestComponent> OnParametersSetLogic { get; set; }
public Func<TestComponent, Task> OnParametersSetAsyncLogic { get; set; }
public int Counter { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
builder.OpenElement(0, "p");
builder.AddContent(1, Counter);
builder.CloseElement();
}
protected override void OnInit()
{
if (RunsBaseOnInit)
{
base.OnInit();
}
OnInitLogic?.Invoke(this);
}
protected override async Task OnInitAsync()
{
if (RunsBaseOnInitAsync)
{
await base.OnInitAsync();
}
await OnInitAsyncLogic?.Invoke(this);
}
protected override void OnParametersSet()
{
if (RunsBaseOnParametersSet)
{
base.OnParametersSet();
}
OnParametersSetLogic?.Invoke(this);
}
protected override async Task OnParametersSetAsync()
{
if (RunsBaseOnParametersSetAsync)
{
await base.OnParametersSetAsync();
}
await OnParametersSetAsyncLogic?.Invoke(this);
}
}
}
}

View File

@ -28,6 +28,9 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
public new int AssignRootComponentId(IComponent component)
=> base.AssignRootComponentId(component);
public new void RenderRootComponent(int componentId)
=> base.RenderRootComponent(componentId);
public new void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs args)
=> base.DispatchEvent(componentId, eventHandlerId, args);