Add 'firstTime' parameter to OnAfterRender

Fixes: #11610

I took the approach here of building this into `ComponentBase` instead
of `IHandleAfterRender` - *because* my reasoning is that `firstTime` is
an opinionated construct. There's nothing fundamental about `firstTime`
that requires tracking by the rendering, it's simply an opinion that
it's going to be useful for component authors, and reinforces a common
technique.

Feedback on this is welcome.
This commit is contained in:
Ryan Nowak 2019-08-09 15:44:22 -07:00
parent 29cf7ecb80
commit 1f4341a248
12 changed files with 170 additions and 29 deletions

View File

@ -4,7 +4,7 @@
Hello, world!
@code {
protected override void OnAfterRender()
protected override void OnAfterRender(bool firstRender)
{
BenchmarkEvent.Send(JSRuntime, "Rendered index.cshtml");
}

View File

@ -37,7 +37,7 @@
largeOrgChartJson = JsonSerializer.Serialize(largeOrgChart);
}
protected override void OnAfterRender()
protected override void OnAfterRender(bool firstRender)
{
BenchmarkEvent.Send(JSRuntime, "Finished JSON processing");
}

View File

@ -46,7 +46,7 @@ Number of items: <input id="num-items" type="number" @bind=numItems />
show = true;
}
protected override void OnAfterRender()
protected override void OnAfterRender(bool firstRender)
{
BenchmarkEvent.Send(JSRuntime, "Finished rendering list");
}

View File

@ -84,8 +84,8 @@ namespace Microsoft.AspNetCore.Components
void Microsoft.AspNetCore.Components.IComponent.Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { }
System.Threading.Tasks.Task Microsoft.AspNetCore.Components.IHandleAfterRender.OnAfterRenderAsync() { throw null; }
System.Threading.Tasks.Task Microsoft.AspNetCore.Components.IHandleEvent.HandleEventAsync(Microsoft.AspNetCore.Components.EventCallbackWorkItem callback, object arg) { throw null; }
protected virtual void OnAfterRender() { }
protected virtual System.Threading.Tasks.Task OnAfterRenderAsync() { throw null; }
protected virtual void OnAfterRender(bool firstRender) { }
protected virtual System.Threading.Tasks.Task OnAfterRenderAsync(bool firstRender) { throw null; }
protected virtual void OnInitialized() { }
protected virtual System.Threading.Tasks.Task OnInitializedAsync() { throw null; }
protected virtual void OnParametersSet() { }

View File

@ -84,8 +84,8 @@ namespace Microsoft.AspNetCore.Components
void Microsoft.AspNetCore.Components.IComponent.Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { }
System.Threading.Tasks.Task Microsoft.AspNetCore.Components.IHandleAfterRender.OnAfterRenderAsync() { throw null; }
System.Threading.Tasks.Task Microsoft.AspNetCore.Components.IHandleEvent.HandleEventAsync(Microsoft.AspNetCore.Components.EventCallbackWorkItem callback, object arg) { throw null; }
protected virtual void OnAfterRender() { }
protected virtual System.Threading.Tasks.Task OnAfterRenderAsync() { throw null; }
protected virtual void OnAfterRender(bool firstRender) { }
protected virtual System.Threading.Tasks.Task OnAfterRenderAsync(bool firstRender) { throw null; }
protected virtual void OnInitialized() { }
protected virtual System.Threading.Tasks.Task OnInitializedAsync() { throw null; }
protected virtual void OnParametersSet() { }

View File

