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