Skip to content

Commit 2b3ceb1

Browse files
rynowakRyan Nowak
authored and
Ryan Nowak
committed
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.
1 parent eb564a8 commit 2b3ceb1

File tree

12 files changed

+170
-29
lines changed

12 files changed

+170
-29
lines changed

src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Pages/Index.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
Hello, world!
55

66
@code {
7-
protected override void OnAfterRender()
7+
protected override void OnAfterRender(bool firstRender)
88
{
99
BenchmarkEvent.Send(JSRuntime, "Rendered index.cshtml");
1010
}

src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Pages/Json.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
largeOrgChartJson = JsonSerializer.Serialize(largeOrgChart);
3838
}
3939

40-
protected override void OnAfterRender()
40+
protected override void OnAfterRender(bool firstRender)
4141
{
4242
BenchmarkEvent.Send(JSRuntime, "Finished JSON processing");
4343
}

src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Pages/RenderList.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Number of items: <input id="num-items" type="number" @bind=numItems />
4646
show = true;
4747
}
4848

49-
protected override void OnAfterRender()
49+
protected override void OnAfterRender(bool firstRender)
5050
{
5151
BenchmarkEvent.Send(JSRuntime, "Finished rendering list");
5252
}

src/Components/Components/ref/Microsoft.AspNetCore.Components.netcoreapp3.0.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ protected virtual void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering
142142
void Microsoft.AspNetCore.Components.IComponent.Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { }
143143
System.Threading.Tasks.Task Microsoft.AspNetCore.Components.IHandleAfterRender.OnAfterRenderAsync() { throw null; }
144144
System.Threading.Tasks.Task Microsoft.AspNetCore.Components.IHandleEvent.HandleEventAsync(Microsoft.AspNetCore.Components.EventCallbackWorkItem callback, object arg) { throw null; }
145-
protected virtual void OnAfterRender() { }
146-
protected virtual System.Threading.Tasks.Task OnAfterRenderAsync() { throw null; }
145+
protected virtual void OnAfterRender(bool firstRender) { }
146+
protected virtual System.Threading.Tasks.Task OnAfterRenderAsync(bool firstRender) { throw null; }
147147
protected virtual void OnInitialized() { }
148148
protected virtual System.Threading.Tasks.Task OnInitializedAsync() { throw null; }
149149
protected virtual void OnParametersSet() { }

src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ protected virtual void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering
142142
void Microsoft.AspNetCore.Components.IComponent.Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { }
143143
System.Threading.Tasks.Task Microsoft.AspNetCore.Components.IHandleAfterRender.OnAfterRenderAsync() { throw null; }
144144
System.Threading.Tasks.Task Microsoft.AspNetCore.Components.IHandleEvent.HandleEventAsync(Microsoft.AspNetCore.Components.EventCallbackWorkItem callback, object arg) { throw null; }
145-
protected virtual void OnAfterRender() { }
146-
protected virtual System.Threading.Tasks.Task OnAfterRenderAsync() { throw null; }
145+
protected virtual void OnAfterRender(bool firstRender) { }
146+
protected virtual System.Threading.Tasks.Task OnAfterRenderAsync(bool firstRender) { throw null; }
147147
protected virtual void OnInitialized() { }
148148
protected virtual System.Threading.Tasks.Task OnInitializedAsync() { throw null; }
149149
protected virtual void OnParametersSet() { }

src/Components/Components/src/ComponentBase.cs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public abstract class ComponentBase : IComponent, IHandleEvent, IHandleAfterRend
3030
private bool _initialized;
3131
private bool _hasNeverRendered = true;
3232
private bool _hasPendingQueuedRender;
33+
private bool _hasCalledOnAfterRender;
3334

3435
/// <summary>
3536
/// Constructs an instance of <see cref="ComponentBase"/>.
@@ -129,7 +130,17 @@ protected virtual bool ShouldRender()
129130
/// <summary>
130131
/// Method invoked after each time the component has been rendered.
131132
/// </summary>
132-
protected virtual void OnAfterRender()
133+
/// <param name="firstRender">
134+
/// Set to <c>true</c> if this is the first time <see cref="OnAfterRender(bool)"/> has been invoked
135+
/// on this component instance; otherwise <c>false</c>.
136+
/// </param>
137+
/// <remarks>
138+
/// The <see cref="OnAfterRender(bool)"/> and <see cref="OnAfterRenderAsync(bool)"/> lifecycle methods
139+
/// are useful for performing interop, or interacting with values recieved from <c>@ref</c>.
140+
/// Use the <paramref name="firstRender"/> parameter to ensure that initialization work is only performed
141+
/// once.
142+
/// </remarks>
143+
protected virtual void OnAfterRender(bool firstRender)
133144
{
134145
}
135146

