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

[Blazor] Prevents HtmlRenderer from calling OnAfterRender by default
This commit is contained in:
Javier Calvarro Nelson 2019-08-03 13:03:48 +02:00 committed by GitHub
parent 706309f266
commit 521cabc545
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 62 additions and 3 deletions

View File

@ -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<string, string> _htmlEncoder;
/// <summary>
@ -40,7 +43,19 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// <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>

View File

@ -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)

View File

@ -58,6 +58,26 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
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()
{
@ -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, "<p>Hello</p>");
}
}
private class OnAfterRenderState
{
public bool OnAfterRenderRan { get; set; }
}
private class GreetingComponent : ComponentBase
{
[Parameter] public string Name { get; set; }