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:
parent
4033560734
commit
76bf82eb49
|
|
@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Blazor.Components
|
||||||
/// Optional base class for Blazor components. Alternatively, Blazor components may
|
/// Optional base class for Blazor components. Alternatively, Blazor components may
|
||||||
/// implement <see cref="IComponent"/> directly.
|
/// implement <see cref="IComponent"/> directly.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class BlazorComponent : IComponent, IHandleEvent
|
public abstract class BlazorComponent : IComponent, IHandleEvent, IHandleAfterRender
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Specifies the name of the <see cref="RenderTree"/>-building method.
|
/// 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
|
/// Method invoked when the component has received parameters from its parent in
|
||||||
/// the render tree, and the incoming values have been assigned to properties.
|
/// the render tree, and the incoming values have been assigned to properties.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <returns>A <see cref="Task"/> representing any asynchronous operation, or <see langword="null"/>.</returns>
|
||||||
protected virtual Task OnParametersSetAsync()
|
protected virtual Task OnParametersSetAsync()
|
||||||
=> null;
|
=> null;
|
||||||
|
|
||||||
|
|
@ -115,6 +116,22 @@ namespace Microsoft.AspNetCore.Blazor.Components
|
||||||
protected virtual bool ShouldRender()
|
protected virtual bool ShouldRender()
|
||||||
=> true;
|
=> 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)
|
void IComponent.Init(RenderHandle renderHandle)
|
||||||
{
|
{
|
||||||
// This implicitly means a BlazorComponent can only be associated with a single
|
// This implicitly means a BlazorComponent can only be associated with a single
|
||||||
|
|
@ -191,5 +208,24 @@ namespace Microsoft.AspNetCore.Blazor.Components
|
||||||
|
|
||||||
task.ContinueWith(ContinueAfterLifecycleTask);
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -86,5 +86,8 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
|
||||||
$"events because it does not implement {typeof(IHandleEvent).FullName}.");
|
$"events because it does not implement {typeof(IHandleEvent).FullName}.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void NotifyRenderCompleted()
|
||||||
|
=> (_component as IHandleAfterRender)?.OnAfterRender();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,9 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
|
||||||
RenderInExistingBatch(nextToRender);
|
RenderInExistingBatch(nextToRender);
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateDisplay(_batchBuilder.ToBatch());
|
var batch = _batchBuilder.ToBatch();
|
||||||
|
UpdateDisplay(batch);
|
||||||
|
InvokeRenderCompletedCalls(batch.UpdatedComponents);
|
||||||
}
|
}
|
||||||
finally
|
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)
|
private void RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
|
||||||
{
|
{
|
||||||
renderQueueEntry.ComponentState
|
renderQueueEntry.ComponentState
|
||||||
|
|
|
||||||
|
|
@ -370,5 +370,13 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
||||||
resetButton.Click();
|
resetButton.Click();
|
||||||
Assert.Equal("Current count: 0", appElement.FindElement(currentCountTextSelector).Text);
|
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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
private class NoOpRenderer : Renderer
|
||||||
{
|
{
|
||||||
public NoOpRenderer() : base(new TestServiceProvider())
|
public NoOpRenderer() : base(new TestServiceProvider())
|
||||||
|
|
@ -1213,5 +1293,24 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
||||||
builder.CloseElement();
|
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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Blazor.Components;
|
using Microsoft.AspNetCore.Blazor.Components;
|
||||||
using Microsoft.AspNetCore.Blazor.Rendering;
|
using Microsoft.AspNetCore.Blazor.Rendering;
|
||||||
using Microsoft.AspNetCore.Blazor.RenderTree;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Blazor.Test.Helpers
|
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; }
|
public List<CapturedBatch> Batches { get; }
|
||||||
= new List<CapturedBatch>();
|
= new List<CapturedBatch>();
|
||||||
|
|
||||||
|
|
@ -34,6 +35,8 @@ namespace Microsoft.AspNetCore.Blazor.Test.Helpers
|
||||||
|
|
||||||
protected override void UpdateDisplay(RenderBatch renderBatch)
|
protected override void UpdateDisplay(RenderBatch renderBatch)
|
||||||
{
|
{
|
||||||
|
OnUpdateDisplay?.Invoke(renderBatch);
|
||||||
|
|
||||||
var capturedBatch = new CapturedBatch();
|
var capturedBatch = new CapturedBatch();
|
||||||
Batches.Add(capturedBatch);
|
Batches.Add(capturedBatch);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
<option value="BasicTestApp.LogicalElementInsertionCases">Logical element insertion cases</option>
|
<option value="BasicTestApp.LogicalElementInsertionCases">Logical element insertion cases</option>
|
||||||
<option value="BasicTestApp.ElementRefComponent">Element ref component</option>
|
<option value="BasicTestApp.ElementRefComponent">Element ref component</option>
|
||||||
<option value="BasicTestApp.ComponentRefComponent">Component 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 -->
|
<!--<option value="BasicTestApp.RouterTest.Default">Router</option> Excluded because it requires additional setup to work correctly when loaded manually -->
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue