Mechanism for components running logic when parents change their properties
This commit is contained in:
parent
1c9c74c801
commit
76dafa819f
|
|
@ -11,7 +11,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
|
public abstract class BlazorComponent : IComponent, IHandlePropertiesChanged
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual void BuildRenderTree(RenderTreeBuilder builder)
|
public virtual void BuildRenderTree(RenderTreeBuilder builder)
|
||||||
|
|
@ -35,6 +35,11 @@ namespace Microsoft.AspNetCore.Blazor.Components
|
||||||
public virtual Task ExecuteAsync()
|
public virtual Task ExecuteAsync()
|
||||||
=> throw new NotImplementedException($"Blazor components do not implement {nameof(ExecuteAsync)}.");
|
=> throw new NotImplementedException($"Blazor components do not implement {nameof(ExecuteAsync)}.");
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual void OnPropertiesChanged()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles click events by invoking <paramref name="handler"/>.
|
/// Handles click events by invoking <paramref name="handler"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
// 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 notifications when their
|
||||||
|
/// properties are changed by their parent component.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHandlePropertiesChanged
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Notifies the component that its properties have changed.
|
||||||
|
/// </summary>
|
||||||
|
void OnPropertiesChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -271,9 +271,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
||||||
|
|
||||||
if (hasSetAnyProperty)
|
if (hasSetAnyProperty)
|
||||||
{
|
{
|
||||||
// TODO: Instead, call some OnPropertiesUpdated method on IComponent,
|
TriggerChildComponentRender(batchBuilder, newComponentNode);
|
||||||
// whose default implementation causes itself to be rerendered
|
|
||||||
_renderer.RenderInExistingBatch(batchBuilder, componentId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -508,11 +506,28 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
||||||
attributeNode.AttributeValue);
|
attributeNode.AttributeValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderer.RenderInExistingBatch(batchBuilder, node.ComponentId);
|
TriggerChildComponentRender(batchBuilder, node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void TriggerChildComponentRender(RenderBatchBuilder batchBuilder, in RenderTreeNode node)
|
||||||
|
{
|
||||||
|
if (node.Component is IHandlePropertiesChanged notifyableComponent)
|
||||||
|
{
|
||||||
|
// TODO: Ensure any exceptions thrown here are handled equivalently to
|
||||||
|
// unhandled exceptions during rendering.
|
||||||
|
notifyableComponent.OnPropertiesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Consider moving the responsibility for triggering re-rendering
|
||||||
|
// into the OnPropertiesChanged handler (if implemented) so that components
|
||||||
|
// can control whether any given set of property changes cause re-rendering.
|
||||||
|
// Not doing so yet because it's unclear that the usage patterns would be
|
||||||
|
// good to use.
|
||||||
|
_renderer.RenderInExistingBatch(batchBuilder, node.ComponentId);
|
||||||
|
}
|
||||||
|
|
||||||
private void DisposeChildComponents(RenderBatchBuilder batchBuilder, RenderTreeNode[] nodes, int elementOrComponentIndex)
|
private void DisposeChildComponents(RenderBatchBuilder batchBuilder, RenderTreeNode[] nodes, int elementOrComponentIndex)
|
||||||
{
|
{
|
||||||
var endIndex = nodes[elementOrComponentIndex].ElementDescendantsEndIndex;
|
var endIndex = nodes[elementOrComponentIndex].ElementDescendantsEndIndex;
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,23 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
||||||
elem => Assert.Equal("Child 3", elem.FindElement(By.ClassName("message")).Text));
|
elem => Assert.Equal("Child 3", elem.FindElement(By.ClassName("message")).Text));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ChildComponentsNotifiedWhenPropertiesChanged()
|
||||||
|
{
|
||||||
|
// Child component receives notification that lets it compute a property before first render
|
||||||
|
var appElement = MountTestComponent<PropertiesChangedHandlerParent>();
|
||||||
|
var suppliedValueElement = appElement.FindElement(By.ClassName("supplied"));
|
||||||
|
var computedValueElement = appElement.FindElement(By.ClassName("computed"));
|
||||||
|
var incrementButton = appElement.FindElement(By.TagName("button"));
|
||||||
|
Assert.Equal("You supplied: 100", suppliedValueElement.Text);
|
||||||
|
Assert.Equal("I computed: 200", computedValueElement.Text);
|
||||||
|
|
||||||
|
// When property changes, child is renotified before rerender
|
||||||
|
incrementButton.Click();
|
||||||
|
Assert.Equal("You supplied: 101", suppliedValueElement.Text);
|
||||||
|
Assert.Equal("I computed: 202", computedValueElement.Text);
|
||||||
|
}
|
||||||
|
|
||||||
private IWebElement MountTestComponent<TComponent>() where TComponent: IComponent
|
private IWebElement MountTestComponent<TComponent>() where TComponent: IComponent
|
||||||
{
|
{
|
||||||
var componentTypeName = typeof(TComponent).FullName;
|
var componentTypeName = typeof(TComponent).FullName;
|
||||||
|
|
|
||||||
|
|
@ -762,6 +762,59 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
||||||
Assert.Null(newComponentInstance.ObjectProperty); // To observe that the property wasn't even written, we nulled it out on the original
|
Assert.Null(newComponentInstance.ObjectProperty); // To observe that the property wasn't even written, we nulled it out on the original
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NotifiesIHandlePropertiesChangedBeforeFirstRender()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
newTree.OpenComponentElement<HandlePropertiesChangedComponent>(0);
|
||||||
|
newTree.CloseElement();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var batch = GetRenderedBatch();
|
||||||
|
var diffForChildComponent = batch.UpdatedComponents.Array[1];
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(diffForChildComponent.CurrentState,
|
||||||
|
node => AssertNode.Text(node, "Notifications: 1", 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NotifiesIHandlePropertiesChangedWhenChanged()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var newTree1 = new RenderTreeBuilder(renderer);
|
||||||
|
var newTree2 = new RenderTreeBuilder(renderer);
|
||||||
|
oldTree.OpenComponentElement<HandlePropertiesChangedComponent>(0);
|
||||||
|
oldTree.AddAttribute(1, nameof(HandlePropertiesChangedComponent.IntProperty), 123);
|
||||||
|
oldTree.CloseElement();
|
||||||
|
newTree1.OpenComponentElement<HandlePropertiesChangedComponent>(0);
|
||||||
|
newTree1.AddAttribute(1, nameof(HandlePropertiesChangedComponent.IntProperty), 123);
|
||||||
|
newTree1.CloseElement();
|
||||||
|
newTree2.OpenComponentElement<HandlePropertiesChangedComponent>(0);
|
||||||
|
newTree2.AddAttribute(1, nameof(HandlePropertiesChangedComponent.IntProperty), 456);
|
||||||
|
newTree2.CloseElement();
|
||||||
|
|
||||||
|
// Act/Assert 0: Initial render
|
||||||
|
var batch0 = GetRenderedBatch(new RenderTreeBuilder(renderer), oldTree);
|
||||||
|
var diffForChildComponent0 = batch0.UpdatedComponents.Array[1];
|
||||||
|
var childComponentNode = batch0.UpdatedComponents.Array[0].CurrentState.Array[0];
|
||||||
|
var childComponentInstance = (HandlePropertiesChangedComponent)childComponentNode.Component;
|
||||||
|
Assert.Equal(1, childComponentInstance.NotificationsCount);
|
||||||
|
Assert.Collection(diffForChildComponent0.CurrentState,
|
||||||
|
node => AssertNode.Text(node, "Notifications: 1", 0));
|
||||||
|
|
||||||
|
// Act/Assert 1: If properties didn't change, we don't notify
|
||||||
|
GetRenderedBatch(oldTree, newTree1);
|
||||||
|
Assert.Equal(1, childComponentInstance.NotificationsCount);
|
||||||
|
|
||||||
|
// Act/Assert 2: If properties did change, we do notify
|
||||||
|
var batch2 = GetRenderedBatch(newTree1, newTree2);
|
||||||
|
var diffForChildComponent2 = batch2.UpdatedComponents.Array[1];
|
||||||
|
Assert.Equal(2, childComponentInstance.NotificationsCount);
|
||||||
|
Assert.Collection(diffForChildComponent2.CurrentState,
|
||||||
|
node => AssertNode.Text(node, "Notifications: 2", 0));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CallsDisposeOnlyOnRemovedChildComponents()
|
public void CallsDisposeOnlyOnRemovedChildComponents()
|
||||||
{
|
{
|
||||||
|
|
@ -801,9 +854,12 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
||||||
}
|
}
|
||||||
|
|
||||||
private RenderBatch GetRenderedBatch()
|
private RenderBatch GetRenderedBatch()
|
||||||
|
=> GetRenderedBatch(oldTree, newTree);
|
||||||
|
|
||||||
|
private RenderBatch GetRenderedBatch(RenderTreeBuilder from, RenderTreeBuilder to)
|
||||||
{
|
{
|
||||||
var batchBuilder = new RenderBatchBuilder();
|
var batchBuilder = new RenderBatchBuilder();
|
||||||
diff.ApplyNewRenderTreeVersion(batchBuilder, 0, oldTree.GetNodes(), newTree.GetNodes());
|
diff.ApplyNewRenderTreeVersion(batchBuilder, 0, from.GetNodes(), to.GetNodes());
|
||||||
return batchBuilder.ToBatch();
|
return batchBuilder.ToBatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -835,6 +891,23 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class HandlePropertiesChangedComponent : IComponent, IHandlePropertiesChanged
|
||||||
|
{
|
||||||
|
public int NotificationsCount { get; private set; }
|
||||||
|
|
||||||
|
public int IntProperty { get; set; }
|
||||||
|
|
||||||
|
public void BuildRenderTree(RenderTreeBuilder builder)
|
||||||
|
{
|
||||||
|
builder.AddText(0, $"Notifications: {NotificationsCount}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnPropertiesChanged()
|
||||||
|
{
|
||||||
|
NotificationsCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class DisposableComponent : IComponent, IDisposable
|
private class DisposableComponent : IComponent, IDisposable
|
||||||
{
|
{
|
||||||
public int DisposalCount { get; private set; }
|
public int DisposalCount { get; private set; }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
<div class="supplied">You supplied: @SuppliedValue</div>
|
||||||
|
<div class="computed">I computed: @ComputedValue</div>
|
||||||
|
|
||||||
|
@functions {
|
||||||
|
public int SuppliedValue { get; set; }
|
||||||
|
private int ComputedValue { get; set; }
|
||||||
|
|
||||||
|
public override void OnPropertiesChanged()
|
||||||
|
{
|
||||||
|
ComputedValue = SuppliedValue * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<c:PropertiesChangedHandlerChild SuppliedValue="@valueToSupply" />
|
||||||
|
<button onclick=@{ valueToSupply++; }>Increment</button>
|
||||||
|
|
||||||
|
@functions {
|
||||||
|
private int valueToSupply = 100;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue