diff --git a/src/Components/Components/src/Rendering/HtmlRenderer.cs b/src/Components/Components/src/Rendering/HtmlRenderer.cs index c5c77e5169..5eb2e71095 100644 --- a/src/Components/Components/src/Rendering/HtmlRenderer.cs +++ b/src/Components/Components/src/Rendering/HtmlRenderer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.ExceptionServices; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.Extensions.Logging; @@ -21,6 +22,8 @@ namespace Microsoft.AspNetCore.Components.Rendering "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr" }; + private static readonly Task CanceledRenderTask = Task.FromCanceled(new CancellationToken(canceled: true)); + private readonly Func _htmlEncoder; /// @@ -40,7 +43,19 @@ namespace Microsoft.AspNetCore.Components.Rendering /// protected override Task UpdateDisplayAsync(in RenderBatch renderBatch) { - return Task.CompletedTask; + // By default we return a canceled task. This has the effect of making it so that the + // OnAfterRenderAsync callbacks on components don't run by default. + // This way, by default prerendering gets the correct behavior and other renderers + // override the UpdateDisplayAsync method already, so those components can + // either complete a task when the client acknowledges the render, or return a canceled task + // when the renderer gets disposed. + + // We believe that returning a canceled task is the right behavior as we expect that any class + // that subclasses this class to provide an implementation for a given rendering scenario respects + // the contract that OnAfterRender should only be called when the display has successfully been updated + // and the application is interactive. (Element and component references are populated and JavaScript interop + // is available). + return CanceledRenderTask; } /// diff --git a/src/Components/Components/src/Rendering/Renderer.cs b/src/Components/Components/src/Rendering/Renderer.cs index 8cbc6b78b6..6e0ed1633e 100644 --- a/src/Components/Components/src/Rendering/Renderer.cs +++ b/src/Components/Components/src/Rendering/Renderer.cs @@ -445,8 +445,12 @@ namespace Microsoft.AspNetCore.Components.Rendering { if (updateDisplayTask.IsCanceled) { - // The display update was cancelled (maybe due to a timeout on the components server-side case or due - // to the renderer being disposed) + // The display update was canceled. + // This can be due to a timeout on the components server-side case, or the renderer being disposed. + + // The latter case is normal during prerendering, as the render never fully completes (the display never + // gets updated, no references get populated and JavaScript interop is not available) and we simply discard + // the renderer after producing the prerendered content. return Task.CompletedTask; } if (updateDisplayTask.IsFaulted) diff --git a/src/Mvc/Mvc.ViewFeatures/test/HtmlHelperComponentExtensionsTests.cs b/src/Mvc/Mvc.ViewFeatures/test/HtmlHelperComponentExtensionsTests.cs index c8d0777dd3..665e050196 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/HtmlHelperComponentExtensionsTests.cs +++ b/src/Mvc/Mvc.ViewFeatures/test/HtmlHelperComponentExtensionsTests.cs @@ -58,6 +58,26 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test Assert.Equal("

Hello Steve!

", content); } + [Fact] + public async Task RenderComponent_DoesNotInvokeOnAfterRenderInComponent() + { + // Arrange + var helper = CreateHelper(); + var writer = new StringWriter(); + + // Act + var state = new OnAfterRenderState(); + var result = await helper.RenderComponentAsync(new + { + State = state + }); + result.WriteTo(writer, HtmlEncoder.Default); + + // Assert + Assert.Equal("

Hello

", writer.ToString()); + Assert.False(state.OnAfterRenderRan); + } + [Fact] public async Task CanCatch_ComponentWithSynchronousException() { @@ -309,6 +329,26 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test } } + private class OnAfterRenderComponent : ComponentBase + { + [Parameter] public OnAfterRenderState State { get; set; } + + protected override void OnAfterRender() + { + State.OnAfterRenderRan = true; + } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddMarkupContent(0, "

Hello

"); + } + } + + private class OnAfterRenderState + { + public bool OnAfterRenderRan { get; set; } + } + private class GreetingComponent : ComponentBase { [Parameter] public string Name { get; set; }