diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffComputer.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffComputer.cs index 7479658696..ba6f156b18 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffComputer.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffComputer.cs @@ -179,6 +179,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree ref var newComponentNode = ref newTree[newComponentIndex]; var componentId = oldComponentNode.ComponentId; var componentInstance = oldComponentNode.Component; + var hasSetAnyProperty = false; // Preserve the actual componentInstance newComponentNode.SetChildComponentInstance(componentId, componentInstance); @@ -207,6 +208,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree if (!Equals(oldPropertyValue, newPropertyValue)) { SetChildComponentProperty(componentInstance, newName, newPropertyValue); + hasSetAnyProperty = true; } } else @@ -216,6 +218,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree // could consider removing the 'name equality' check entirely for perf SetChildComponentProperty(componentInstance, newName, newPropertyValue); RemoveChildComponentProperty(componentInstance, oldName); + hasSetAnyProperty = true; } oldStartIndex++; @@ -235,6 +238,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree SetChildComponentProperty(componentInstance, newTree[newStartIndex].AttributeName, newTree[newStartIndex].AttributeValue); + hasSetAnyProperty = true; newStartIndex++; hasMoreNew = newEndIndexIncl >= newStartIndex; } @@ -242,11 +246,19 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree { RemoveChildComponentProperty(componentInstance, oldTree[oldStartIndex].AttributeName); + hasSetAnyProperty = true; oldStartIndex++; hasMoreOld = oldEndIndexIncl >= oldStartIndex; } } } + + if (hasSetAnyProperty) + { + // TODO: Instead, call some OnPropertiesUpdated method on IComponent, + // whose default implementation causes itself to be rerendered + _renderer.RenderComponent(componentId); + } } private static void RemoveChildComponentProperty(IComponent component, string componentPropertyName) diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs index e7f344bc2a..8adf4f2dd4 100644 --- a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs @@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering /// Updates the rendered state of the specified . /// /// The identifier of the to render. - protected void RenderComponent(int componentId) + protected internal void RenderComponent(int componentId) => GetRequiredComponentState(componentId).Render(); /// diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffComputerTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffComputerTest.cs index f172ef03ac..9c44beb9ff 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffComputerTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffComputerTest.cs @@ -819,7 +819,8 @@ namespace Microsoft.AspNetCore.Blazor.Test private class FakeRenderer : Renderer { internal protected override void UpdateDisplay(int componentId, RenderTreeDiff renderTreeDiff) - => throw new NotImplementedException(); + { + } } private class FakeComponent : IComponent @@ -831,7 +832,8 @@ namespace Microsoft.AspNetCore.Blazor.Test private string PrivateProperty { get; set; } public void BuildRenderTree(RenderTreeBuilder builder) - => throw new NotImplementedException(); + { + } } private class FakeComponent2 : IComponent diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs index 191444dc09..58aff8c38c 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs @@ -374,6 +374,42 @@ namespace Microsoft.AspNetCore.Blazor.Test Assert.Same(objectThatWillNotChange, updatedComponentInstance.ObjectProperty); } + [Fact] + public void ReRendersChildComponentsWhenPropertiesChange() + { + // Arrange: First render + var renderer = new TestRenderer(); + var firstRender = true; + var component = new TestComponent(builder => + { + builder.OpenComponentElement(1); + builder.AddAttribute(2, nameof(MessageComponent.Message), firstRender ? "first" : "second"); + builder.CloseElement(); + }); + + var rootComponentId = renderer.AssignComponentId(component); + renderer.RenderComponent(rootComponentId); + + var childComponentId = renderer.RenderTreesByComponentId[rootComponentId] + .Single(node => node.NodeType == RenderTreeNodeType.Component) + .ComponentId; + + // This isn't strictly necessary for the test, but it's more common for components + // to be updated after their first render than before it + renderer.RenderComponent(childComponentId); + + // Act: Second render + firstRender = false; + renderer.RenderComponent(rootComponentId); + + var updatedComponentNode = renderer.RenderTreesByComponentId[rootComponentId] + .Single(node => node.NodeType == RenderTreeNodeType.Component); + + // Assert + Assert.Collection(renderer.RenderTreesByComponentId[updatedComponentNode.ComponentId], + node => AssertNode.Text(node, "second")); + } + private class NoOpRenderer : Renderer { public new int AssignComponentId(IComponent component)