diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs index 00f742407d..d21a2c915f 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs @@ -57,17 +57,6 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree entry = entry.WithElementSubtreeLength(_entries.Count - indexOfEntryBeingClosed); } - /// - /// Marks a previously appended component frame as closed. Calls to this method - /// must be balanced with calls to . - /// - public void CloseComponent() - { - var indexOfEntryBeingClosed = _openElementIndices.Pop(); - ref var entry = ref _entries.Buffer[indexOfEntryBeingClosed]; - entry = entry.WithComponentSubtreeLength(_entries.Count - indexOfEntryBeingClosed); - } - /// /// Appends a frame representing text content. /// @@ -171,6 +160,39 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree Append(RenderTreeFrame.ChildComponent(sequence)); } + /// + /// Marks a previously appended component frame as closed. Calls to this method + /// must be balanced with calls to . + /// + public void CloseComponent() + { + var indexOfEntryBeingClosed = _openElementIndices.Pop(); + ref var entry = ref _entries.Buffer[indexOfEntryBeingClosed]; + entry = entry.WithComponentSubtreeLength(_entries.Count - indexOfEntryBeingClosed); + } + + /// + /// Appends a frame denoting the start of a region (that is, a tree fragment that is + /// processed as a unit for the purposes of diffing). + /// + /// An integer that represents the position of the instruction in the source code. + public void OpenRegion(int sequence) + { + _openElementIndices.Push(_entries.Count); + Append(RenderTreeFrame.Region(sequence)); + } + + /// + /// Marks a previously appended region frame as closed. Calls to this method + /// must be balanced with calls to . + /// + internal void CloseRegion() + { + var indexOfEntryBeingClosed = _openElementIndices.Pop(); + ref var entry = ref _entries.Buffer[indexOfEntryBeingClosed]; + entry = entry.WithRegionSubtreeLength(_entries.Count - indexOfEntryBeingClosed); + } + private void AssertCanAddAttribute() { if (_lastNonAttributeFrameType != RenderTreeFrameType.Element diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrame.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrame.cs index 2f113b6b46..81b45ed5bc 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrame.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrame.cs @@ -118,6 +118,17 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree /// [FieldOffset(24)] public readonly IComponent Component; + // -------------------------------------------------------------------------------- + // RenderTreeFrameType.Region + // -------------------------------------------------------------------------------- + + /// + /// If the property equals + /// gets the number of frames in the subtree for which this frame is the root. + /// The value is zero if the frame has not yet been closed. + /// + [FieldOffset(8)] public readonly int RegionSubtreeLength; + private RenderTreeFrame(int sequence, string elementName, int elementSubtreeLength) : this() { @@ -170,6 +181,14 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree AttributeEventHandlerId = eventHandlerId; } + private RenderTreeFrame(int sequence, int regionSubtreeLength) + : this() + { + FrameType = RenderTreeFrameType.Region; + Sequence = sequence; + RegionSubtreeLength = regionSubtreeLength; + } + internal static RenderTreeFrame Element(int sequence, string elementName) => new RenderTreeFrame(sequence, elementName: elementName, elementSubtreeLength: 0); @@ -185,6 +204,9 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree internal static RenderTreeFrame ChildComponent(int sequence) where T : IComponent => new RenderTreeFrame(sequence, typeof(T), 0); + internal static RenderTreeFrame Region(int sequence) + => new RenderTreeFrame(sequence, regionSubtreeLength: 0); + internal RenderTreeFrame WithElementSubtreeLength(int elementSubtreeLength) => new RenderTreeFrame(Sequence, elementName: ElementName, elementSubtreeLength: elementSubtreeLength); @@ -199,5 +221,8 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree internal RenderTreeFrame WithAttributeEventHandlerId(int eventHandlerId) => new RenderTreeFrame(Sequence, AttributeName, AttributeValue, eventHandlerId); + + internal RenderTreeFrame WithRegionSubtreeLength(int regionSubtreeLength) + => new RenderTreeFrame(Sequence, regionSubtreeLength: regionSubtreeLength); } } diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrameType.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrameType.cs index 9a899c84ec..ffc936958f 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrameType.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrameType.cs @@ -27,5 +27,13 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree /// Represents a child component. /// Component = 4, + + /// + /// Defines the boundary around range of sibling frames that should be treated as an + /// unsplittable group for the purposes of diffing. This is typically used when appending + /// a tree fragment generated by external code, because the sequence numbers in that tree + /// fragment are not comparable to sequence numbers outside it. + /// + Region = 5, } } diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs index 84fdcc0c37..68c7bc5d6a 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs @@ -245,6 +245,20 @@ namespace Microsoft.AspNetCore.Blazor.Test }); } + [Fact] + public void CannotAddAttributeToRegion() + { + // Arrange + var builder = new RenderTreeBuilder(new TestRenderer()); + + // Act/Assert + Assert.Throws(() => + { + builder.OpenRegion(0); + builder.AddAttribute(1, "name", "value"); + }); + } + [Fact] public void CanAddChildComponents() { @@ -272,6 +286,34 @@ namespace Microsoft.AspNetCore.Blazor.Test frame => AssertFrame.Attribute(frame, "child2attribute", "C")); } + [Fact] + public void CanAddRegions() + { + // Arrange + var builder = new RenderTreeBuilder(new TestRenderer()); + + // Act + builder.OpenElement(10, "parent"); // 0: + builder.OpenRegion(11); // 1: [region + builder.AddText(3, "Hello"); // 2: Hello + builder.OpenRegion(4); // 3: [region + builder.OpenElement(3, "another"); // 4: + builder.CloseElement(); // + builder.CloseRegion(); // ] + builder.AddText(6, "Goodbye"); // 5: Goodbye + builder.CloseRegion(); // ] + builder.CloseElement(); // + + // Assert + Assert.Collection(builder.GetFrames(), + frame => AssertFrame.Element(frame, "parent", 6, 10), + frame => AssertFrame.Region(frame, 5, 11), + frame => AssertFrame.Text(frame, "Hello", 3), + frame => AssertFrame.Region(frame, 2, 4), + frame => AssertFrame.Element(frame, "another", 1, 3), + frame => AssertFrame.Text(frame, "Goodbye", 6)); + } + [Fact] public void CanClear() { diff --git a/test/shared/AssertFrame.cs b/test/shared/AssertFrame.cs index faa9b1d497..64d25559b4 100644 --- a/test/shared/AssertFrame.cs +++ b/test/shared/AssertFrame.cs @@ -67,6 +67,13 @@ namespace Microsoft.AspNetCore.Blazor.Test.Shared Assert.Equal(componentId, frame.ComponentId); } + public static void Region(RenderTreeFrame frame, int subtreeLength, int? sequence = null) + { + Assert.Equal(RenderTreeFrameType.Region, frame.FrameType); + Assert.Equal(subtreeLength, frame.RegionSubtreeLength); + AssertFrame.Sequence(frame, sequence); + } + public static void Whitespace(RenderTreeFrame frame, int? sequence = null) { Assert.Equal(RenderTreeFrameType.Text, frame.FrameType);