diff --git a/src/Microsoft.AspNetCore.Blazor/Components/BlazorComponent.cs b/src/Microsoft.AspNetCore.Blazor/Components/BlazorComponent.cs
index 9555b94191..b4f29e8e5c 100644
--- a/src/Microsoft.AspNetCore.Blazor/Components/BlazorComponent.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Components/BlazorComponent.cs
@@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Blazor.Components
/// Optional base class for Blazor components. Alternatively, Blazor components may
/// implement directly.
///
- public abstract class BlazorComponent : IComponent, IHandleEvent
+ public abstract class BlazorComponent : IComponent, IHandleEvent, IHandleAfterRender
{
///
/// Specifies the name of the -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.
///
+ /// A representing any asynchronous operation, or .
protected virtual Task OnParametersSetAsync()
=> null;
@@ -115,6 +116,22 @@ namespace Microsoft.AspNetCore.Blazor.Components
protected virtual bool ShouldRender()
=> true;
+ ///
+ /// Method invoked after each time the component has been rendered.
+ ///
+ protected virtual void OnAfterRender()
+ {
+ }
+
+ ///
+ /// 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 , because
+ /// that would cause an infinite render loop.
+ ///
+ /// A representing any asynchronous operation, or .
+ 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);
+ }
+ });
+ }
}
}
diff --git a/src/Microsoft.AspNetCore.Blazor/Components/IHandleAfterRender.cs b/src/Microsoft.AspNetCore.Blazor/Components/IHandleAfterRender.cs
new file mode 100644
index 0000000000..4ed3eaa505
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor/Components/IHandleAfterRender.cs
@@ -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
+{
+ ///
+ /// Interface implemented by components that receive notification that they have been rendered.
+ ///
+ public interface IHandleAfterRender
+ {
+ ///
+ /// Notifies the component that it has been rendered.
+ ///
+ void OnAfterRender();
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs
index ddd30f0370..e981c9eba0 100644
--- a/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs
@@ -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();
}
}
diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs
index 51ed35de4b..a6c5431547 100644
--- a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs
@@ -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 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
diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/ComponentRenderingTest.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/ComponentRenderingTest.cs
index 33cbcc6a73..b19358a2cf 100644
--- a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/ComponentRenderingTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/ComponentRenderingTest.cs
@@ -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();
+ var inputElement = appElement.FindElement(By.TagName("input"));
+ Assert.Equal("Value set after render", inputElement.GetAttribute("value"));
+ }
}
}
diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs
index 8d9a0f0557..87ba995fe7 100644
--- a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs
@@ -991,6 +991,86 @@ namespace Microsoft.AspNetCore.Blazor.Test
});
}
+ [Fact]
+ public void CallsAfterRenderOnEachRender()
+ {
+ // Arrange
+ var onAfterRenderCallCountLog = new List();
+ 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(0);
+ builder.AddAttribute(1, "some param", showComponent3);
+ builder.CloseComponent();
+
+ // Second child will not be re-rendered because nothing changes
+ builder.OpenComponent(2);
+ builder.CloseComponent();
+
+ // Third component will be disposed
+ if (showComponent3)
+ {
+ builder.OpenComponent(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)
+ {
+ }
+ }
}
}
diff --git a/test/shared/TestRenderer.cs b/test/shared/TestRenderer.cs
index 6d1bdc9ff9..2817338a8c 100644
--- a/test/shared/TestRenderer.cs
+++ b/test/shared/TestRenderer.cs
@@ -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 OnUpdateDisplay { get; set; }
+
public List Batches { get; }
= new List();
@@ -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);
diff --git a/test/testapps/BasicTestApp/AfterRenderInteropComponent.cshtml b/test/testapps/BasicTestApp/AfterRenderInteropComponent.cshtml
new file mode 100644
index 0000000000..48740eef18
--- /dev/null
+++ b/test/testapps/BasicTestApp/AfterRenderInteropComponent.cshtml
@@ -0,0 +1,13 @@
+@using Microsoft.AspNetCore.Blazor
+@using Microsoft.AspNetCore.Blazor.Browser.Interop
+
+
+
+@functions {
+ ElementRef myInput;
+
+ protected override void OnAfterRender()
+ {
+ RegisteredFunction.Invoke