diff --git a/src/Components/Components/src/Rendering/RenderTreeBuilder.cs b/src/Components/Components/src/Rendering/RenderTreeBuilder.cs index 0f5e161074..215fd84c53 100644 --- a/src/Components/Components/src/Rendering/RenderTreeBuilder.cs +++ b/src/Components/Components/src/Rendering/RenderTreeBuilder.cs @@ -667,15 +667,16 @@ namespace Microsoft.AspNetCore.Components.Rendering // internal because this should only be used during the post-event tree patching logic // It's expensive because it involves copying all the subsequent memory in the array - internal void InsertAttributeExpensive(int insertAtIndex, int sequence, string attributeName, object? attributeValue) + internal bool InsertAttributeExpensive(int insertAtIndex, int sequence, string attributeName, object? attributeValue) { // Replicate the same attribute omission logic as used elsewhere if ((attributeValue == null) || (attributeValue is bool boolValue && !boolValue)) { - return; + return false; } _entries.InsertExpensive(insertAtIndex, RenderTreeFrame.Attribute(sequence, attributeName, attributeValue)); + return true; } /// diff --git a/src/Components/Components/src/Rendering/RenderTreeUpdater.cs b/src/Components/Components/src/Rendering/RenderTreeUpdater.cs index 2f5db8b146..4b97d52317 100644 --- a/src/Components/Components/src/Rendering/RenderTreeUpdater.cs +++ b/src/Components/Components/src/Rendering/RenderTreeUpdater.cs @@ -75,7 +75,14 @@ namespace Microsoft.AspNetCore.Components.Rendering // If we get here, we didn't find the desired attribute, so we have to insert a new frame for it var insertAtIndex = elementFrameIndex + 1; - renderTreeBuilder.InsertAttributeExpensive(insertAtIndex, RenderTreeDiffBuilder.SystemAddedAttributeSequenceNumber, attributeName, attributeValue); + var didInsertFrame = renderTreeBuilder.InsertAttributeExpensive(insertAtIndex, RenderTreeDiffBuilder.SystemAddedAttributeSequenceNumber, attributeName, attributeValue); + if (!didInsertFrame) + { + // The builder decided to omit the new frame, e.g., because it's a false-valued bool + // In this case there's nothing else to update + return; + } + framesArray = renderTreeBuilder.GetFrames().Array; // Refresh in case it mutated due to the expansion // Update subtree length for this and all ancestor containers diff --git a/src/Components/Components/test/RenderTreeUpdaterTest.cs b/src/Components/Components/test/RenderTreeUpdaterTest.cs index 5011d6fb4a..de9ba94aca 100644 --- a/src/Components/Components/test/RenderTreeUpdaterTest.cs +++ b/src/Components/Components/test/RenderTreeUpdaterTest.cs @@ -127,6 +127,30 @@ namespace Microsoft.AspNetCore.Components.Test frame => AssertFrame.Attribute(frame, "eventname", v => Assert.IsType(v), 1)); } + [Fact] + public void OmitsAttributeIfNotFoundButValueIsOmissible() + { + // Arrange + var valuePropName = "testprop"; + var renderer = new TestRenderer(); + var builder = new RenderTreeBuilder(); + builder.OpenElement(0, "elem"); + builder.AddAttribute(1, "eventname", (Action)(() => { })); + builder.SetUpdatesAttributeName(valuePropName); + builder.CloseElement(); + var frames = builder.GetFrames(); + frames.Array[1] = frames.Array[1].WithAttributeEventHandlerId(123); + + // Act + RenderTreeUpdater.UpdateToMatchClientState(builder, 123, false); + frames = builder.GetFrames(); + + // Assert + Assert.Collection(frames.AsEnumerable(), + frame => AssertFrame.Element(frame, "elem", 2, 0), + frame => AssertFrame.Attribute(frame, "eventname", v => Assert.IsType(v), 1)); + } + [Fact] public void ExpandsAllAncestorsWhenAddingAttribute() {