Move to batched rendering

This commit is contained in:
Steve Sanderson 2018-01-29 20:16:42 +00:00
parent 27d0ce4da9
commit 080e6395cb
14 changed files with 259 additions and 221 deletions

View File

@ -147,20 +147,10 @@ export class BrowserRenderer {
const containerElement = document.createElement('blazor-component'); const containerElement = document.createElement('blazor-component');
insertNodeIntoDOM(containerElement, parent, childIndex); 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); const childComponentId = renderTreeNode.componentId(node);
this.attachComponentToElement(childComponentId, containerElement); 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) { insertText(parent: Element, childIndex: number, textNode: RenderTreeNodePointer) {

View File

@ -66,20 +66,23 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void UpdateDisplay( protected override void UpdateDisplay(RenderBatch batch)
int componentId,
RenderTreeDiff renderTreeDiff)
{ {
RegisteredFunction.InvokeUnmarshalled<RenderComponentArgs, object>( // TODO: Pass to JS in a single call
"renderRenderTree", for (var i = 0; i < batch.UpdatedComponents.Count; i++)
new RenderComponentArgs {
{ ref var diff = ref batch.UpdatedComponents.Array[i];
BrowserRendererId = _browserRendererId, RegisteredFunction.InvokeUnmarshalled<RenderComponentArgs, object>(
ComponentId = componentId, "renderRenderTree",
RenderTreeEdits = renderTreeDiff.Edits.Array, new RenderComponentArgs
RenderTreeEditsLength = renderTreeDiff.Edits.Count, {
RenderTree = renderTreeDiff.CurrentState.Array 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 // Encapsulates the data we pass to the JS rendering function

View File

@ -28,17 +28,6 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering
eventArgs); 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) private static UIEventArgs ParseEventArgsJson(string eventArgsType, string eventArgsJson)
{ {
switch (eventArgsType) switch (eventArgsType)

View File

@ -56,6 +56,16 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
_items[_itemsInUse++] = item; _items[_itemsInUse++] = item;
} }
/// <summary>
/// Sets the supplied value at the specified index. The index must be within
/// range for the array.
/// </summary>
/// <param name="index">The index.</param>
/// <param name="value">The value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Overwrite(int index, in T value)
=> _items[index] = value;
/// <summary> /// <summary>
/// Removes the last item. /// Removes the last item.
/// </summary> /// </summary>

View File

@ -11,6 +11,11 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
/// </summary> /// </summary>
public readonly struct RenderTreeDiff public readonly struct RenderTreeDiff
{ {
/// <summary>
/// Gets the ID of the component.
/// </summary>
public int ComponentId { get; }
/// <summary> /// <summary>
/// Gets the changes to the render tree since a previous state. /// Gets the changes to the render tree since a previous state.
/// </summary> /// </summary>
@ -23,9 +28,11 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
public ArrayRange<RenderTreeNode> CurrentState { get; } public ArrayRange<RenderTreeNode> CurrentState { get; }
internal RenderTreeDiff( internal RenderTreeDiff(
int componentId,
ArrayRange<RenderTreeEdit> entries, ArrayRange<RenderTreeEdit> entries,
ArrayRange<RenderTreeNode> referenceTree) ArrayRange<RenderTreeNode> referenceTree)
{ {
ComponentId = componentId;
Edits = entries; Edits = entries;
CurrentState = referenceTree; CurrentState = referenceTree;
} }

View File

@ -24,18 +24,24 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
/// component instances onto retained Component nodes. It's particularly convenient to do that /// 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. /// here because we have the right information and are already walking the trees to do the diff.
/// </summary> /// </summary>
public RenderTreeDiff ApplyNewRenderTreeVersion( public void ApplyNewRenderTreeVersion(
RenderBatchBuilder batchBuilder,
int componentId,
ArrayRange<RenderTreeNode> oldTree, ArrayRange<RenderTreeNode> oldTree,
ArrayRange<RenderTreeNode> newTree) ArrayRange<RenderTreeNode> newTree)
{ {
_entries.Clear(); _entries.Clear();
var siblingIndex = 0; 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( private void AppendDiffEntriesForRange(
RenderBatchBuilder batchBuilder,
RenderTreeNode[] oldTree, int oldStartIndex, int oldEndIndexExcl, RenderTreeNode[] oldTree, int oldStartIndex, int oldEndIndexExcl,
RenderTreeNode[] newTree, int newStartIndex, int newEndIndexExcl, RenderTreeNode[] newTree, int newStartIndex, int newEndIndexExcl,
ref int siblingIndex) ref int siblingIndex)
@ -51,7 +57,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
if (oldSeq == newSeq) if (oldSeq == newSeq)
{ {
AppendDiffEntriesForNodesWithSameSequence(oldTree, oldStartIndex, newTree, newStartIndex, ref siblingIndex); AppendDiffEntriesForNodesWithSameSequence(batchBuilder, oldTree, oldStartIndex, newTree, newStartIndex, ref siblingIndex);
oldStartIndex = NextSiblingIndex(oldTree[oldStartIndex], oldStartIndex); oldStartIndex = NextSiblingIndex(oldTree[oldStartIndex], oldStartIndex);
newStartIndex = NextSiblingIndex(newTree[newStartIndex], newStartIndex); newStartIndex = NextSiblingIndex(newTree[newStartIndex], newStartIndex);
hasMoreOld = oldEndIndexExcl > oldStartIndex; hasMoreOld = oldEndIndexExcl > oldStartIndex;
@ -137,7 +143,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
{ {
if (newNodeType == RenderTreeNodeType.Element || newNodeType == RenderTreeNodeType.Component) if (newNodeType == RenderTreeNodeType.Element || newNodeType == RenderTreeNodeType.Component)
{ {
InstantiateChildComponents(newTree, newStartIndex); InstantiateChildComponents(batchBuilder, newTree, newStartIndex);
} }
Append(RenderTreeEdit.PrependNode(siblingIndex, newStartIndex)); Append(RenderTreeEdit.PrependNode(siblingIndex, newStartIndex));
@ -169,6 +175,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
} }
private void UpdateRetainedChildComponent( private void UpdateRetainedChildComponent(
RenderBatchBuilder batchBuilder,
RenderTreeNode[] oldTree, int oldComponentIndex, RenderTreeNode[] oldTree, int oldComponentIndex,
RenderTreeNode[] newTree, int newComponentIndex) RenderTreeNode[] newTree, int newComponentIndex)
{ {
@ -260,7 +267,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
{ {
// TODO: Instead, call some OnPropertiesUpdated method on IComponent, // TODO: Instead, call some OnPropertiesUpdated method on IComponent,
// whose default implementation causes itself to be rerendered // 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( private void AppendDiffEntriesForNodesWithSameSequence(
RenderBatchBuilder batchBuilder,
RenderTreeNode[] oldTree, int oldNodeIndex, RenderTreeNode[] oldTree, int oldNodeIndex,
RenderTreeNode[] newTree, int newNodeIndex, RenderTreeNode[] newTree, int newNodeIndex,
ref int siblingIndex) ref int siblingIndex)
@ -342,6 +350,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
// Diff the attributes // Diff the attributes
AppendDiffEntriesForRange( AppendDiffEntriesForRange(
batchBuilder,
oldTree, oldNodeIndex + 1, oldNodeAttributesEndIndexExcl, oldTree, oldNodeIndex + 1, oldNodeAttributesEndIndexExcl,
newTree, newNodeIndex + 1, newNodeAttributesEndIndexExcl, newTree, newNodeIndex + 1, newNodeAttributesEndIndexExcl,
ref siblingIndex); ref siblingIndex);
@ -357,6 +366,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
Append(RenderTreeEdit.StepIn(siblingIndex)); Append(RenderTreeEdit.StepIn(siblingIndex));
var childSiblingIndex = 0; var childSiblingIndex = 0;
AppendDiffEntriesForRange( AppendDiffEntriesForRange(
batchBuilder,
oldTree, oldNodeAttributesEndIndexExcl, oldNodeChildrenEndIndexExcl, oldTree, oldNodeAttributesEndIndexExcl, oldNodeChildrenEndIndexExcl,
newTree, newNodeAttributesEndIndexExcl, newNodeChildrenEndIndexExcl, newTree, newNodeAttributesEndIndexExcl, newNodeChildrenEndIndexExcl,
ref childSiblingIndex); ref childSiblingIndex);
@ -385,6 +395,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
if (oldComponentType == newComponentType) if (oldComponentType == newComponentType)
{ {
UpdateRetainedChildComponent( UpdateRetainedChildComponent(
batchBuilder,
oldTree, oldNodeIndex, oldTree, oldNodeIndex,
newTree, newNodeIndex); newTree, newNodeIndex);
@ -460,30 +471,34 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
_entries.Append(entry); _entries.Append(entry);
} }
private void InstantiateChildComponents(RenderTreeNode[] nodes, int elementOrComponentIndex) private void InstantiateChildComponents(RenderBatchBuilder batchBuilder, RenderTreeNode[] nodes, int elementOrComponentIndex)
{ {
var endIndex = nodes[elementOrComponentIndex].ElementDescendantsEndIndex; var endIndex = nodes[elementOrComponentIndex].ElementDescendantsEndIndex;
for (var i = elementOrComponentIndex; i <= endIndex; i++) 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)}"); throw new InvalidOperationException($"Child component already exists during {nameof(InstantiateChildComponents)}");
} }
_renderer.InstantiateChildComponent(nodes, i); _renderer.InstantiateChildComponent(nodes, i);
var childComponentInstance = nodes[i].Component; var childComponentInstance = node.Component;
// All descendants of a component are its properties // 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++) for (var attributeNodeIndex = i + 1; attributeNodeIndex <= componentDescendantsEndIndex; attributeNodeIndex++)
{ {
ref var attributeNode = ref nodes[attributeNodeIndex];
SetChildComponentProperty( SetChildComponentProperty(
childComponentInstance, childComponentInstance,
nodes[attributeNodeIndex].AttributeName, attributeNode.AttributeName,
nodes[attributeNodeIndex].AttributeValue); attributeNode.AttributeValue);
} }
_renderer.RenderInExistingBatch(batchBuilder, node.ComponentId);
} }
} }
} }

View File

@ -38,25 +38,21 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
} }
/// <summary> /// <summary>
/// Regenerates the <see cref="RenderTree"/> and notifies the <see cref="Renderer"/> /// Regenerates the <see cref="RenderTree"/> and adds the changes to the
/// to update the visible UI state. /// <paramref name="batchBuilder"/>.
/// </summary> /// </summary>
public void Render() public void Render(RenderBatchBuilder batchBuilder)
{ {
// Swap the old and new tree builders // Swap the old and new tree builders
(_renderTreeBuilderCurrent, _renderTreeBuilderPrevious) = (_renderTreeBuilderPrevious, _renderTreeBuilderCurrent); (_renderTreeBuilderCurrent, _renderTreeBuilderPrevious) = (_renderTreeBuilderPrevious, _renderTreeBuilderCurrent);
_renderTreeBuilderCurrent.Clear(); _renderTreeBuilderCurrent.Clear();
_component.BuildRenderTree(_renderTreeBuilderCurrent); _component.BuildRenderTree(_renderTreeBuilderCurrent);
var diff = _diffComputer.ApplyNewRenderTreeVersion( _diffComputer.ApplyNewRenderTreeVersion(
batchBuilder,
_componentId,
_renderTreeBuilderPrevious.GetNodes(), _renderTreeBuilderPrevious.GetNodes(),
_renderTreeBuilderCurrent.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);
} }
/// <summary> /// <summary>
@ -82,7 +78,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
// After any event, we synchronously re-render. Most of the time this means that // 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. // developers don't need to call Render() on their components explicitly.
Render(); _renderer.RenderNewBatch(_componentId);
} }
} }
} }

View File

@ -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
{
/// <summary>
/// Describes a set of UI changes.
/// </summary>
public readonly struct RenderBatch
{
/// <summary>
/// Gets the changes to components that were added or updated.
/// </summary>
public ArrayRange<RenderTreeDiff> UpdatedComponents { get; }
internal RenderBatch(ArrayRange<RenderTreeDiff> updatedComponents)
{
UpdatedComponents = updatedComponents;
}
}
}

View File

@ -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<RenderTreeDiff> _updatedComponentDiffs = new ArrayBuilder<RenderTreeDiff>();
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());
}
}

View File

@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.RenderTree; using Microsoft.AspNetCore.Blazor.RenderTree;
using System; using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading;
namespace Microsoft.AspNetCore.Blazor.Rendering namespace Microsoft.AspNetCore.Blazor.Rendering
{ {
@ -24,6 +25,11 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
= new ConditionalWeakTable<IComponent, ComponentState>(); = new ConditionalWeakTable<IComponent, ComponentState>();
private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it 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;
/// <summary> /// <summary>
/// Associates the <see cref="IComponent"/> with the <see cref="Renderer"/>, assigning /// Associates the <see cref="IComponent"/> with the <see cref="Renderer"/>, assigning
/// an identifier that is unique within the scope of the <see cref="Renderer"/>. /// an identifier that is unique within the scope of the <see cref="Renderer"/>.
@ -43,13 +49,10 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
} }
/// <summary> /// <summary>
/// Updates the visible UI to display the supplied <paramref name="renderTree"/> /// Updates the visible UI.
/// at the location corresponding to the <paramref name="componentId"/>.
/// </summary> /// </summary>
/// <param name="componentId">The identifier for the updated <see cref="IComponent"/>.</param> /// <param name="renderBatch">The changes to the UI since the previous call.</param>
/// <param name="renderTreeDiff">The changes to the render tree since the component was last rendered.</param> internal protected abstract void UpdateDisplay(RenderBatch renderBatch);
internal protected abstract void UpdateDisplay(
int componentId, RenderTreeDiff renderTreeDiff);
/// <summary> /// <summary>
/// Updates the rendered state of the specified <see cref="IComponent"/>. /// Updates the rendered state of the specified <see cref="IComponent"/>.
@ -57,12 +60,40 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
/// <param name="componentId">The identifier of the <see cref="IComponent"/> to render.</param> /// <param name="componentId">The identifier of the <see cref="IComponent"/> to render.</param>
protected internal void RenderNewBatch(int componentId) 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);
} }
/// <summary> /// <summary>