@@ -138,8 +149,18 @@ protected virtual void OnAfterRender()
138149
/// not automatically re-render after the completion of any returned <see cref="Task"/>, because
139150
/// that would cause an infinite render loop.
140151
/// </summary>
152+
/// <param name="firstRender">
153+
/// Set to <c>true</c> if this is the first time <see cref="OnAfterRender(bool)"/> has been invoked
154+
/// on this component instance; otherwise <c>false</c>.
155+
/// </param>
141156
/// <returns>A <see cref="Task"/> representing any asynchronous operation.</returns>
142-
protected virtual Task OnAfterRenderAsync()
157+
/// <remarks>
158+
/// The <see cref="OnAfterRender(bool)"/> and <see cref="OnAfterRenderAsync(bool)"/> lifecycle methods
159+
/// are useful for performing interop, or interacting with values recieved from <c>@ref</c>.
160+
/// Use the <paramref name="firstRender"/> parameter to ensure that initialization work is only performed
161+
/// once.
162+
/// </remarks>
163+
protected virtual Task OnAfterRenderAsync(bool firstRender)
143164
=> Task.CompletedTask;
144165

145166
/// <summary>
@@ -298,9 +319,12 @@ Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object arg)
298319

299320
Task IHandleAfterRender.OnAfterRenderAsync()
300321
{
301-
OnAfterRender();
322+
var firstRender = !_hasCalledOnAfterRender;
323+
_hasCalledOnAfterRender |= true;
324+
325+
OnAfterRender(firstRender);
302326

303-
return OnAfterRenderAsync();
327+
return OnAfterRenderAsync(firstRender);
304328

305329
// Note that we don't call StateHasChanged to trigger a render after
306330
// handling this, because that would be an infinite loop. The only

src/Components/Components/test/ComponentBaseTest.cs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
using System;
55
using System.Diagnostics;
6+
using System.Reflection.Metadata.Ecma335;
7+
using System.Runtime.ExceptionServices;
68
using System.Threading;
79
using System.Threading.Tasks;
810
using Microsoft.AspNetCore.Components.Rendering;
@@ -261,6 +263,98 @@ public async Task DoesNotRenderAfterOnInitAsyncTaskIsCancelled()
261263
Assert.Equal(2, renderer.Batches.Count);
262264
}
263265

266+
[Fact]
267+
public async Task RunsOnAfterRender_AfterRenderingCompletes()
268+
{
269+
// Arrange
270+
var renderer = new TestRenderer();
271+
var component = new TestComponent() { Counter = 1 };
272+
273+
var onAfterRenderCompleted = false;
274+
component.OnAfterRenderLogic = (c, firstRender) =>
275+
{
276+
Assert.True(firstRender);
277+
Assert.Single(renderer.Batches);
278+
onAfterRenderCompleted = true;
279+
};
280+
281+
// Act
282+
var componentId = renderer.AssignRootComponentId(component);
283+
var renderTask = renderer.RenderRootComponentAsync(componentId);
284+
285+
// Assert
286+
await renderTask;
287+
Assert.True(onAfterRenderCompleted);
288+
289+
// Component should not be rendered again. OnAfterRender doesn't do that.
290+
Assert.Single(renderer.Batches);
291+
292+
// Act: Render again!
293+
onAfterRenderCompleted = false;
294+
component.OnAfterRenderLogic = (c, firstRender) =>
295+
{
296+
Assert.False(firstRender);
297+
Assert.Equal(2, renderer.Batches.Count);
298+
onAfterRenderCompleted = true;
299+
};
300+
301+
renderTask = renderer.RenderRootComponentAsync(componentId);
302+
303+
// Assert
304+
Assert.True(onAfterRenderCompleted);
305+
Assert.Equal(2, renderer.Batches.Count);
306+
await renderTask;
307+
}
308+
309+
[Fact]
310+
public async Task RunsOnAfterRenderAsync_AfterRenderingCompletes()
311+
{
312+
// Arrange
313+
var renderer = new TestRenderer();
314+
var component = new TestComponent() { Counter = 1 };
315+
316+
var onAfterRenderCompleted = false;
317+
var tcs = new TaskCompletionSource<object>();
318+
component.OnAfterRenderAsyncLogic = async (c, firstRender) =>
319+
{
320+
Assert.True(firstRender);
321+
Assert.Single(renderer.Batches);
322+
onAfterRenderCompleted = true;
323+
await tcs.Task;
324+
};
325+
326+
// Act
327+
var componentId = renderer.AssignRootComponentId(component);
328+
var renderTask = renderer.RenderRootComponentAsync(componentId);
329+
330+
// Assert
331+
tcs.SetResult(null);
332+
await renderTask;
333+
Assert.True(onAfterRenderCompleted);
334+
335+
// Component should not be rendered again. OnAfterRenderAsync doesn't do that.
336+
Assert.Single(renderer.Batches);
337+
338+
// Act: Render again!
339+
onAfterRenderCompleted = false;
340+
tcs = new TaskCompletionSource<object>();
341+
component.OnAfterRenderAsyncLogic = async (c, firstRender) =>
342+
{
343+
Assert.False(firstRender);
344+
Assert.Equal(2, renderer.Batches.Count);
345+
onAfterRenderCompleted = true;
346+
await tcs.Task;
347+
};
348+
349+
renderTask = renderer.RenderRootComponentAsync(componentId);
350+
351+
// Assert
352+
tcs.SetResult(null);
353+
await renderTask;
354+
Assert.True(onAfterRenderCompleted);
355+
Assert.Equal(2, renderer.Batches.Count);
356+
}
357+
264358
[Fact]
265359
public async Task DoesNotRenderAfterOnInitAsyncTaskIsCancelledUsingCancellationToken()
266360
{
@@ -386,6 +480,10 @@ private class TestComponent : ComponentBase
386480

387481
public bool RunsBaseOnParametersSetAsync { get; set; } = true;
388482

483+
public bool RunsBaseOnAfterRender { get; set; } = true;
484+
485+
public bool RunsBaseOnAfterRenderAsync { get; set; } = true;
486+
389487
public Action<TestComponent> OnInitLogic { get; set; }
390488

391489
public Func<TestComponent, Task> OnInitAsyncLogic { get; set; }
@@ -394,6 +492,10 @@ private class TestComponent : ComponentBase
394492

395493
public Func<TestComponent, Task> OnParametersSetAsyncLogic { get; set; }
396494

495+
public Action<TestComponent, bool> OnAfterRenderLogic { get; set; }
496+
497+
public Func<TestComponent, bool, Task> OnAfterRenderAsyncLogic { get; set; }
498+
397499
public int Counter { get; set; }
398500

399501
protected override void BuildRenderTree(RenderTreeBuilder builder)
@@ -448,6 +550,32 @@ protected override async Task OnParametersSetAsync()
448550
await OnParametersSetAsyncLogic(this);
449551
}
450552
}
553+
554+
protected override void OnAfterRender(bool firstRender)
555+
{
556+
if (RunsBaseOnAfterRender)
557+
{
558+
base.OnAfterRender(firstRender);
559+
}
560+
561+
if (OnAfterRenderLogic != null)
562+
{
563+
OnAfterRenderLogic(this, firstRender);
564+
}
565+
}
566+
567+
protected override async Task OnAfterRenderAsync(bool firstRender)
568+
{
569+
if (RunsBaseOnAfterRenderAsync)
570+
{
571+
await base.OnAfterRenderAsync(firstRender);
572+
}
573+
574+
if (OnAfterRenderAsyncLogic != null)
575+
{
576+
await OnAfterRenderAsyncLogic(this, firstRender);
577+
}
578+
}
451579
}
452580
}
453581
}

