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)