View File

@ -459,7 +459,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
private class TestRenderer : Renderer private class TestRenderer : Renderer
{ {
protected override void UpdateDisplay(int componentId, RenderTreeDiff renderTreeDiff) protected override void UpdateDisplay(RenderBatch renderBatch)
=> throw new NotImplementedException(); => throw new NotImplementedException();
} }

View File

@ -297,7 +297,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
private class TestRenderer : Renderer private class TestRenderer : Renderer
{ {
protected internal override void UpdateDisplay(int componentId, RenderTreeDiff renderTreeDiff) protected internal override void UpdateDisplay(RenderBatch renderBatch)
=> throw new NotImplementedException(); => throw new NotImplementedException();
} }
} }

View File

@ -4,6 +4,7 @@
using Microsoft.AspNetCore.Blazor.Components; using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.Rendering; using Microsoft.AspNetCore.Blazor.Rendering;
using Microsoft.AspNetCore.Blazor.RenderTree; using Microsoft.AspNetCore.Blazor.RenderTree;
using Microsoft.AspNetCore.Blazor.Test.Shared;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -13,20 +14,29 @@ namespace Microsoft.AspNetCore.Blazor.Test
{ {
public class RenderTreeDiffComputerTest 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] [Theory]
[MemberData(nameof(RecognizesEquivalentNodesAsSameCases))] [MemberData(nameof(RecognizesEquivalentNodesAsSameCases))]
public void RecognizesEquivalentNodesAsSame(Action<RenderTreeBuilder> appendAction) public void RecognizesEquivalentNodesAsSame(Action<RenderTreeBuilder> appendAction)
{ {
// Arrange // Arrange
var renderer = new FakeRenderer();
var oldTree = new RenderTreeBuilder(renderer);
var newTree = new RenderTreeBuilder(renderer);
var diff = new RenderTreeDiffComputer(renderer);
appendAction(oldTree); appendAction(oldTree);
appendAction(newTree); appendAction(newTree);
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var result = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Empty(result.Edits); Assert.Empty(result.Edits);
@ -50,10 +60,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void RecognizesNewItemsBeingInserted() public void RecognizesNewItemsBeingInserted()
{ {
// Arrange // 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(0, "text0");
oldTree.AddText(2, "text2"); oldTree.AddText(2, "text2");
newTree.AddText(0, "text0"); newTree.AddText(0, "text0");
@ -61,7 +67,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(2, "text2"); newTree.AddText(2, "text2");
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var result = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -76,10 +82,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void RecognizesOldItemsBeingRemoved() public void RecognizesOldItemsBeingRemoved()
{ {
// Arrange // 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(0, "text0");
oldTree.AddText(1, "text1"); oldTree.AddText(1, "text1");
oldTree.AddText(2, "text2"); oldTree.AddText(2, "text2");
@ -87,7 +89,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(2, "text2"); newTree.AddText(2, "text2");
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var result = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -98,10 +100,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void RecognizesTrailingSequenceWithinLoopBlockBeingRemoved() public void RecognizesTrailingSequenceWithinLoopBlockBeingRemoved()
{ {
// Arrange // 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
oldTree.AddText(1, "x"); // Will be removed oldTree.AddText(1, "x"); // Will be removed
oldTree.AddText(2, "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 newTree.AddText(0, "x"); // Loop start
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var result = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -122,10 +120,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void RecognizesTrailingSequenceWithinLoopBlockBeingAppended() public void RecognizesTrailingSequenceWithinLoopBlockBeingAppended()
{ {
// Arrange // 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
oldTree.AddText(0, "x"); // Loop start oldTree.AddText(0, "x"); // Loop start
newTree.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 newTree.AddText(0, "x"); // Loop start
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var result = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -154,10 +148,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void RecognizesTrailingLoopBlockBeingRemoved() public void RecognizesTrailingLoopBlockBeingRemoved()
{ {
// Arrange // 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(0, "x");
oldTree.AddText(1, "x"); oldTree.AddText(1, "x");
oldTree.AddText(0, "x"); // Will be removed oldTree.AddText(0, "x"); // Will be removed
@ -166,7 +156,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(1, "x"); newTree.AddText(1, "x");
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var result = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -178,10 +168,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void RecognizesTrailingLoopBlockBeingAdded() public void RecognizesTrailingLoopBlockBeingAdded()
{ {
// Arrange // 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(0, "x");
oldTree.AddText(1, "x"); oldTree.AddText(1, "x");
newTree.AddText(0, "x"); newTree.AddText(0, "x");
@ -190,7 +176,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(1, "x"); // Will be added newTree.AddText(1, "x"); // Will be added
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var result = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -210,10 +196,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void RecognizesLeadingLoopBlockItemsBeingAdded() public void RecognizesLeadingLoopBlockItemsBeingAdded()
{ {
// Arrange // 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");
oldTree.AddText(2, "x"); // Note that the '0' and '1' items are not present on this iteration oldTree.AddText(2, "x"); // Note that the '0' and '1' items are not present on this iteration
newTree.AddText(2, "x"); newTree.AddText(2, "x");
@ -222,7 +204,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(2, "x"); newTree.AddText(2, "x");
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var result = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -242,10 +224,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void RecognizesLeadingLoopBlockItemsBeingRemoved() public void RecognizesLeadingLoopBlockItemsBeingRemoved()
{ {
// Arrange // 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");
oldTree.AddText(0, "x"); oldTree.AddText(0, "x");
oldTree.AddText(1, "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 newTree.AddText(2, "x"); // Note that the '0' and '1' items are not present on this iteration
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var result = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -266,15 +244,11 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void HandlesAdjacentItemsBeingRemovedAndInsertedAtOnce() public void HandlesAdjacentItemsBeingRemovedAndInsertedAtOnce()
{ {
// Arrange // Arrange
var renderer = new FakeRenderer();
var oldTree = new RenderTreeBuilder(renderer);
var newTree = new RenderTreeBuilder(renderer);
var diff = new RenderTreeDiffComputer(renderer);
oldTree.AddText(0, "text"); oldTree.AddText(0, "text");
newTree.AddText(1, "text"); newTree.AddText(1, "text");
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var result = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -286,17 +260,13 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void RecognizesTextUpdates() public void RecognizesTextUpdates()
{ {
// Arrange // 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(123, "old text 1");
oldTree.AddText(182, "old text 2"); oldTree.AddText(182, "old text 2");
newTree.AddText(123, "new text 1"); newTree.AddText(123, "new text 1");
newTree.AddText(182, "new text 2"); newTree.AddText(182, "new text 2");
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var result = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, 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. // decide just to throw in this scenario, since it's unnecessary to support it.
// Arrange // 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.OpenElement(123, "old element");
oldTree.CloseElement(); oldTree.CloseElement();
newTree.OpenElement(123, "new element"); newTree.OpenElement(123, "new element");
newTree.CloseElement(); newTree.CloseElement();
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var result = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -346,15 +312,11 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void RecognizesComponentTypeChangesAtSameSequenceNumber() public void RecognizesComponentTypeChangesAtSameSequenceNumber()
{ {
// Arrange // Arrange
var renderer = new FakeRenderer();
var oldTree = new RenderTreeBuilder(renderer);
var newTree = new RenderTreeBuilder(renderer);
var diff = new RenderTreeDiffComputer(renderer);
oldTree.OpenComponentElement<FakeComponent>(123); oldTree.OpenComponentElement<FakeComponent>(123);
newTree.OpenComponentElement<FakeComponent2>(123); newTree.OpenComponentElement<FakeComponent2>(123);
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var result = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -370,10 +332,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void RecognizesAttributesAdded() public void RecognizesAttributesAdded()
{ {
// Arrange // 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.OpenElement(0, "My element");
oldTree.AddAttribute(1, "existing", "existing value"); oldTree.AddAttribute(1, "existing", "existing value");
oldTree.CloseElement(); oldTree.CloseElement();
@ -383,7 +341,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement(); newTree.CloseElement();
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var result = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -398,10 +356,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void RecognizesAttributesRemoved() public void RecognizesAttributesRemoved()
{ {
// Arrange // 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.OpenElement(0, "My element");
oldTree.AddAttribute(1, "will be removed", "will be removed value"); oldTree.AddAttribute(1, "will be removed", "will be removed value");
oldTree.AddAttribute(2, "will survive", "surviving value"); oldTree.AddAttribute(2, "will survive", "surviving value");
@ -411,7 +365,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement(); newTree.CloseElement();
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var result = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -426,10 +380,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void RecognizesAttributeStringValuesChanged() public void RecognizesAttributeStringValuesChanged()
{ {
// Arrange // 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.OpenElement(0, "My element");
oldTree.AddAttribute(1, "will remain", "will remain value"); oldTree.AddAttribute(1, "will remain", "will remain value");
oldTree.AddAttribute(2, "will change", "will change value"); oldTree.AddAttribute(2, "will change", "will change value");
@ -440,7 +390,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement(); newTree.CloseElement();
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var result = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -455,10 +405,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void RecognizesAttributeEventHandlerValuesChanged() public void RecognizesAttributeEventHandlerValuesChanged()
{ {
// Arrange // Arrange
var renderer = new FakeRenderer();
var oldTree = new RenderTreeBuilder(renderer);
var newTree = new RenderTreeBuilder(renderer);
var diff = new RenderTreeDiffComputer(renderer);
UIEventHandler retainedHandler = _ => { }; UIEventHandler retainedHandler = _ => { };
UIEventHandler removedHandler = _ => { }; UIEventHandler removedHandler = _ => { };
UIEventHandler addedHandler = _ => { }; UIEventHandler addedHandler = _ => { };
@ -472,7 +418,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement(); newTree.CloseElement();
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var result = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -487,10 +433,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void RecognizesAttributeNamesChangedAtSameSourceSequence() public void RecognizesAttributeNamesChangedAtSameSourceSequence()
{ {
// Arrange // 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.OpenElement(0, "My element");
oldTree.AddAttribute(1, "oldname", "same value"); oldTree.AddAttribute(1, "oldname", "same value");
oldTree.CloseElement(); oldTree.CloseElement();
@ -499,7 +441,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement(); newTree.CloseElement();
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var result = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -519,10 +461,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void DiffsElementsHierarchically() public void DiffsElementsHierarchically()
{ {
// Arrange // 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.AddText(09, "unrelated");
oldTree.OpenElement(10, "root"); oldTree.OpenElement(10, "root");
oldTree.OpenElement(11, "child"); oldTree.OpenElement(11, "child");
@ -542,7 +480,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement(); newTree.CloseElement();
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var result = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -563,10 +501,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void SkipsUnmodifiedSubtrees() public void SkipsUnmodifiedSubtrees()
{ {
// Arrange // 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.OpenElement(10, "root");
oldTree.AddText(11, "Text that will change"); oldTree.AddText(11, "Text that will change");
oldTree.OpenElement(12, "Subtree that will not change"); oldTree.OpenElement(12, "Subtree that will not change");
@ -586,7 +520,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement(); newTree.CloseElement();
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var result = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -603,10 +537,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void SkipsUnmodifiedTrailingSiblings() public void SkipsUnmodifiedTrailingSiblings()
{ {
// Arrange // 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(10, "text1");
oldTree.AddText(11, "text2"); oldTree.AddText(11, "text2");
oldTree.AddText(12, "text3"); oldTree.AddText(12, "text3");
@ -617,7 +547,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(13, "text4"); newTree.AddText(13, "text4");
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var result = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -632,10 +562,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void InstantiatesChildComponentsForInsertedNodes() public void InstantiatesChildComponentsForInsertedNodes()
{ {
// Arrange // 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.AddText(10, "text1"); // 0: text1
oldTree.OpenElement(11, "container"); // 1: <container> oldTree.OpenElement(11, "container"); // 1: <container>
oldTree.CloseElement(); // </container> oldTree.CloseElement(); // </container>
@ -648,10 +574,14 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement(); // </container> newTree.CloseElement(); // </container>
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var renderBatch = GetRenderedBatch();
// Assert // 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 => AssertEdit(entry, RenderTreeEditType.StepIn, 1),
entry => entry =>
{ {
@ -672,16 +602,26 @@ namespace Microsoft.AspNetCore.Blazor.Test
Assert.IsType<FakeComponent2>(newTreeNode.Component); Assert.IsType<FakeComponent2>(newTreeNode.Component);
}, },
entry => AssertEdit(entry, RenderTreeEditType.StepOut, 0)); 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] [Fact]
public void SetsKnownPropertiesOnChildComponents() public void SetsKnownPropertiesOnChildComponents()
{ {
// Arrange // Arrange
var renderer = new FakeRenderer();
var oldTree = new RenderTreeBuilder(renderer);
var newTree = new RenderTreeBuilder(renderer);
var diff = new RenderTreeDiffComputer(renderer);
var testObject = new object(); var testObject = new object();
newTree.OpenComponentElement<FakeComponent>(0); newTree.OpenComponentElement<FakeComponent>(0);
newTree.AddAttribute(1, nameof(FakeComponent.IntProperty), 123); newTree.AddAttribute(1, nameof(FakeComponent.IntProperty), 123);
@ -690,11 +630,14 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement(); newTree.CloseElement();
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var renderBatch = GetRenderedBatch();
var componentInstance = newTree.GetNodes().First().Component as FakeComponent; var componentInstance = newTree.GetNodes().First().Component as FakeComponent;
// Assert // 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.NotNull(componentInstance);
Assert.Equal(123, componentInstance.IntProperty); Assert.Equal(123, componentInstance.IntProperty);
Assert.Equal("some string", componentInstance.StringProperty); Assert.Equal("some string", componentInstance.StringProperty);
@ -705,10 +648,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void ThrowsIfAssigningUnknownPropertiesToChildComponents() public void ThrowsIfAssigningUnknownPropertiesToChildComponents()
{ {
// Arrange // Arrange
var renderer = new FakeRenderer();
var oldTree = new RenderTreeBuilder(renderer);
var newTree = new RenderTreeBuilder(renderer);
var diff = new RenderTreeDiffComputer(renderer);
var testObject = new object(); var testObject = new object();
newTree.OpenComponentElement<FakeComponent>(0); newTree.OpenComponentElement<FakeComponent>(0);
newTree.AddAttribute(1, "SomeUnknownProperty", 123); newTree.AddAttribute(1, "SomeUnknownProperty", 123);
@ -717,7 +656,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Act/Assert // Act/Assert
var ex = Assert.Throws<InvalidOperationException>(() => var ex = Assert.Throws<InvalidOperationException>(() =>
{ {
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); 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() public void ThrowsIfAssigningReadOnlyPropertiesToChildComponents()
{ {
// Arrange // Arrange
var renderer = new FakeRenderer();
var oldTree = new RenderTreeBuilder(renderer);
var newTree = new RenderTreeBuilder(renderer);
var diff = new RenderTreeDiffComputer(renderer);
var testObject = new object(); var testObject = new object();
newTree.OpenComponentElement<FakeComponent>(0); newTree.OpenComponentElement<FakeComponent>(0);
newTree.AddAttribute(1, nameof(FakeComponent.ReadonlyProperty), 123); newTree.AddAttribute(1, nameof(FakeComponent.ReadonlyProperty), 123);
@ -738,7 +673,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Act/Assert // Act/Assert
var ex = Assert.Throws<InvalidOperationException>(() => var ex = Assert.Throws<InvalidOperationException>(() =>
{ {
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 " + Assert.StartsWith($"Unable to set property '{nameof(FakeComponent.ReadonlyProperty)}' on " +
$"component of type '{typeof(FakeComponent).FullName}'.", ex.Message); $"component of type '{typeof(FakeComponent).FullName}'.", ex.Message);
@ -748,10 +683,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void RetainsChildComponentsForExistingNodes() public void RetainsChildComponentsForExistingNodes()
{ {
// Arrange // 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.AddText(10, "text1"); // 0: text1
oldTree.OpenElement(11, "container"); // 1: <container> oldTree.OpenElement(11, "container"); // 1: <container>
oldTree.OpenComponentElement<FakeComponent>(12); // 2: <FakeComponent> oldTree.OpenComponentElement<FakeComponent>(12); // 2: <FakeComponent>
@ -767,12 +698,12 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement(); // </FakeComponent2> newTree.CloseElement(); // </FakeComponent2>
newTree.CloseElement(); // </container newTree.CloseElement(); // </container
diff.ApplyNewRenderTreeVersion(new RenderTreeBuilder(renderer).GetNodes(), oldTree.GetNodes()); diff.ApplyNewRenderTreeVersion(new RenderBatchBuilder(), 0, new RenderTreeBuilder(renderer).GetNodes(), oldTree.GetNodes());
var originalFakeComponentInstance = oldTree.GetNodes().Array[2].Component; var originalFakeComponentInstance = oldTree.GetNodes().Array[2].Component;
var originalFakeComponent2Instance = oldTree.GetNodes().Array[3].Component; var originalFakeComponent2Instance = oldTree.GetNodes().Array[3].Component;
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var result = GetSingleUpdatedComponent();
var newNode1 = newTree.GetNodes().Array[2]; var newNode1 = newTree.GetNodes().Array[2];
var newNode2 = newTree.GetNodes().Array[3]; var newNode2 = newTree.GetNodes().Array[3];
@ -788,10 +719,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void UpdatesChangedPropertiesOnRetainedChildComponents() public void UpdatesChangedPropertiesOnRetainedChildComponents()
{ {
// Arrange // Arrange
var renderer = new FakeRenderer();
var oldTree = new RenderTreeBuilder(renderer);
var newTree = new RenderTreeBuilder(renderer);
var diff = new RenderTreeDiffComputer(renderer);
var objectWillNotChange = new object(); var objectWillNotChange = new object();
oldTree.OpenComponentElement<FakeComponent>(12); oldTree.OpenComponentElement<FakeComponent>(12);
oldTree.AddAttribute(13, nameof(FakeComponent.StringProperty), "String will change"); 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.AddAttribute(14, nameof(FakeComponent.ObjectProperty), objectWillNotChange);
newTree.CloseElement(); 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; var originalComponentInstance = (FakeComponent)oldTree.GetNodes().Array[0].Component;
originalComponentInstance.ObjectProperty = null; // So we can see it doesn't get reassigned originalComponentInstance.ObjectProperty = null; // So we can see it doesn't get reassigned
// Act // Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes()); var renderBatch = GetRenderedBatch();
var newComponentInstance = (FakeComponent)oldTree.GetNodes().Array[0].Component; var newComponentInstance = (FakeComponent)oldTree.GetNodes().Array[0].Component;
// Assert // Assert
Assert.Equal(2, renderBatch.UpdatedComponents.Count);
Assert.Same(originalComponentInstance, newComponentInstance); Assert.Same(originalComponentInstance, newComponentInstance);
Assert.Equal("String did change", newComponentInstance.StringProperty); 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 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 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 private class FakeComponent2 : IComponent
{ {
public void BuildRenderTree(RenderTreeBuilder builder) public void BuildRenderTree(RenderTreeBuilder builder)
=> throw new NotImplementedException(); {
builder.AddText(100, $"Hello from {nameof(FakeComponent2)}");
}
} }
private static void AssertEdit( private static void AssertEdit(

View File

@ -45,6 +45,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
{ {
builder.AddText(0, "Hello"); builder.AddText(0, "Hello");
builder.OpenComponentElement<MessageComponent>(1); builder.OpenComponentElement<MessageComponent>(1);
builder.AddAttribute(2, nameof(MessageComponent.Message), "Nested component output");
builder.CloseElement(); builder.CloseElement();
}); });
@ -57,13 +58,8 @@ namespace Microsoft.AspNetCore.Blazor.Test
// The nested component exists // The nested component exists
Assert.IsType<MessageComponent>(componentNode.Component); Assert.IsType<MessageComponent>(componentNode.Component);
((MessageComponent)(componentNode.Component)).Message = "Nested component output";
// It isn't rendered until the consumer asks for it to be // The nested component was rendered as part of the batch
Assert.False(renderer.RenderTreesByComponentId.ContainsKey(nestedComponentId));
// It can be rendered
renderer.RenderNewBatch(nestedComponentId);
Assert.Collection(renderer.RenderTreesByComponentId[nestedComponentId], Assert.Collection(renderer.RenderTreesByComponentId[nestedComponentId],
node => AssertNode.Text(node, "Nested component output")); node => AssertNode.Text(node, "Nested component output"));
} }
@ -418,7 +414,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
public new void RenderNewBatch(int componentId) public new void RenderNewBatch(int componentId)
=> base.RenderNewBatch(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) public new void DispatchEvent(int componentId, int renderTreeIndex, UIEventArgs args)
=> base.DispatchEvent(componentId, renderTreeIndex, 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;
}
} }
} }