src/Components/Components/test/RendererTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4244,7 +4244,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
42444244
renderFactory(this)(builder);
42454245
}
42464246

4247-
protected override async Task OnAfterRenderAsync()
4247+
protected override async Task OnAfterRenderAsync(bool firstRender)
42484248
{
42494249
if (TryGetEntry(EventType.OnAfterRenderAsyncSync, out var entrySync))
42504250
{

src/Components/test/testassets/BasicTestApp/AfterRenderInteropComponent.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
@code {
77
ElementReference myInput;
88

9-
protected override void OnAfterRender()
9+
protected override void OnAfterRender(bool firstRender)
1010
{
1111
JSRuntime.InvokeAsync<object>("setElementValue", myInput, "Value set after render");
1212
}

src/Components/test/testassets/BasicTestApp/InteropOnInitializationComponent.razor

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,9 @@
2626
string infoFromJs;
2727
ElementReference myElem;
2828

29-
protected override async Task OnAfterRenderAsync()
29+
protected override async Task OnAfterRenderAsync(bool firstRender)
3030
{
31-
// TEMPORARY: Currently we need this guard to avoid making the interop
32-
// call during prerendering. Soon this will be unnecessary because we
33-
// will change OnAfterRenderAsync not to run during the prerendering phase.
34-
if (!ComponentContext.IsConnected)
35-
{
36-
return;
37-
}
38-
39-
if (infoFromJs == null)
31+
if (firstRender)
4032
{
4133
// We can only use the ElementRef in OnAfterRenderAsync (and not any
4234
// earlier lifecycle method), because there is no JS element until

src/Components/test/testassets/BasicTestApp/PrerenderedToInteractiveTransition.razor

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,10 @@
2222

2323
@code {
2424
int count;
25-
bool firstRender = false;
26-
protected override Task OnAfterRenderAsync()
25+
protected override Task OnAfterRenderAsync(bool firstRender)
2726
{
28-
if (!firstRender)
27+
if (firstRender)
2928
{
30-
firstRender = true;
31-
3229
// We need to queue another render when we connect, otherwise the
3330
// browser won't see anything.
3431
StateHasChanged();

src/Mvc/Mvc.ViewFeatures/test/HtmlHelperComponentExtensionsTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ private class OnAfterRenderComponent : ComponentBase
333333
{
334334
[Parameter] public OnAfterRenderState State { get; set; }
335335

336-
protected override void OnAfterRender()
336+
protected override void OnAfterRender(bool firstRender)
337337
{
338338
State.OnAfterRenderRan = true;
339339
}

0 commit comments

Comments
 (0)