@ -30,6 +30,7 @@ namespace Microsoft.AspNetCore.Components
private bool _initialized;
private bool _hasNeverRendered = true;
private bool _hasPendingQueuedRender;
private bool _hasCalledOnAfterRender;
/// <summary>
/// Constructs an instance of <see cref="ComponentBase"/>.
@ -129,7 +130,17 @@ namespace Microsoft.AspNetCore.Components
/// <summary>
/// Method invoked after each time the component has been rendered.
/// </summary>
protected virtual void OnAfterRender()
/// <param name="firstRender">
/// Set to <c>true</c> if this is the first time <see cref="OnAfterRender(bool)"/> has been invoked
/// on this component instance; otherwise <c>false</c>.
/// </param>
/// <remarks>
/// The <see cref="OnAfterRender(bool)"/> and <see cref="OnAfterRenderAsync(bool)"/> lifecycle methods
/// are useful for performing interop, or interacting with values recieved from <c>@ref</c>.
/// Use the <paramref name="firstRender"/> parameter to ensure that initialization work is only performed
/// once.
/// </remarks>
protected virtual void OnAfterRender(bool firstRender)
{
}
@ -138,8 +149,18 @@ namespace Microsoft.AspNetCore.Components
/// not automatically re-render after the completion of any returned <see cref="Task"/>, because
/// that would cause an infinite render loop.
/// </summary>
/// <param name="firstRender">
/// Set to <c>true</c> if this is the first time <see cref="OnAfterRender(bool)"/> has been invoked
/// on this component instance; otherwise <c>false</c>.
/// </param>
/// <returns>A <see cref="Task"/> representing any asynchronous operation.</returns>
protected virtual Task OnAfterRenderAsync()
/// <remarks>
/// The <see cref="OnAfterRender(bool)"/> and <see cref="OnAfterRenderAsync(bool)"/> lifecycle methods
/// are useful for performing interop, or interacting with values recieved from <c>@ref</c>.
/// Use the <paramref name="firstRender"/> parameter to ensure that initialization work is only performed
/// once.
/// </remarks>
protected virtual Task OnAfterRenderAsync(bool firstRender)
=> Task.CompletedTask;
/// <summary>
@ -298,9 +319,12 @@ namespace Microsoft.AspNetCore.Components
Task IHandleAfterRender.OnAfterRenderAsync()
{
OnAfterRender();
var firstRender = !_hasCalledOnAfterRender;
_hasCalledOnAfterRender |= true;
return OnAfterRenderAsync();
OnAfterRender(firstRender);
return OnAfterRenderAsync(firstRender);
// Note that we don't call StateHasChanged to trigger a render after
// handling this, because that would be an infinite loop. The only

View File

@ -3,6 +3,8 @@
using System;
using System.Diagnostics;
using System.Reflection.Metadata.Ecma335;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering;
@ -261,6 +263,98 @@ namespace Microsoft.AspNetCore.Components.Test
Assert.Equal(2, renderer.Batches.Count);
}
[Fact]
public async Task RunsOnAfterRender_AfterRenderingCompletes()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent() { Counter = 1 };
var onAfterRenderCompleted = false;
component.OnAfterRenderLogic = (c, firstRender) =>
{
Assert.True(firstRender);
Assert.Single(renderer.Batches);
onAfterRenderCompleted = true;
};
// Act
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.RenderRootComponentAsync(componentId);
// Assert
await renderTask;
Assert.True(onAfterRenderCompleted);
// Component should not be rendered again. OnAfterRender doesn't do that.
Assert.Single(renderer.Batches);
// Act: Render again!
onAfterRenderCompleted = false;
component.OnAfterRenderLogic = (c, firstRender) =>
{
Assert.False(firstRender);
Assert.Equal(2, renderer.Batches.Count);
onAfterRenderCompleted = true;
};
renderTask = renderer.RenderRootComponentAsync(componentId);
// Assert
Assert.True(onAfterRenderCompleted);
Assert.Equal(2, renderer.Batches.Count);
await renderTask;
}
[Fact]
public async Task RunsOnAfterRenderAsync_AfterRenderingCompletes()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent() { Counter = 1 };
var onAfterRenderCompleted = false;
var tcs = new TaskCompletionSource<object>();
component.OnAfterRenderAsyncLogic = async (c, firstRender) =>
{
Assert.True(firstRender);
Assert.Single(renderer.Batches);
onAfterRenderCompleted = true;
await tcs.Task;
};
// Act
var componentId = renderer.AssignRootComponentId(component);
var renderTask = renderer.RenderRootComponentAsync(componentId);
// Assert
tcs.SetResult(null);
await renderTask;
Assert.True(onAfterRenderCompleted);
// Component should not be rendered again. OnAfterRenderAsync doesn't do that.
Assert.Single(renderer.Batches);
// Act: Render again!
onAfterRenderCompleted = false;
tcs = new TaskCompletionSource<object>();
component.OnAfterRenderAsyncLogic = async (c, firstRender) =>
{
Assert.False(firstRender);
Assert.Equal(2, renderer.Batches.Count);
onAfterRenderCompleted = true;
await tcs.Task;
};
renderTask = renderer.RenderRootComponentAsync(componentId);
// Assert
tcs.SetResult(null);
await renderTask;
Assert.True(onAfterRenderCompleted);
Assert.Equal(2, renderer.Batches.Count);
}
[Fact]
public async Task DoesNotRenderAfterOnInitAsyncTaskIsCancelledUsingCancellationToken()
{
@ -386,6 +480,10 @@ namespace Microsoft.AspNetCore.Components.Test
public bool RunsBaseOnParametersSetAsync { get; set; } = true;
public bool RunsBaseOnAfterRender { get; set; } = true;
public bool RunsBaseOnAfterRenderAsync { get; set; } = true;
public Action<TestComponent> OnInitLogic { get; set; }
public Func<TestComponent, Task> OnInitAsyncLogic { get; set; }
@ -394,6 +492,10 @@ namespace Microsoft.AspNetCore.Components.Test
public Func<TestComponent, Task> OnParametersSetAsyncLogic { get; set; }
public Action<TestComponent, bool> OnAfterRenderLogic { get; set; }
public Func<TestComponent, bool, Task> OnAfterRenderAsyncLogic { get; set; }
public int Counter { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
@ -448,6 +550,32 @@ namespace Microsoft.AspNetCore.Components.Test
await OnParametersSetAsyncLogic(this);
}
}
protected override void OnAfterRender(bool firstRender)
{
if (RunsBaseOnAfterRender)
{
base.OnAfterRender(firstRender);
}
if (OnAfterRenderLogic != null)
{
OnAfterRenderLogic(this, firstRender);
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (RunsBaseOnAfterRenderAsync)
{
await base.OnAfterRenderAsync(firstRender);
}
if (OnAfterRenderAsyncLogic != null)
{
await OnAfterRenderAsyncLogic(this, firstRender);
}
}
}
}
}

