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
|
||||
/// 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}.");
|
||||
}
|
||||
}
|
||||
|
||||
public void NotifyRenderCompleted()
|
||||
=> (_component as IHandleAfterRender)?.OnAfterRender();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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.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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue