OnAfterRender / OnAfterRenderAsync (#691)

* Implement OnAfterRender and OnAfterRenderAsync

* Add E2E test combining OnAfterRender with "ref" and JS interop

... because this combination is the key to integration with 3rd-party JS
libs
This commit is contained in:
Steve Sanderson 2018-04-27 19:45:19 +01:00 committed by GitHub
parent 4033560734
commit 76bf82eb49
9 changed files with 195 additions and 3 deletions

View File

@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Blazor.Components
/// Optional base class for Blazor components. Alternatively, Blazor components may
/// implement <see cref="IComponent"/> directly.
/// </summary>
public abstract class BlazorComponent : IComponent, IHandleEvent
public abstract class BlazorComponent : IComponent, IHandleEvent, IHandleAfterRender
{
/// <summary>
/// Specifies the name of the <see cref="RenderTree"/>-building method.
@ -87,6 +87,7 @@ namespace Microsoft.AspNetCore.Blazor.Components
/// Method invoked when the component has received parameters from its parent in
/// the render tree, and the incoming values have been assigned to properties.
/// </summary>
/// <returns>A <see cref="Task"/> representing any asynchronous operation, or <see langword="null"/>.</returns>
protected virtual Task OnParametersSetAsync()
=> null;
@ -115,6 +116,22 @@ namespace Microsoft.AspNetCore.Blazor.Components
protected virtual bool ShouldRender()
=> true;
/// <summary>
/// Method invoked after each time the component has been rendered.
/// </summary>
protected virtual void OnAfterRender()
{
}
/// <summary>
/// Method invoked after each time the component has been rendered. Note that the component does
/// not automatically re-render after the completion of any returned <see cref="Task"/>, because
/// that would cause an infinite render loop.
/// </summary>
/// <returns>A <see cref="Task"/> representing any asynchronous operation, or <see langword="null"/>.</returns>
protected virtual Task OnAfterRenderAsync()
=> null;
void IComponent.Init(RenderHandle renderHandle)
{
// This implicitly means a BlazorComponent can only be associated with a single
@ -191,5 +208,24 @@ namespace Microsoft.AspNetCore.Blazor.Components
task.ContinueWith(ContinueAfterLifecycleTask);
}
void IHandleAfterRender.OnAfterRender()
{
OnAfterRender();
OnAfterRenderAsync()?.ContinueWith(task =>
{
// Note that we don't call StateHasChanged to trigger a render after
// handling this, because that would be an infinite loop. The only
// reason we have OnAfterRenderAsync is so that the developer doesn't
// have to use "async void" and do their own exception handling in
// the case where they want to start an async task.
if (task.Exception != null)
{
HandleException(task.Exception);
}
});
}
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Blazor.Components
{
/// <summary>
/// Interface implemented by components that receive notification that they have been rendered.
/// </summary>
public interface IHandleAfterRender
{
/// <summary>
/// Notifies the component that it has been rendered.
/// </summary>
void OnAfterRender();
}
}

View File

@ -86,5 +86,8 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
$"events because it does not implement {typeof(IHandleEvent).FullName}.");
}
}
public void NotifyRenderCompleted()
=> (_component as IHandleAfterRender)?.OnAfterRender();
}
}

View File

@ -164,7 +164,9 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
RenderInExistingBatch(nextToRender);
}
UpdateDisplay(_batchBuilder.ToBatch());
var batch = _batchBuilder.ToBatch();
UpdateDisplay(batch);
InvokeRenderCompletedCalls(batch.UpdatedComponents);
}
finally
{
@ -174,6 +176,17 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
}
}
private void InvokeRenderCompletedCalls(ArrayRange<RenderTreeDiff> updatedComponents)
{
var array = updatedComponents.Array;
for (var i = 0; i < updatedComponents.Count; i++)
{
// The component might be rendered and disposed in the same batch (if its parent
// was rendered later in the batch, and removed the child from the tree).
GetOptionalComponentState(array[i].ComponentId)?.NotifyRenderCompleted();
}
}
private void RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
{
renderQueueEntry.ComponentState

View File

@ -370,5 +370,13 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
resetButton.Click();
Assert.Equal("Current count: 0", appElement.FindElement(currentCountTextSelector).Text);
}
[Fact]
public void CanUseJsInteropForRefElementsDuringOnAfterRender()
{
var appElement = MountTestComponent<AfterRenderInteropComponent>();
var inputElement = appElement.FindElement(By.TagName("input"));
Assert.Equal("Value set after render", inputElement.GetAttribute("value"));
}
}
}

View File

@ -991,6 +991,86 @@ namespace Microsoft.AspNetCore.Blazor.Test
});
}
[Fact]
public void CallsAfterRenderOnEachRender()
{
// Arrange
var onAfterRenderCallCountLog = new List<int>();
var component = new AfterRenderCaptureComponent();
var renderer = new TestRenderer
{
OnUpdateDisplay = _ => onAfterRenderCallCountLog.Add(component.OnAfterRenderCallCount)
};
renderer.AssignComponentId(component);
// Act
component.TriggerRender();
// Assert
// When the display was first updated, OnAfterRender had not yet been called
Assert.Equal(new[] { 0 }, onAfterRenderCallCountLog);
// But OnAfterRender was called since then
Assert.Equal(1, component.OnAfterRenderCallCount);
// Act/Assert 2: On a subsequent render, the same happens again
component.TriggerRender();
Assert.Equal(new[] { 0, 1 }, onAfterRenderCallCountLog);
Assert.Equal(2, component.OnAfterRenderCallCount);
}
[Fact]
public void DoesNotCallOnAfterRenderForComponentsNotRendered()
{
// Arrange
var showComponent3 = true;
var parentComponent = new TestComponent(builder =>
{
// First child will be re-rendered because we'll change its param
builder.OpenComponent<AfterRenderCaptureComponent>(0);
builder.AddAttribute(1, "some param", showComponent3);
builder.CloseComponent();
// Second child will not be re-rendered because nothing changes
builder.OpenComponent<AfterRenderCaptureComponent>(2);
builder.CloseComponent();
// Third component will be disposed
if (showComponent3)
{
builder.OpenComponent<AfterRenderCaptureComponent>(3);
builder.CloseComponent();
}
});
var renderer = new TestRenderer();
var parentComponentId = renderer.AssignComponentId(parentComponent);
// Act: First render
parentComponent.TriggerRender();
// Assert: All child components were notified of "after render"
var batch1 = renderer.Batches.Single();
var parentComponentEdits1 = batch1.DiffsByComponentId[parentComponentId].Single().Edits;
var childComponents = parentComponentEdits1
.Select(
edit => (AfterRenderCaptureComponent)batch1.ReferenceFrames[edit.ReferenceFrameIndex].Component)
.ToArray();
Assert.Equal(1, childComponents[0].OnAfterRenderCallCount);
Assert.Equal(1, childComponents[1].OnAfterRenderCallCount);
Assert.Equal(1, childComponents[2].OnAfterRenderCallCount);
// Act: Second render
showComponent3 = false;
parentComponent.TriggerRender();
// Assert: Only the re-rendered component was notified of "after render"
var batch2 = renderer.Batches.Skip(1).Single();
Assert.Equal(2, batch2.DiffsInOrder.Count); // Parent and first child
Assert.Equal(1, batch2.DisposedComponentIDs.Count); // Third child
Assert.Equal(2, childComponents[0].OnAfterRenderCallCount); // Retained and re-rendered
Assert.Equal(1, childComponents[1].OnAfterRenderCallCount); // Retained and not re-rendered
Assert.Equal(1, childComponents[2].OnAfterRenderCallCount); // Disposed
}
private class NoOpRenderer : Renderer
{
public NoOpRenderer() : base(new TestServiceProvider())
@ -1213,5 +1293,24 @@ namespace Microsoft.AspNetCore.Blazor.Test
builder.CloseElement();
}
}
private class AfterRenderCaptureComponent : AutoRenderComponent, IComponent, IHandleAfterRender
{
public int OnAfterRenderCallCount { get; private set; }
public void OnAfterRender()
{
OnAfterRenderCallCount++;
}
void IComponent.SetParameters(ParameterCollection parameters)
{
TriggerRender();
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
}
}
}
}

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.Rendering;
using Microsoft.AspNetCore.Blazor.RenderTree;
namespace Microsoft.AspNetCore.Blazor.Test.Helpers
{
@ -20,6 +19,8 @@ namespace Microsoft.AspNetCore.Blazor.Test.Helpers
{
}
public Action<RenderBatch> OnUpdateDisplay { get; set; }
public List<CapturedBatch> Batches { get; }
= new List<CapturedBatch>();
@ -34,6 +35,8 @@ namespace Microsoft.AspNetCore.Blazor.Test.Helpers
protected override void UpdateDisplay(RenderBatch renderBatch)
{
OnUpdateDisplay?.Invoke(renderBatch);
var capturedBatch = new CapturedBatch();
Batches.Add(capturedBatch);

View File

@ -0,0 +1,13 @@
@using Microsoft.AspNetCore.Blazor
@using Microsoft.AspNetCore.Blazor.Browser.Interop
<input ref="myInput" value="Value set during render" />
@functions {
ElementRef myInput;
protected override void OnAfterRender()
{
RegisteredFunction.Invoke<object>("setElementValue", myInput, "Value set after render");
}
}

View File

@ -31,6 +31,7 @@
<option value="BasicTestApp.LogicalElementInsertionCases">Logical element insertion cases</option>
<option value="BasicTestApp.ElementRefComponent">Element ref component</option>
<option value="BasicTestApp.ComponentRefComponent">Component ref component</option>
<option value="BasicTestApp.AfterRenderInteropComponent">After-render interop component</option>
<!--<option value="BasicTestApp.RouterTest.Default">Router</option> Excluded because it requires additional setup to work correctly when loaded manually -->
</select>
&nbsp;