View File

@ -4244,7 +4244,7 @@ namespace Microsoft.AspNetCore.Components.Test
renderFactory(this)(builder);
}
protected override async Task OnAfterRenderAsync()
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (TryGetEntry(EventType.OnAfterRenderAsyncSync, out var entrySync))
{

View File

@ -6,7 +6,7 @@
@code {
ElementReference myInput;
protected override void OnAfterRender()
protected override void OnAfterRender(bool firstRender)
{
JSRuntime.InvokeAsync<object>("setElementValue", myInput, "Value set after render");
}

View File

@ -26,17 +26,9 @@
string infoFromJs;
ElementReference myElem;
protected override async Task OnAfterRenderAsync()
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// TEMPORARY: Currently we need this guard to avoid making the interop
// call during prerendering. Soon this will be unnecessary because we
// will change OnAfterRenderAsync not to run during the prerendering phase.
if (!ComponentContext.IsConnected)
{
return;
}
if (infoFromJs == null)
if (firstRender)
{
// We can only use the ElementRef in OnAfterRenderAsync (and not any
// earlier lifecycle method), because there is no JS element until

View File

@ -23,13 +23,10 @@
@code {
int count;
bool firstRender = false;
protected override Task OnAfterRenderAsync()
protected override Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender)
if (firstRender)
{
firstRender = true;
// We need to queue another render when we connect, otherwise the
// browser won't see anything.
StateHasChanged();

View File

@ -333,7 +333,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
{
[Parameter] public OnAfterRenderState State { get; set; }
protected override void OnAfterRender()
protected override void OnAfterRender(bool firstRender)
{
State.OnAfterRenderRan = true;
}