Mechanism for components running logic when parents change their properties

This commit is contained in:
Steve Sanderson 2018-01-31 16:19:01 +00:00
parent 1c9c74c801
commit 76dafa819f
7 changed files with 151 additions and 6 deletions

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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; }

View File

@ -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;
}
}

View File

@ -0,0 +1,6 @@
<c:PropertiesChangedHandlerChild SuppliedValue="@valueToSupply" />
<button onclick=@{ valueToSupply++; }>Increment</button>
@functions {
private int valueToSupply = 100;
}