From 080e6395cb2769f964cdb121fc634517f8e55130 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 29 Jan 2018 20:16:42 +0000 Subject: [PATCH] Move to batched rendering --- .../src/Rendering/BrowserRenderer.ts | 14 +- .../Rendering/BrowserRenderer.cs | 29 ++- .../BrowserRendererEventDispatcher.cs | 11 - .../RenderTree/ArrayBuilder.cs | 10 + .../RenderTree/RenderTreeDiff.cs | 7 + .../RenderTree/RenderTreeDiffComputer.cs | 41 +++- .../Rendering/ComponentState.cs | 18 +- .../Rendering/RenderBatch.cs | 23 ++ .../Rendering/RenderBatchBuilder.cs | 29 +++ .../Rendering/Renderer.cs | 49 +++- .../RazorCompilerTest.cs | 2 +- .../RenderTreeBuilderTest.cs | 2 +- .../RenderTreeDiffComputerTest.cs | 226 +++++++----------- .../RendererTest.cs | 19 +- 14 files changed, 259 insertions(+), 221 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatch.cs create mode 100644 src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts index b542fbda43..0d51b2b1d7 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts @@ -147,20 +147,10 @@ export class BrowserRenderer { const containerElement = document.createElement('blazor-component'); insertNodeIntoDOM(containerElement, parent, childIndex); + // All we have to do is associate the child component ID with its location. We don't actually + // do any rendering here, because the diff for the child will appear later in the render batch. const childComponentId = renderTreeNode.componentId(node); this.attachComponentToElement(childComponentId, containerElement); - - if (!renderComponentMethod) { - renderComponentMethod = platform.findMethod( - 'Microsoft.AspNetCore.Blazor.Browser', 'Microsoft.AspNetCore.Blazor.Browser.Rendering', 'BrowserRendererEventDispatcher', 'RenderChildComponent' - ); - } - - // TODO: Consider caching the .NET string instance for this.browserRendererId - platform.callMethod(renderComponentMethod, null, [ - platform.toDotNetString(this.browserRendererId.toString()), - platform.toDotNetString(childComponentId.toString()) - ]); } insertText(parent: Element, childIndex: number, textNode: RenderTreeNodePointer) { diff --git a/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs b/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs index 954f987886..96b188f653 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs +++ b/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs @@ -66,20 +66,23 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering } /// - protected override void UpdateDisplay( - int componentId, - RenderTreeDiff renderTreeDiff) + protected override void UpdateDisplay(RenderBatch batch) { - RegisteredFunction.InvokeUnmarshalled( - "renderRenderTree", - new RenderComponentArgs - { - BrowserRendererId = _browserRendererId, - ComponentId = componentId, - RenderTreeEdits = renderTreeDiff.Edits.Array, - RenderTreeEditsLength = renderTreeDiff.Edits.Count, - RenderTree = renderTreeDiff.CurrentState.Array - }); + // TODO: Pass to JS in a single call + for (var i = 0; i < batch.UpdatedComponents.Count; i++) + { + ref var diff = ref batch.UpdatedComponents.Array[i]; + RegisteredFunction.InvokeUnmarshalled( + "renderRenderTree", + new RenderComponentArgs + { + BrowserRendererId = _browserRendererId, + ComponentId = diff.ComponentId, + RenderTreeEdits = diff.Edits.Array, + RenderTreeEditsLength = diff.Edits.Count, + RenderTree = diff.CurrentState.Array + }); + } } // Encapsulates the data we pass to the JS rendering function diff --git a/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRendererEventDispatcher.cs b/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRendererEventDispatcher.cs index 94f7c07009..20e93cd35c 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRendererEventDispatcher.cs +++ b/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRendererEventDispatcher.cs @@ -28,17 +28,6 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering eventArgs); } - // Again, the params are received as strings for the same reason as above and - // can be simplified once runtime support improves. - public static void RenderChildComponent( - string browserRendererId, - string componentId) - { - var browserRenderer = BrowserRendererRegistry.Find(int.Parse(browserRendererId)); - browserRenderer.RenderNewBatchInternal( - int.Parse(componentId)); - } - private static UIEventArgs ParseEventArgsJson(string eventArgsType, string eventArgsJson) { switch (eventArgsType) diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/ArrayBuilder.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/ArrayBuilder.cs index 238334f56a..4a54e5965c 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/ArrayBuilder.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/ArrayBuilder.cs @@ -56,6 +56,16 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree _items[_itemsInUse++] = item; } + /// + /// Sets the supplied value at the specified index. The index must be within + /// range for the array. + /// + /// The index. + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Overwrite(int index, in T value) + => _items[index] = value; + /// /// Removes the last item. /// diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiff.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiff.cs index 71c7fe957f..0c117937c5 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiff.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiff.cs @@ -11,6 +11,11 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree /// public readonly struct RenderTreeDiff { + /// + /// Gets the ID of the component. + /// + public int ComponentId { get; } + /// /// Gets the changes to the render tree since a previous state. /// @@ -23,9 +28,11 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree public ArrayRange CurrentState { get; } internal RenderTreeDiff( + int componentId, ArrayRange entries, ArrayRange referenceTree) { + ComponentId = componentId; Edits = entries; CurrentState = referenceTree; } diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffComputer.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffComputer.cs index 1f075cb050..f91b52d2e0 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffComputer.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffComputer.cs @@ -24,18 +24,24 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree /// component instances onto retained Component nodes. It's particularly convenient to do that /// here because we have the right information and are already walking the trees to do the diff. /// - public RenderTreeDiff ApplyNewRenderTreeVersion( + public void ApplyNewRenderTreeVersion( + RenderBatchBuilder batchBuilder, + int componentId, ArrayRange oldTree, ArrayRange newTree) { _entries.Clear(); var siblingIndex = 0; - AppendDiffEntriesForRange(oldTree.Array, 0, oldTree.Count, newTree.Array, 0, newTree.Count, ref siblingIndex); - return new RenderTreeDiff(_entries.ToRange(), newTree); + var slotId = batchBuilder.ReserveUpdatedComponentSlotId(); + AppendDiffEntriesForRange(batchBuilder, oldTree.Array, 0, oldTree.Count, newTree.Array, 0, newTree.Count, ref siblingIndex); + batchBuilder.SetUpdatedComponent( + slotId, + new RenderTreeDiff(componentId, _entries.ToRange(), newTree)); } private void AppendDiffEntriesForRange( + RenderBatchBuilder batchBuilder, RenderTreeNode[] oldTree, int oldStartIndex, int oldEndIndexExcl, RenderTreeNode[] newTree, int newStartIndex, int newEndIndexExcl, ref int siblingIndex) @@ -51,7 +57,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree if (oldSeq == newSeq) { - AppendDiffEntriesForNodesWithSameSequence(oldTree, oldStartIndex, newTree, newStartIndex, ref siblingIndex); + AppendDiffEntriesForNodesWithSameSequence(batchBuilder, oldTree, oldStartIndex, newTree, newStartIndex, ref siblingIndex); oldStartIndex = NextSiblingIndex(oldTree[oldStartIndex], oldStartIndex); newStartIndex = NextSiblingIndex(newTree[newStartIndex], newStartIndex); hasMoreOld = oldEndIndexExcl > oldStartIndex; @@ -137,7 +143,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree { if (newNodeType == RenderTreeNodeType.Element || newNodeType == RenderTreeNodeType.Component) { - InstantiateChildComponents(newTree, newStartIndex); + InstantiateChildComponents(batchBuilder, newTree, newStartIndex); } Append(RenderTreeEdit.PrependNode(siblingIndex, newStartIndex)); @@ -169,6 +175,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree } private void UpdateRetainedChildComponent( + RenderBatchBuilder batchBuilder, RenderTreeNode[] oldTree, int oldComponentIndex, RenderTreeNode[] newTree, int newComponentIndex) { @@ -260,7 +267,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree { // TODO: Instead, call some OnPropertiesUpdated method on IComponent, // whose default implementation causes itself to be rerendered - _renderer.RenderInExistingBatch(componentId); + _renderer.RenderInExistingBatch(batchBuilder, componentId); } } @@ -308,6 +315,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree } private void AppendDiffEntriesForNodesWithSameSequence( + RenderBatchBuilder batchBuilder, RenderTreeNode[] oldTree, int oldNodeIndex, RenderTreeNode[] newTree, int newNodeIndex, ref int siblingIndex) @@ -342,6 +350,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree // Diff the attributes AppendDiffEntriesForRange( + batchBuilder, oldTree, oldNodeIndex + 1, oldNodeAttributesEndIndexExcl, newTree, newNodeIndex + 1, newNodeAttributesEndIndexExcl, ref siblingIndex); @@ -357,6 +366,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree Append(RenderTreeEdit.StepIn(siblingIndex)); var childSiblingIndex = 0; AppendDiffEntriesForRange( + batchBuilder, oldTree, oldNodeAttributesEndIndexExcl, oldNodeChildrenEndIndexExcl, newTree, newNodeAttributesEndIndexExcl, newNodeChildrenEndIndexExcl, ref childSiblingIndex); @@ -385,6 +395,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree if (oldComponentType == newComponentType) { UpdateRetainedChildComponent( + batchBuilder, oldTree, oldNodeIndex, newTree, newNodeIndex); @@ -460,30 +471,34 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree _entries.Append(entry); } - private void InstantiateChildComponents(RenderTreeNode[] nodes, int elementOrComponentIndex) + private void InstantiateChildComponents(RenderBatchBuilder batchBuilder, RenderTreeNode[] nodes, int elementOrComponentIndex) { var endIndex = nodes[elementOrComponentIndex].ElementDescendantsEndIndex; for (var i = elementOrComponentIndex; i <= endIndex; i++) { - if (nodes[i].NodeType == RenderTreeNodeType.Component) + ref var node = ref nodes[i]; + if (node.NodeType == RenderTreeNodeType.Component) { - if (nodes[i].Component != null) + if (node.Component != null) { throw new InvalidOperationException($"Child component already exists during {nameof(InstantiateChildComponents)}"); } _renderer.InstantiateChildComponent(nodes, i); - var childComponentInstance = nodes[i].Component; + var childComponentInstance = node.Component; // All descendants of a component are its properties - var componentDescendantsEndIndex = nodes[i].ElementDescendantsEndIndex; + var componentDescendantsEndIndex = node.ElementDescendantsEndIndex; for (var attributeNodeIndex = i + 1; attributeNodeIndex <= componentDescendantsEndIndex; attributeNodeIndex++) { + ref var attributeNode = ref nodes[attributeNodeIndex]; SetChildComponentProperty( childComponentInstance, - nodes[attributeNodeIndex].AttributeName, - nodes[attributeNodeIndex].AttributeValue); + attributeNode.AttributeName, + attributeNode.AttributeValue); } + + _renderer.RenderInExistingBatch(batchBuilder, node.ComponentId); } } } diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs index d565f4aebc..020df06189 100644 --- a/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs @@ -38,25 +38,21 @@ namespace Microsoft.AspNetCore.Blazor.Rendering } /// - /// Regenerates the and notifies the - /// to update the visible UI state. + /// Regenerates the and adds the changes to the + /// . /// - public void Render() + public void Render(RenderBatchBuilder batchBuilder) { // Swap the old and new tree builders (_renderTreeBuilderCurrent, _renderTreeBuilderPrevious) = (_renderTreeBuilderPrevious, _renderTreeBuilderCurrent); _renderTreeBuilderCurrent.Clear(); _component.BuildRenderTree(_renderTreeBuilderCurrent); - var diff = _diffComputer.ApplyNewRenderTreeVersion( + _diffComputer.ApplyNewRenderTreeVersion( + batchBuilder, + _componentId, _renderTreeBuilderPrevious.GetNodes(), _renderTreeBuilderCurrent.GetNodes()); - - // TODO: Instead of triggering the UpdateDisplay here, collect all the (componentId, diff) - // pairs from the whole render cycle, including descendant components, then send them - // in bulk to a single UpdateDisplay. Need to ensure that if the same component gets - // triggered multiple times that the diff reflects the entire chain. - _renderer.UpdateDisplay(_componentId, diff); } /// @@ -82,7 +78,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering // After any event, we synchronously re-render. Most of the time this means that // developers don't need to call Render() on their components explicitly. - Render(); + _renderer.RenderNewBatch(_componentId); } } } diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatch.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatch.cs new file mode 100644 index 0000000000..389a38a7e6 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatch.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Blazor.RenderTree; + +namespace Microsoft.AspNetCore.Blazor.Rendering +{ + /// + /// Describes a set of UI changes. + /// + public readonly struct RenderBatch + { + /// + /// Gets the changes to components that were added or updated. + /// + public ArrayRange UpdatedComponents { get; } + + internal RenderBatch(ArrayRange updatedComponents) + { + UpdatedComponents = updatedComponents; + } + } +} diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs new file mode 100644 index 0000000000..259a0c2074 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Blazor.RenderTree; + +namespace Microsoft.AspNetCore.Blazor.Rendering +{ + internal class RenderBatchBuilder + { + private ArrayBuilder _updatedComponentDiffs = new ArrayBuilder(); + + public int ReserveUpdatedComponentSlotId() + { + int id = _updatedComponentDiffs.Count; + _updatedComponentDiffs.Append(default); + return id; + } + + public void SetUpdatedComponent(int updatedComponentSlotId, RenderTreeDiff diff) + => _updatedComponentDiffs.Overwrite(updatedComponentSlotId, diff); + + public void Clear() + => _updatedComponentDiffs.Clear(); + + public RenderBatch ToBatch() + => new RenderBatch( + _updatedComponentDiffs.ToRange()); + } +} diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs index 8c5321e96b..af34be7513 100644 --- a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Blazor.Components; using Microsoft.AspNetCore.Blazor.RenderTree; using System; using System.Runtime.CompilerServices; +using System.Threading; namespace Microsoft.AspNetCore.Blazor.Rendering { @@ -24,6 +25,11 @@ namespace Microsoft.AspNetCore.Blazor.Rendering = new ConditionalWeakTable(); private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it + // Because rendering is currently synchronous and single-threaded, we can keep re-using the + // same RenderBatchBuilder instance to avoid reallocating + private readonly RenderBatchBuilder _sharedRenderBatchBuilder = new RenderBatchBuilder(); + private int _renderBatchLock = 0; + /// /// Associates the with the , assigning /// an identifier that is unique within the scope of the . @@ -43,13 +49,10 @@ namespace Microsoft.AspNetCore.Blazor.Rendering } /// - /// Updates the visible UI to display the supplied - /// at the location corresponding to the . + /// Updates the visible UI. /// - /// The identifier for the updated . - /// The changes to the render tree since the component was last rendered. - internal protected abstract void UpdateDisplay( - int componentId, RenderTreeDiff renderTreeDiff); + /// The changes to the UI since the previous call. + internal protected abstract void UpdateDisplay(RenderBatch renderBatch); /// /// Updates the rendered state of the specified . @@ -57,12 +60,40 @@ namespace Microsoft.AspNetCore.Blazor.Rendering /// The identifier of the to render. protected internal void RenderNewBatch(int componentId) { - RenderInExistingBatch(componentId); + // It's very important that components' rendering logic has no side-effects, and in particular + // components must *not* trigger Render from inside their render logic, otherwise you could + // easily get hard-to-debug infinite loops. + // Since rendering is currently synchronous and single-threaded, we can enforce the above by + // checking here that no other rendering process is already underway. This also means we only + // need a single _renderBatchBuilder instance that can be reused throughout the lifetime of + // the Renderer instance, which also means we're not allocating on a typical render cycle. + // In the future, if rendering becomes async, we'll need a more sophisticated system of + // capturing successive diffs from each component and probably serializing them for the + // interop calls instead of using shared memory. + + // Note that Monitor.TryEnter is not yet supported in Mono WASM, so using the following instead + var renderAlreadyRunning = Interlocked.CompareExchange(ref _renderBatchLock, 1, 0) == 1; + if (renderAlreadyRunning) + { + throw new InvalidOperationException("Cannot render while a render is already in progress. " + + "Render logic must not have side-effects such as manually triggering other rendering."); + } + + try + { + RenderInExistingBatch(_sharedRenderBatchBuilder, componentId); + UpdateDisplay(_sharedRenderBatchBuilder.ToBatch()); + } + finally + { + _sharedRenderBatchBuilder.Clear(); + Interlocked.Exchange(ref _renderBatchLock, 0); + } } - internal void RenderInExistingBatch(int componentId) + internal void RenderInExistingBatch(RenderBatchBuilder batchBuilder, int componentId) { - GetRequiredComponentState(componentId).Render(); + GetRequiredComponentState(componentId).Render(batchBuilder); } /// diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs index 1c4400d9a9..823997fb94 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs @@ -459,7 +459,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test private class TestRenderer : Renderer { - protected override void UpdateDisplay(int componentId, RenderTreeDiff renderTreeDiff) + protected override void UpdateDisplay(RenderBatch renderBatch) => throw new NotImplementedException(); } diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs index 170d2e5a92..80f12bbdbc 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs @@ -297,7 +297,7 @@ namespace Microsoft.AspNetCore.Blazor.Test private class TestRenderer : Renderer { - protected internal override void UpdateDisplay(int componentId, RenderTreeDiff renderTreeDiff) + protected internal override void UpdateDisplay(RenderBatch renderBatch) => throw new NotImplementedException(); } } diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffComputerTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffComputerTest.cs index 9c44beb9ff..e8afb908b5 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffComputerTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffComputerTest.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Blazor.Components; using Microsoft.AspNetCore.Blazor.Rendering; using Microsoft.AspNetCore.Blazor.RenderTree; +using Microsoft.AspNetCore.Blazor.Test.Shared; using System; using System.Collections.Generic; using System.Linq; @@ -13,20 +14,29 @@ namespace Microsoft.AspNetCore.Blazor.Test { public class RenderTreeDiffComputerTest { + private readonly Renderer renderer; + private readonly RenderTreeBuilder oldTree; + private readonly RenderTreeBuilder newTree; + private RenderTreeDiffComputer diff; + + public RenderTreeDiffComputerTest() + { + renderer = new FakeRenderer(); + oldTree = new RenderTreeBuilder(renderer); + newTree = new RenderTreeBuilder(renderer); + diff = new RenderTreeDiffComputer(renderer); + } + [Theory] [MemberData(nameof(RecognizesEquivalentNodesAsSameCases))] public void RecognizesEquivalentNodesAsSame(Action appendAction) { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); appendAction(oldTree); appendAction(newTree); // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var result = GetSingleUpdatedComponent(); // Assert Assert.Empty(result.Edits); @@ -50,10 +60,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void RecognizesNewItemsBeingInserted() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); oldTree.AddText(0, "text0"); oldTree.AddText(2, "text2"); newTree.AddText(0, "text0"); @@ -61,7 +67,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.AddText(2, "text2"); // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var result = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -76,10 +82,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void RecognizesOldItemsBeingRemoved() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); oldTree.AddText(0, "text0"); oldTree.AddText(1, "text1"); oldTree.AddText(2, "text2"); @@ -87,7 +89,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.AddText(2, "text2"); // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var result = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -98,10 +100,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void RecognizesTrailingSequenceWithinLoopBlockBeingRemoved() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); oldTree.AddText(0, "x"); // Loop start oldTree.AddText(1, "x"); // Will be removed oldTree.AddText(2, "x"); // Will be removed @@ -110,7 +108,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.AddText(0, "x"); // Loop start // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var result = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -122,10 +120,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void RecognizesTrailingSequenceWithinLoopBlockBeingAppended() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); oldTree.AddText(0, "x"); // Loop start oldTree.AddText(0, "x"); // Loop start newTree.AddText(0, "x"); // Loop start @@ -134,7 +128,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.AddText(0, "x"); // Loop start // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var result = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -154,10 +148,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void RecognizesTrailingLoopBlockBeingRemoved() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); oldTree.AddText(0, "x"); oldTree.AddText(1, "x"); oldTree.AddText(0, "x"); // Will be removed @@ -166,7 +156,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.AddText(1, "x"); // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var result = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -178,10 +168,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void RecognizesTrailingLoopBlockBeingAdded() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); oldTree.AddText(0, "x"); oldTree.AddText(1, "x"); newTree.AddText(0, "x"); @@ -190,7 +176,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.AddText(1, "x"); // Will be added // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var result = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -210,10 +196,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void RecognizesLeadingLoopBlockItemsBeingAdded() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); oldTree.AddText(2, "x"); oldTree.AddText(2, "x"); // Note that the '0' and '1' items are not present on this iteration newTree.AddText(2, "x"); @@ -222,7 +204,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.AddText(2, "x"); // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var result = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -242,10 +224,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void RecognizesLeadingLoopBlockItemsBeingRemoved() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); oldTree.AddText(2, "x"); oldTree.AddText(0, "x"); oldTree.AddText(1, "x"); @@ -254,7 +232,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.AddText(2, "x"); // Note that the '0' and '1' items are not present on this iteration // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var result = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -266,15 +244,11 @@ namespace Microsoft.AspNetCore.Blazor.Test public void HandlesAdjacentItemsBeingRemovedAndInsertedAtOnce() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); oldTree.AddText(0, "text"); newTree.AddText(1, "text"); // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var result = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -286,17 +260,13 @@ namespace Microsoft.AspNetCore.Blazor.Test public void RecognizesTextUpdates() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); oldTree.AddText(123, "old text 1"); oldTree.AddText(182, "old text 2"); newTree.AddText(123, "new text 1"); newTree.AddText(182, "new text 2"); // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var result = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -320,17 +290,13 @@ namespace Microsoft.AspNetCore.Blazor.Test // decide just to throw in this scenario, since it's unnecessary to support it. // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); oldTree.OpenElement(123, "old element"); oldTree.CloseElement(); newTree.OpenElement(123, "new element"); newTree.CloseElement(); // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var result = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -346,15 +312,11 @@ namespace Microsoft.AspNetCore.Blazor.Test public void RecognizesComponentTypeChangesAtSameSequenceNumber() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); oldTree.OpenComponentElement(123); newTree.OpenComponentElement(123); // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var result = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -370,10 +332,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void RecognizesAttributesAdded() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); oldTree.OpenElement(0, "My element"); oldTree.AddAttribute(1, "existing", "existing value"); oldTree.CloseElement(); @@ -383,7 +341,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.CloseElement(); // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var result = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -398,10 +356,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void RecognizesAttributesRemoved() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); oldTree.OpenElement(0, "My element"); oldTree.AddAttribute(1, "will be removed", "will be removed value"); oldTree.AddAttribute(2, "will survive", "surviving value"); @@ -411,7 +365,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.CloseElement(); // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var result = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -426,10 +380,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void RecognizesAttributeStringValuesChanged() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); oldTree.OpenElement(0, "My element"); oldTree.AddAttribute(1, "will remain", "will remain value"); oldTree.AddAttribute(2, "will change", "will change value"); @@ -440,7 +390,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.CloseElement(); // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var result = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -455,10 +405,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void RecognizesAttributeEventHandlerValuesChanged() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); UIEventHandler retainedHandler = _ => { }; UIEventHandler removedHandler = _ => { }; UIEventHandler addedHandler = _ => { }; @@ -472,7 +418,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.CloseElement(); // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var result = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -487,10 +433,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void RecognizesAttributeNamesChangedAtSameSourceSequence() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); oldTree.OpenElement(0, "My element"); oldTree.AddAttribute(1, "oldname", "same value"); oldTree.CloseElement(); @@ -499,7 +441,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.CloseElement(); // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var result = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -519,10 +461,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void DiffsElementsHierarchically() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); oldTree.AddText(09, "unrelated"); oldTree.OpenElement(10, "root"); oldTree.OpenElement(11, "child"); @@ -542,7 +480,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.CloseElement(); // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var result = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -563,10 +501,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void SkipsUnmodifiedSubtrees() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); oldTree.OpenElement(10, "root"); oldTree.AddText(11, "Text that will change"); oldTree.OpenElement(12, "Subtree that will not change"); @@ -586,7 +520,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.CloseElement(); // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var result = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -603,10 +537,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void SkipsUnmodifiedTrailingSiblings() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); oldTree.AddText(10, "text1"); oldTree.AddText(11, "text2"); oldTree.AddText(12, "text3"); @@ -617,7 +547,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.AddText(13, "text4"); // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var result = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -632,10 +562,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void InstantiatesChildComponentsForInsertedNodes() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); oldTree.AddText(10, "text1"); // 0: text1 oldTree.OpenElement(11, "container"); // 1: oldTree.CloseElement(); // @@ -648,10 +574,14 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.CloseElement(); // // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var renderBatch = GetRenderedBatch(); // Assert - Assert.Collection(result.Edits, + Assert.Equal(3, renderBatch.UpdatedComponents.Count); + + // First component is the root one + var firstComponentDiff = renderBatch.UpdatedComponents.Array[0]; + Assert.Collection(firstComponentDiff.Edits, entry => AssertEdit(entry, RenderTreeEditType.StepIn, 1), entry => { @@ -672,16 +602,26 @@ namespace Microsoft.AspNetCore.Blazor.Test Assert.IsType(newTreeNode.Component); }, entry => AssertEdit(entry, RenderTreeEditType.StepOut, 0)); + + // Second in batch is the first child component + var secondComponentDiff = renderBatch.UpdatedComponents.Array[1]; + Assert.Equal(0, secondComponentDiff.ComponentId); + Assert.Empty(secondComponentDiff.Edits); // Because FakeComponent produces no nodes + Assert.Empty(secondComponentDiff.CurrentState); // Because FakeComponent produces no nodes + + // Third in batch is the second child component + var thirdComponentDiff = renderBatch.UpdatedComponents.Array[2]; + Assert.Equal(1, thirdComponentDiff.ComponentId); + Assert.Collection(thirdComponentDiff.Edits, + entry => AssertEdit(entry, RenderTreeEditType.PrependNode, 0)); + Assert.Collection(thirdComponentDiff.CurrentState, + node => AssertNode.Text(node, $"Hello from {nameof(FakeComponent2)}")); } [Fact] public void SetsKnownPropertiesOnChildComponents() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); var testObject = new object(); newTree.OpenComponentElement(0); newTree.AddAttribute(1, nameof(FakeComponent.IntProperty), 123); @@ -690,11 +630,14 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.CloseElement(); // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var renderBatch = GetRenderedBatch(); var componentInstance = newTree.GetNodes().First().Component as FakeComponent; // Assert - AssertEdit(result.Edits.Single(), RenderTreeEditType.PrependNode, 0); + Assert.Equal(2, renderBatch.UpdatedComponents.Count); + + var rootComponentDiff = renderBatch.UpdatedComponents.Array[0]; + AssertEdit(rootComponentDiff.Edits.Single(), RenderTreeEditType.PrependNode, 0); Assert.NotNull(componentInstance); Assert.Equal(123, componentInstance.IntProperty); Assert.Equal("some string", componentInstance.StringProperty); @@ -705,10 +648,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void ThrowsIfAssigningUnknownPropertiesToChildComponents() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); var testObject = new object(); newTree.OpenComponentElement(0); newTree.AddAttribute(1, "SomeUnknownProperty", 123); @@ -717,7 +656,7 @@ namespace Microsoft.AspNetCore.Blazor.Test // Act/Assert var ex = Assert.Throws(() => { - diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + diff.ApplyNewRenderTreeVersion(new RenderBatchBuilder(), 0, oldTree.GetNodes(), newTree.GetNodes()); }); Assert.Equal($"Component of type '{typeof(FakeComponent).FullName}' does not have a property matching the name 'SomeUnknownProperty'.", ex.Message); } @@ -726,10 +665,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void ThrowsIfAssigningReadOnlyPropertiesToChildComponents() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); var testObject = new object(); newTree.OpenComponentElement(0); newTree.AddAttribute(1, nameof(FakeComponent.ReadonlyProperty), 123); @@ -738,7 +673,7 @@ namespace Microsoft.AspNetCore.Blazor.Test // Act/Assert var ex = Assert.Throws(() => { - diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + diff.ApplyNewRenderTreeVersion(new RenderBatchBuilder(), 0, oldTree.GetNodes(), newTree.GetNodes()); }); Assert.StartsWith($"Unable to set property '{nameof(FakeComponent.ReadonlyProperty)}' on " + $"component of type '{typeof(FakeComponent).FullName}'.", ex.Message); @@ -748,10 +683,6 @@ namespace Microsoft.AspNetCore.Blazor.Test public void RetainsChildComponentsForExistingNodes() { // Arrange - var renderer = new FakeRenderer(); - var oldTree = new RenderTreeBuilder(renderer); - var newTree = new RenderTreeBuilder(renderer); - var diff = new RenderTreeDiffComputer(renderer); oldTree.AddText(10, "text1"); // 0: text1 oldTree.OpenElement(11, "container"); // 1: oldTree.OpenComponentElement(12); // 2: @@ -767,12 +698,12 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.CloseElement(); // newTree.CloseElement(); // (12); oldTree.AddAttribute(13, nameof(FakeComponent.StringProperty), "String will change"); @@ -802,23 +729,38 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.AddAttribute(14, nameof(FakeComponent.ObjectProperty), objectWillNotChange); newTree.CloseElement(); - diff.ApplyNewRenderTreeVersion(new RenderTreeBuilder(renderer).GetNodes(), oldTree.GetNodes()); + diff.ApplyNewRenderTreeVersion(new RenderBatchBuilder(), 0, new RenderTreeBuilder(renderer).GetNodes(), oldTree.GetNodes()); var originalComponentInstance = (FakeComponent)oldTree.GetNodes().Array[0].Component; originalComponentInstance.ObjectProperty = null; // So we can see it doesn't get reassigned // Act - var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); + var renderBatch = GetRenderedBatch(); var newComponentInstance = (FakeComponent)oldTree.GetNodes().Array[0].Component; // Assert + Assert.Equal(2, renderBatch.UpdatedComponents.Count); Assert.Same(originalComponentInstance, newComponentInstance); Assert.Equal("String did change", newComponentInstance.StringProperty); Assert.Null(newComponentInstance.ObjectProperty); // To observe that the property wasn't even written, we nulled it out on the original } + private RenderTreeDiff GetSingleUpdatedComponent() + { + var diffsInBatch = GetRenderedBatch().UpdatedComponents; + Assert.Equal(1, diffsInBatch.Count); + return diffsInBatch.Array[0]; + } + + private RenderBatch GetRenderedBatch() + { + var batchBuilder = new RenderBatchBuilder(); + diff.ApplyNewRenderTreeVersion(batchBuilder, 0, oldTree.GetNodes(), newTree.GetNodes()); + return batchBuilder.ToBatch(); + } + private class FakeRenderer : Renderer { - internal protected override void UpdateDisplay(int componentId, RenderTreeDiff renderTreeDiff) + internal protected override void UpdateDisplay(RenderBatch renderBatch) { } } @@ -839,7 +781,9 @@ namespace Microsoft.AspNetCore.Blazor.Test private class FakeComponent2 : IComponent { public void BuildRenderTree(RenderTreeBuilder builder) - => throw new NotImplementedException(); + { + builder.AddText(100, $"Hello from {nameof(FakeComponent2)}"); + } } private static void AssertEdit( diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs index 00159e44dd..e8c5b956e6 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs @@ -45,6 +45,7 @@ namespace Microsoft.AspNetCore.Blazor.Test { builder.AddText(0, "Hello"); builder.OpenComponentElement(1); + builder.AddAttribute(2, nameof(MessageComponent.Message), "Nested component output"); builder.CloseElement(); }); @@ -57,13 +58,8 @@ namespace Microsoft.AspNetCore.Blazor.Test // The nested component exists Assert.IsType(componentNode.Component); - ((MessageComponent)(componentNode.Component)).Message = "Nested component output"; - // It isn't rendered until the consumer asks for it to be - Assert.False(renderer.RenderTreesByComponentId.ContainsKey(nestedComponentId)); - - // It can be rendered - renderer.RenderNewBatch(nestedComponentId); + // The nested component was rendered as part of the batch Assert.Collection(renderer.RenderTreesByComponentId[nestedComponentId], node => AssertNode.Text(node, "Nested component output")); } @@ -418,7 +414,7 @@ namespace Microsoft.AspNetCore.Blazor.Test public new void RenderNewBatch(int componentId) => base.RenderNewBatch(componentId); - protected internal override void UpdateDisplay(int componentId, RenderTreeDiff renderTreeDiff) + protected internal override void UpdateDisplay(RenderBatch renderBatch) { } } @@ -437,9 +433,14 @@ namespace Microsoft.AspNetCore.Blazor.Test public new void DispatchEvent(int componentId, int renderTreeIndex, UIEventArgs args) => base.DispatchEvent(componentId, renderTreeIndex, args); - protected internal override void UpdateDisplay(int componentId, RenderTreeDiff renderTreeDiff) + protected internal override void UpdateDisplay(RenderBatch renderBatch) { - RenderTreesByComponentId[componentId] = renderTreeDiff.CurrentState; + // TODO: Capture the batches and rephrase assertions in terms of those + for (var i = 0; i < renderBatch.UpdatedComponents.Count; i++) + { + ref var renderTreeDiff = ref renderBatch.UpdatedComponents.Array[i]; + RenderTreesByComponentId[renderTreeDiff.ComponentId] = renderTreeDiff.CurrentState; + } } }