Skip to content

[Blazor][Fixes #12283] Prevent HtmlRenderer from calling OnAfterRender by default #12684

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/Components/Components/src/Rendering/HtmlRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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;
Expand All @@ -21,6 +22,8 @@ public class HtmlRenderer : Renderer
"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<string, string> _htmlEncoder;

/// <summary>
Expand All @@ -40,7 +43,19 @@ public HtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFacto
/// <inheritdoc />
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;
}

/// <summary>
Expand Down
8 changes: 6 additions & 2 deletions src/Components/Components/src/Rendering/Renderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,12 @@ private Task InvokeRenderCompletedCalls(ArrayRange<RenderTreeDiff> updatedCompon
{
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,26 @@ public async Task CanRender_ComponentWithParametersObject()
Assert.Equal("<p>Hello Steve!</p>", 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<OnAfterRenderComponent>(new
{
State = state
});
result.WriteTo(writer, HtmlEncoder.Default);

// Assert
Assert.Equal("<p>Hello</p>", writer.ToString());
Assert.False(state.OnAfterRenderRan);
}

[Fact]
public async Task CanCatch_ComponentWithSynchronousException()
{
Expand Down Expand Up @@ -309,6 +329,26 @@ protected override async Task OnParametersSetAsync()
}
}

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, "<p>Hello</p>");
}
}

private class OnAfterRenderState
{
public bool OnAfterRenderRan { get; set; }
}

private class GreetingComponent : ComponentBase
{
[Parameter] public string Name { get; set; }
Expand Down