Skip to content

Commit 521cabc

Browse files
authored
[Blazor][Fixes #12283] Prevent HtmlRenderer from calling OnAfterRender by default (#12684)
[Blazor] Prevents HtmlRenderer from calling OnAfterRender by default
1 parent 706309f commit 521cabc

File tree

3 files changed

+62
-3
lines changed

3 files changed

+62
-3
lines changed

src/Components/Components/src/Rendering/HtmlRenderer.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Diagnostics;
77
using System.Runtime.ExceptionServices;
8+
using System.Threading;
89
using System.Threading.Tasks;
910
using Microsoft.AspNetCore.Components.RenderTree;
1011
using Microsoft.Extensions.Logging;
@@ -21,6 +22,8 @@ public class HtmlRenderer : Renderer
2122
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"
2223
};
2324

25+
private static readonly Task CanceledRenderTask = Task.FromCanceled(new CancellationToken(canceled: true));
26+
2427
private readonly Func<string, string> _htmlEncoder;
2528

2629
/// <summary>
@@ -40,7 +43,19 @@ public HtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFacto
4043
/// <inheritdoc />
4144
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
4245
{
43-
return Task.CompletedTask;
46+
// By default we return a canceled task. This has the effect of making it so that the
47+
// OnAfterRenderAsync callbacks on components don't run by default.
48+
// This way, by default prerendering gets the correct behavior and other renderers
49+
// override the UpdateDisplayAsync method already, so those components can
50+
// either complete a task when the client acknowledges the render, or return a canceled task
51+
// when the renderer gets disposed.
52+
53+
// We believe that returning a canceled task is the right behavior as we expect that any class
54+
// that subclasses this class to provide an implementation for a given rendering scenario respects
55+
// the contract that OnAfterRender should only be called when the display has successfully been updated
56+
// and the application is interactive. (Element and component references are populated and JavaScript interop
57+
// is available).
58+
return CanceledRenderTask;
4459
}
4560

4661
/// <summary>

src/Components/Components/src/Rendering/Renderer.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -445,8 +445,12 @@ private Task InvokeRenderCompletedCalls(ArrayRange<RenderTreeDiff> updatedCompon
445445
{
446446
if (updateDisplayTask.IsCanceled)
447447
{
448-
// The display update was cancelled (maybe due to a timeout on the components server-side case or due
449-
// to the renderer being disposed)
448+
// The display update was canceled.
449+
// This can be due to a timeout on the components server-side case, or the renderer being disposed.
450+
451+
// The latter case is normal during prerendering, as the render never fully completes (the display never
452+
// gets updated, no references get populated and JavaScript interop is not available) and we simply discard
453+
// the renderer after producing the prerendered content.
450454
return Task.CompletedTask;
451455
}
452456
if (updateDisplayTask.IsFaulted)

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,26 @@ public async Task CanRender_ComponentWithParametersObject()
5858
Assert.Equal("<p>Hello Steve!</p>", content);
5959
}
6060

61+
[Fact]
62+
public async Task RenderComponent_DoesNotInvokeOnAfterRenderInComponent()
63+
{
64+
// Arrange
65+
var helper = CreateHelper();
66+
var writer = new StringWriter();
67+
68+
// Act
69+
var state = new OnAfterRenderState();
70+
var result = await helper.RenderComponentAsync<OnAfterRenderComponent>(new
71+
{
72+
State = state
73+
});
74+
result.WriteTo(writer, HtmlEncoder.Default);
75+
76+
// Assert
77+
Assert.Equal("<p>Hello</p>", writer.ToString());
78+
Assert.False(state.OnAfterRenderRan);
79+
}
80+
6181
[Fact]
6282
public async Task CanCatch_ComponentWithSynchronousException()
6383
{
@@ -309,6 +329,26 @@ protected override async Task OnParametersSetAsync()
309329
}
310330
}
311331

332+
private class OnAfterRenderComponent : ComponentBase
333+
{
334+
[Parameter] public OnAfterRenderState State { get; set; }
335+
336+
protected override void OnAfterRender()
337+
{
338+
State.OnAfterRenderRan = true;
339+
}
340+
341+
protected override void BuildRenderTree(RenderTreeBuilder builder)
342+
{
343+
builder.AddMarkupContent(0, "<p>Hello</p>");
344+
}
345+
}
346+
347+
private class OnAfterRenderState
348+
{
349+
public bool OnAfterRenderRan { get; set; }
350+
}
351+
312352
private class GreetingComponent : ComponentBase
313353
{
314354
[Parameter] public string Name { get; set; }

0 commit comments

Comments
 (0)