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:
parent
29cf7ecb80
commit
1f4341a248
|
|
@ -4,7 +4,7 @@
|
|||
Hello, world!
|
||||
|
||||
@code {
|
||||
protected override void OnAfterRender()
|
||||
protected override void OnAfterRender(bool firstRender)
|
||||
{
|
||||
BenchmarkEvent.Send(JSRuntime, "Rendered index.cshtml");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
largeOrgChartJson = JsonSerializer.Serialize(largeOrgChart);
|
||||
}
|
||||
|
||||
protected override void OnAfterRender()
|
||||
protected override void OnAfterRender(bool firstRender)
|
||||
{
|
||||
BenchmarkEvent.Send(JSRuntime, "Finished JSON processing");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() { }
|
||||
|
|
|
|||
|
|
@ -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() { }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue