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

View File

@ -66,20 +66,23 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering
}
/// <inheritdoc />
protected override void UpdateDisplay(
int componentId,
RenderTreeDiff renderTreeDiff)
protected override void UpdateDisplay(RenderBatch batch)
{
RegisteredFunction.InvokeUnmarshalled<RenderComponentArgs, object>(
"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<RenderComponentArgs, object>(
"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

View File

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

View File

@ -56,6 +56,16 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
_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>
/// Removes the last item.
/// </summary>

View File

@ -11,6 +11,11 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
/// </summary>
public readonly struct RenderTreeDiff
{
/// <summary>
/// Gets the ID of the component.
/// </summary>
public int ComponentId { get; }
/// <summary>
/// Gets the changes to the render tree since a previous state.
/// </summary>
@ -23,9 +28,11 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
public ArrayRange<RenderTreeNode> CurrentState { get; }
internal RenderTreeDiff(
int componentId,
ArrayRange<RenderTreeEdit> entries,
ArrayRange<RenderTreeNode> referenceTree)
{
ComponentId = componentId;
Edits = entries;
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
/// here because we have the right information and are already walking the trees to do the diff.
/// </summary>
public RenderTreeDiff ApplyNewRenderTreeVersion(
public void ApplyNewRenderTreeVersion(
RenderBatchBuilder batchBuilder,
int componentId,
ArrayRange<RenderTreeNode> oldTree,
ArrayRange<RenderTreeNode> 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);
}
}
}

View File

@ -38,25 +38,21 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
}
/// <summary>
/// Regenerates the <see cref="RenderTree"/> and notifies the <see cref="Renderer"/>
/// to update the visible UI state.
/// Regenerates the <see cref="RenderTree"/> and adds the changes to the
/// <paramref name="batchBuilder"/>.
/// </summary>
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);
}
/// <summary>
@ -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);
}
}
}

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 System;
using System.Runtime.CompilerServices;
using System.Threading;
namespace Microsoft.AspNetCore.Blazor.Rendering
{
@ -24,6 +25,11 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
= new ConditionalWeakTable<IComponent, ComponentState>();
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>
/// Associates the <see cref="IComponent"/> with the <see cref="Renderer"/>, assigning
/// an identifier that is unique within the scope of the <see cref="Renderer"/>.
@ -43,13 +49,10 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
}
/// <summary>
/// Updates the visible UI to display the supplied <paramref name="renderTree"/>
/// at the location corresponding to the <paramref name="componentId"/>.
/// Updates the visible UI.
/// </summary>
/// <param name="componentId">The identifier for the updated <see cref="IComponent"/>.</param>
/// <param name="renderTreeDiff">The changes to the render tree since the component was last rendered.</param>
internal protected abstract void UpdateDisplay(
int componentId, RenderTreeDiff renderTreeDiff);
/// <param name="renderBatch">The changes to the UI since the previous call.</param>
internal protected abstract void UpdateDisplay(RenderBatch renderBatch);
/// <summary>
/// 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>
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>

View File

@ -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();
}

View File

@ -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();
}
}

View File

@ -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<RenderTreeBuilder> 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<FakeComponent>(123);
newTree.OpenComponentElement<FakeComponent2>(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: <container>
oldTree.CloseElement(); // </container>
@ -648,10 +574,14 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement(); // </container>
// 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<FakeComponent2>(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<FakeComponent>(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<FakeComponent>(0);
newTree.AddAttribute(1, "SomeUnknownProperty", 123);
@ -717,7 +656,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Act/Assert
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);
}
@ -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<FakeComponent>(0);
newTree.AddAttribute(1, nameof(FakeComponent.ReadonlyProperty), 123);
@ -738,7 +673,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Act/Assert
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 " +
$"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: <container>
oldTree.OpenComponentElement<FakeComponent>(12); // 2: <FakeComponent>
@ -767,12 +698,12 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement(); // </FakeComponent2>
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 originalFakeComponent2Instance = oldTree.GetNodes().Array[3].Component;
// Act
var result = diff.ApplyNewRenderTreeVersion(oldTree.GetNodes(), newTree.GetNodes());
var result = GetSingleUpdatedComponent();
var newNode1 = newTree.GetNodes().Array[2];
var newNode2 = newTree.GetNodes().Array[3];
@ -788,10 +719,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void UpdatesChangedPropertiesOnRetainedChildComponents()
{
// Arrange
var renderer = new FakeRenderer();
var oldTree = new RenderTreeBuilder(renderer);
var newTree = new RenderTreeBuilder(renderer);
var diff = new RenderTreeDiffComputer(renderer);
var objectWillNotChange = new object();
oldTree.OpenComponentElement<FakeComponent>(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(

View File

@ -45,6 +45,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
{
builder.AddText(0, "Hello");
builder.OpenComponentElement<MessageComponent>(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<MessageComponent>(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;
}
}
}