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
|
||||
/// implement <see cref="IComponent"/> directly.
|
||||
/// </summary>
|
||||
public abstract class BlazorComponent : IComponent
|
||||
public abstract class BlazorComponent : IComponent, IHandlePropertiesChanged
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public virtual void BuildRenderTree(RenderTreeBuilder builder)
|
||||
|
|
@ -35,6 +35,11 @@ namespace Microsoft.AspNetCore.Blazor.Components
|
|||
public virtual Task ExecuteAsync()
|
||||
=> throw new NotImplementedException($"Blazor components do not implement {nameof(ExecuteAsync)}.");
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void OnPropertiesChanged()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles click events by invoking <paramref name="handler"/>.
|
||||
/// </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)
|
||||
{
|
||||
// TODO: Instead, call some OnPropertiesUpdated method on IComponent,
|
||||
// whose default implementation causes itself to be rerendered
|
||||
_renderer.RenderInExistingBatch(batchBuilder, componentId);
|
||||
TriggerChildComponentRender(batchBuilder, newComponentNode);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -508,11 +506,28 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
|||
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)
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
[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
|
||||
{
|
||||
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
|
||||
}
|
||||
|
||||
[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]
|
||||
public void CallsDisposeOnlyOnRemovedChildComponents()
|
||||
{
|
||||
|
|
@ -801,9 +854,12 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
}
|
||||
|
||||
private RenderBatch GetRenderedBatch()
|
||||
=> GetRenderedBatch(oldTree, newTree);
|
||||
|
||||
private RenderBatch GetRenderedBatch(RenderTreeBuilder from, RenderTreeBuilder to)
|
||||
{
|
||||
var batchBuilder = new RenderBatchBuilder();
|
||||
diff.ApplyNewRenderTreeVersion(batchBuilder, 0, oldTree.GetNodes(), newTree.GetNodes());
|
||||
diff.ApplyNewRenderTreeVersion(batchBuilder, 0, from.GetNodes(), to.GetNodes());
|
||||
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
|
||||
{
|
||||
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