Have RenderTreeDiff build its own array of referenced frames rather than pointing to the latest render tree

This is in preparation for supporting multiple diffs for the same
component in a single batch (which means we can't rely on there being at
most only new render tree per component)
This commit is contained in:
Steve Sanderson 2018-02-07 00:47:49 +00:00
parent 33932f41fc
commit 83fa72bc7e
13 changed files with 319 additions and 149 deletions

View File

@ -229,7 +229,7 @@ function removeAttributeFromDOM(parent: Element, childIndex: number, attributeNa
element.removeAttribute(attributeName);
}
function raiseEvent(browserRendererId: number, componentId: number, renderTreeFrameIndex: number, eventInfoType: EventInfoType, eventInfo: any) {
function raiseEvent(browserRendererId: number, componentId: number, referenceTreeFrameIndex: number, eventInfoType: EventInfoType, eventInfo: any) {
if (!raiseEventMethod) {
raiseEventMethod = platform.findMethod(
'Microsoft.AspNetCore.Blazor.Browser', 'Microsoft.AspNetCore.Blazor.Browser.Rendering', 'BrowserRendererEventDispatcher', 'DispatchEvent'
@ -239,7 +239,7 @@ function raiseEvent(browserRendererId: number, componentId: number, renderTreeFr
const eventDescriptor = {
BrowserRendererId: browserRendererId,
ComponentId: componentId,
RenderTreeFrameIndex: renderTreeFrameIndex,
ReferenceTreeFrameIndex: referenceTreeFrameIndex,
EventArgsType: eventInfoType
};

View File

@ -32,8 +32,8 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering
_browserRendererId = BrowserRendererRegistry.Add(this);
}
internal void DispatchBrowserEvent(int componentId, int renderTreeIndex, UIEventArgs eventArgs)
=> DispatchEvent(componentId, renderTreeIndex, eventArgs);
internal void DispatchBrowserEvent(int componentId, int referenceTreeIndex, UIEventArgs eventArgs)
=> DispatchEvent(componentId, referenceTreeIndex, eventArgs);
internal void RenderNewBatchInternal(int componentId)
=> RenderNewBatch(componentId);

View File

@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering
var browserRenderer = BrowserRendererRegistry.Find(eventDescriptor.BrowserRendererId);
browserRenderer.DispatchBrowserEvent(
eventDescriptor.ComponentId,
eventDescriptor.RenderTreeFrameIndex,
eventDescriptor.ReferenceTreeFrameIndex,
eventArgs);
}
@ -45,7 +45,7 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering
{
public int BrowserRendererId { get; set; }
public int ComponentId { get; set; }
public int RenderTreeFrameIndex { get; set; }
public int ReferenceTreeFrameIndex { get; set; }
public string EventArgsType { get; set; }
}
}

View File

@ -51,15 +51,40 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
/// Appends a new item, automatically resizing the underlying array if necessary.
/// </summary>
/// <param name="item">The item to append.</param>
/// <returns>The index of the appended item.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] // Just like System.Collections.Generic.List<T>
public void Append(in T item)
public int Append(in T item)
{
if (_itemsInUse == _items.Length)
{
SetCapacity(_itemsInUse * 2, preserveContents: true);
}
_items[_itemsInUse++] = item;
var indexOfAppendedItem = _itemsInUse++;
_items[indexOfAppendedItem] = item;
return indexOfAppendedItem;
}
internal int Append(T[] source, int startIndex, int length)
{
// Expand storage if needed. Using same doubling approach as would
// be used if you inserted the items one-by-one.
var requiredCapacity = _itemsInUse + length;
if (_items.Length < requiredCapacity)
{
var candidateCapacity = _itemsInUse * 2;
while (candidateCapacity < requiredCapacity)
{
candidateCapacity *= 2;
}
SetCapacity(candidateCapacity, preserveContents: true);
}
Array.Copy(source, startIndex, _items, _itemsInUse, length);
var startIndexOfAppendedItems = _itemsInUse;
_itemsInUse += length;
return startIndexOfAppendedItems;
}
/// <summary>
@ -94,9 +119,9 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
{
SetCapacity((previousItemsInUse + _items.Length) / 2, preserveContents: false);
}
else
else if (previousItemsInUse > 0)
{
Array.Clear(_items, 0, _itemsInUse); // Release to GC
Array.Clear(_items, 0, previousItemsInUse); // Release to GC
}
}

View File

@ -1,13 +1,10 @@
// 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 System;
namespace Microsoft.AspNetCore.Blazor.RenderTree
{
/// <summary>
/// Describes changes to a component's render tree between successive renders,
/// as well as the resulting state.
/// Describes changes to a component's render tree between successive renders.
/// </summary>
public readonly struct RenderTreeDiff
{
@ -22,19 +19,20 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
public ArrayRange<RenderTreeEdit> Edits { get; }
/// <summary>
/// Gets the latest render tree. That is, the result of applying the <see cref="Edits"/>
/// to the previous state.
/// Gets render frames that may be referenced by entries in <see cref="Edits"/>.
/// For example, edit entries of type <see cref="RenderTreeEditType.PrependFrame"/>
/// will point to an entry in this array to specify the subtree to be prepended.
/// </summary>
public ArrayRange<RenderTreeFrame> CurrentState { get; }
public ArrayRange<RenderTreeFrame> ReferenceFrames { get; }
internal RenderTreeDiff(
int componentId,
ArrayRange<RenderTreeEdit> entries,
ArrayRange<RenderTreeFrame> referenceTree)
ArrayRange<RenderTreeFrame> referenceFrames)
{
ComponentId = componentId;
Edits = entries;
CurrentState = referenceTree;
ReferenceFrames = referenceFrames;
}
}
}

View File

@ -12,6 +12,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
{
private readonly Renderer _renderer;
private readonly ArrayBuilder<RenderTreeEdit> _entries = new ArrayBuilder<RenderTreeEdit>(10);
private readonly ArrayBuilder<RenderTreeFrame> _referenceFrames = new ArrayBuilder<RenderTreeFrame>(10);
public RenderTreeDiffComputer(Renderer renderer)
{
@ -31,13 +32,14 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
ArrayRange<RenderTreeFrame> newTree)
{
_entries.Clear();
_referenceFrames.Clear();
var siblingIndex = 0;
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));
new RenderTreeDiff(componentId, _entries.ToRange(), _referenceFrames.ToRange()));
}
private void AppendDiffEntriesForRange(
@ -136,7 +138,8 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
var newFrameType = newFrame.FrameType;
if (newFrameType == RenderTreeFrameType.Attribute)
{
Append(RenderTreeEdit.SetAttribute(siblingIndex, newStartIndex));
var referenceFrameIndex = _referenceFrames.Append(newFrame);
Append(RenderTreeEdit.SetAttribute(siblingIndex, referenceFrameIndex));
newStartIndex++;
}
else
@ -146,7 +149,8 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
InstantiateChildComponents(batchBuilder, newTree, newStartIndex);
}
Append(RenderTreeEdit.PrependFrame(siblingIndex, newStartIndex));
var referenceFrameIndex = AppendSubtreeToReferenceFrames(newTree, newStartIndex);
Append(RenderTreeEdit.PrependFrame(siblingIndex, referenceFrameIndex));
newStartIndex = NextSiblingIndex(newFrame, newStartIndex);
siblingIndex++;
}
@ -180,6 +184,35 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
}
}
public UIEventHandler TemporaryGetEventHandlerMethod(int referenceFrameIndex)
{
// TODO: Change how event handlers are referenced
// The approach used here is invalid. We can't really assume that the event handler exists
// in the most recent diff's _referenceFrames. Nor can we assume that its index in the
// ComponentState's _renderTreeBuilderCurrent frames array is unchanged (any number of rerenders
// might have taken place since then, inserting frames before it, but not actually changing
// the delegate instance and therefore not including it in any _referenceFrames).
// Need some way of referencing event handlers that is independent of this.
var eventHandler = _referenceFrames.Buffer[referenceFrameIndex].AttributeValue as UIEventHandler;
return eventHandler
?? throw new ArgumentException($"The reference frame at index {referenceFrameIndex} does not specify a {nameof(UIEventHandler)}.");
}
private int AppendSubtreeToReferenceFrames(RenderTreeFrame[] fromTree, int fromIndex)
{
ref var rootFrame = ref fromTree[fromIndex];
var subtreeLength = rootFrame.ElementSubtreeLength;
if (subtreeLength == 0)
{
// It's just a single frame (e.g., a text frame)
return _referenceFrames.Append(rootFrame);
}
else
{
return _referenceFrames.Append(fromTree, fromIndex, subtreeLength);
}
}
private void UpdateRetainedChildComponent(
RenderBatchBuilder batchBuilder,
RenderTreeFrame[] oldTree, int oldComponentIndex,
@ -340,7 +373,8 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
var newText = newFrame.TextContent;
if (!string.Equals(oldText, newText, StringComparison.Ordinal))
{
Append(RenderTreeEdit.UpdateText(siblingIndex, newFrameIndex));
var referenceFrameIndex = _referenceFrames.Append(newFrame);
Append(RenderTreeEdit.UpdateText(siblingIndex, referenceFrameIndex));
}
siblingIndex++;
break;
@ -390,7 +424,9 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
// Elements with different names are treated as completely unrelated
InstantiateChildComponents(batchBuilder, newTree, newFrameIndex);
DisposeChildComponents(batchBuilder, oldTree, oldFrameIndex);
Append(RenderTreeEdit.PrependFrame(siblingIndex, newFrameIndex));
var referenceFrameIndex = AppendSubtreeToReferenceFrames(newTree, newFrameIndex);
Append(RenderTreeEdit.PrependFrame(siblingIndex, referenceFrameIndex));
siblingIndex++;
Append(RenderTreeEdit.RemoveFrame(siblingIndex));
}
@ -415,7 +451,9 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
// Child components of different types are treated as completely unrelated
InstantiateChildComponents(batchBuilder, newTree, newFrameIndex);
DisposeChildComponents(batchBuilder, oldTree, oldFrameIndex);
Append(RenderTreeEdit.PrependFrame(siblingIndex, newFrameIndex));
var referenceFrameIndex = AppendSubtreeToReferenceFrames(newTree, newFrameIndex);
Append(RenderTreeEdit.PrependFrame(siblingIndex, referenceFrameIndex));
siblingIndex++;
Append(RenderTreeEdit.RemoveFrame(siblingIndex));
}
@ -432,7 +470,8 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
var valueChanged = !Equals(oldFrame.AttributeValue, newFrame.AttributeValue);
if (valueChanged)
{
Append(RenderTreeEdit.SetAttribute(siblingIndex, newFrameIndex));
var referenceFrameIndex = _referenceFrames.Append(newFrame);
Append(RenderTreeEdit.SetAttribute(siblingIndex, referenceFrameIndex));
}
}
else
@ -440,7 +479,8 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
// Since this code path is never reachable for Razor components (because you
// can't have two different attribute names from the same source sequence), we
// could consider removing the 'name equality' check entirely for perf
Append(RenderTreeEdit.SetAttribute(siblingIndex, newFrameIndex));
var referenceFrameIndex = _referenceFrames.Append(newFrame);
Append(RenderTreeEdit.SetAttribute(siblingIndex, referenceFrameIndex));
Append(RenderTreeEdit.RemoveAttribute(siblingIndex, oldName));
}
break;

View File

@ -19,11 +19,11 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
public readonly int SiblingIndex;
/// <summary>
/// Gets the index of related data in an associated render tree. For example, if the
/// Gets the index of related data in an associated render frames array. For example, if the
/// <see cref="Type"/> value is <see cref="RenderTreeEditType.PrependFrame"/>, gets the
/// index of the new frame data in an associated render tree.
/// </summary>
public readonly int NewTreeIndex;
public readonly int ReferenceFrameIndex;
/// <summary>
/// If the <see cref="Type"/> value is <see cref="RenderTreeEditType.RemoveAttribute"/>,
@ -42,11 +42,11 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
SiblingIndex = siblingIndex;
}
private RenderTreeEdit(RenderTreeEditType type, int siblingIndex, int newTreeIndex) : this()
private RenderTreeEdit(RenderTreeEditType type, int siblingIndex, int referenceFrameIndex) : this()
{
Type = type;
SiblingIndex = siblingIndex;
NewTreeIndex = newTreeIndex;
ReferenceFrameIndex = referenceFrameIndex;
}
private RenderTreeEdit(RenderTreeEditType type, int siblingIndex, string removedAttributeName) : this()
@ -59,14 +59,14 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
internal static RenderTreeEdit RemoveFrame(int siblingIndex)
=> new RenderTreeEdit(RenderTreeEditType.RemoveFrame, siblingIndex);
internal static RenderTreeEdit PrependFrame(int siblingIndex, int newTreeIndex)
=> new RenderTreeEdit(RenderTreeEditType.PrependFrame, siblingIndex, newTreeIndex);
internal static RenderTreeEdit PrependFrame(int siblingIndex, int referenceFrameIndex)
=> new RenderTreeEdit(RenderTreeEditType.PrependFrame, siblingIndex, referenceFrameIndex);
internal static RenderTreeEdit UpdateText(int siblingIndex, int newTreeIndex)
=> new RenderTreeEdit(RenderTreeEditType.UpdateText, siblingIndex, newTreeIndex);
internal static RenderTreeEdit UpdateText(int siblingIndex, int referenceFrameIndex)
=> new RenderTreeEdit(RenderTreeEditType.UpdateText, siblingIndex, referenceFrameIndex);
internal static RenderTreeEdit SetAttribute(int siblingIndex, int newTreeIndex)
=> new RenderTreeEdit(RenderTreeEditType.SetAttribute, siblingIndex, newTreeIndex);
internal static RenderTreeEdit SetAttribute(int siblingIndex, int referenceFrameIndex)
=> new RenderTreeEdit(RenderTreeEditType.SetAttribute, siblingIndex, referenceFrameIndex);
internal static RenderTreeEdit RemoveAttribute(int siblingIndex, string name)
=> new RenderTreeEdit(RenderTreeEditType.RemoveAttribute, siblingIndex, name);

View File

@ -58,22 +58,16 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
/// <summary>
/// Invokes the handler corresponding to an event.
/// </summary>
/// <param name="renderTreeIndex">The index of the current render tree frame that holds the event handler to be invoked.</param>
/// <param name="referenceTreeIndex">The index of the frame in the latest diff reference tree that holds the event handler to be invoked.</param>
/// <param name="eventArgs">Arguments to be passed to the event handler.</param>
public void DispatchEvent(int renderTreeIndex, UIEventArgs eventArgs)
public void DispatchEvent(int referenceTreeIndex, UIEventArgs eventArgs)
{
if (eventArgs == null)
{
throw new ArgumentNullException(nameof(eventArgs));
}
var frames = _renderTreeBuilderCurrent.GetFrames();
var eventHandler = frames.Array[renderTreeIndex].AttributeValue as UIEventHandler;
if (eventHandler == null)
{
throw new ArgumentException($"The render tree frame at index {renderTreeIndex} does not specify a {nameof(UIEventHandler)}.");
}
var eventHandler = _diffComputer.TemporaryGetEventHandlerMethod(referenceTreeIndex);
eventHandler.Invoke(eventArgs);
// After any event, we synchronously re-render. Most of the time this means that

View File

@ -106,10 +106,10 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
/// Notifies the specified component that an event has occurred.
/// </summary>
/// <param name="componentId">The unique identifier for the component within the scope of this <see cref="Renderer"/>.</param>
/// <param name="renderTreeIndex">The index into the component's current render tree that specifies which event handler to invoke.</param>
/// <param name="referenceTreeIndex">The index into the component's latest diff reference tree that specifies which event handler to invoke.</param>
/// <param name="eventArgs">Arguments to be passed to the event handler.</param>
protected void DispatchEvent(int componentId, int renderTreeIndex, UIEventArgs eventArgs)
=> GetRequiredComponentState(componentId).DispatchEvent(renderTreeIndex, eventArgs);
protected void DispatchEvent(int componentId, int referenceTreeIndex, UIEventArgs eventArgs)
=> GetRequiredComponentState(componentId).DispatchEvent(referenceTreeIndex, eventArgs);
internal void InstantiateChildComponent(RenderTreeFrame[] frames, int componentFrameIndex)
{

View File

@ -74,7 +74,8 @@ namespace Microsoft.AspNetCore.Blazor.Test
entry =>
{
AssertEdit(entry, RenderTreeEditType.PrependFrame, 1);
Assert.Equal(1, entry.NewTreeIndex);
Assert.Equal(0, entry.ReferenceFrameIndex);
AssertFrame.Text(result.ReferenceFrames.Array[0], "text1", 1);
});
}
@ -120,12 +121,12 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void RecognizesTrailingSequenceWithinLoopBlockBeingAppended()
{
// Arrange
oldTree.AddText(0, "x"); // Loop start
oldTree.AddText(0, "x"); // Loop start
newTree.AddText(0, "x"); // Loop start
newTree.AddText(1, "x"); // Will be added
newTree.AddText(2, "x"); // Will be added
newTree.AddText(0, "x"); // Loop start
oldTree.AddText(10, "x"); // Loop start
oldTree.AddText(10, "x"); // Loop start
newTree.AddText(10, "x"); // Loop start
newTree.AddText(11, "x"); // Will be added
newTree.AddText(12, "x"); // Will be added
newTree.AddText(10, "x"); // Loop start
// Act
var result = GetSingleUpdatedComponent();
@ -135,13 +136,16 @@ namespace Microsoft.AspNetCore.Blazor.Test
entry =>
{
AssertEdit(entry, RenderTreeEditType.PrependFrame, 1);
Assert.Equal(1, entry.NewTreeIndex);
Assert.Equal(0, entry.ReferenceFrameIndex);
},
entry =>
{
AssertEdit(entry, RenderTreeEditType.PrependFrame, 2);
Assert.Equal(2, entry.NewTreeIndex);
Assert.Equal(1, entry.ReferenceFrameIndex);
});
Assert.Collection(result.ReferenceFrames,
frame => AssertFrame.Text(frame, "x", 11),
frame => AssertFrame.Text(frame, "x", 12));
}
[Fact]
@ -168,12 +172,12 @@ namespace Microsoft.AspNetCore.Blazor.Test
public void RecognizesTrailingLoopBlockBeingAdded()
{
// Arrange
oldTree.AddText(0, "x");
oldTree.AddText(1, "x");
newTree.AddText(0, "x");
newTree.AddText(1, "x");
newTree.AddText(0, "x"); // Will be added
newTree.AddText(1, "x"); // Will be added
oldTree.AddText(10, "x");
oldTree.AddText(11, "x");
newTree.AddText(10, "x");
newTree.AddText(11, "x");
newTree.AddText(10, "x"); // Will be added
newTree.AddText(11, "x"); // Will be added
// Act
var result = GetSingleUpdatedComponent();
@ -183,25 +187,28 @@ namespace Microsoft.AspNetCore.Blazor.Test
entry =>
{
AssertEdit(entry, RenderTreeEditType.PrependFrame, 2);
Assert.Equal(2, entry.NewTreeIndex);
Assert.Equal(0, entry.ReferenceFrameIndex);
},
entry =>
{
AssertEdit(entry, RenderTreeEditType.PrependFrame, 3);
Assert.Equal(3, entry.NewTreeIndex);
Assert.Equal(1, entry.ReferenceFrameIndex);
});
Assert.Collection(result.ReferenceFrames,
frame => AssertFrame.Text(frame, "x", 10),
frame => AssertFrame.Text(frame, "x", 11));
}
[Fact]
public void RecognizesLeadingLoopBlockItemsBeingAdded()
{
// Arrange
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");
newTree.AddText(0, "x");
newTree.AddText(1, "x");
newTree.AddText(2, "x");
oldTree.AddText(12, "x");
oldTree.AddText(12, "x"); // Note that the '0' and '1' items are not present on this iteration
newTree.AddText(12, "x");
newTree.AddText(10, "x");
newTree.AddText(11, "x");
newTree.AddText(12, "x");
// Act
var result = GetSingleUpdatedComponent();
@ -211,13 +218,16 @@ namespace Microsoft.AspNetCore.Blazor.Test
entry =>
{
AssertEdit(entry, RenderTreeEditType.PrependFrame, 1);
Assert.Equal(1, entry.NewTreeIndex);
Assert.Equal(0, entry.ReferenceFrameIndex);
},
entry =>
{
AssertEdit(entry, RenderTreeEditType.PrependFrame, 2);
Assert.Equal(2, entry.NewTreeIndex);
Assert.Equal(1, entry.ReferenceFrameIndex);
});
Assert.Collection(result.ReferenceFrames,
frame => AssertFrame.Text(frame, "x", 10),
frame => AssertFrame.Text(frame, "x", 11));
}
[Fact]
@ -273,12 +283,12 @@ namespace Microsoft.AspNetCore.Blazor.Test
entry =>
{
AssertEdit(entry, RenderTreeEditType.UpdateText, 0);
Assert.Equal(0, entry.NewTreeIndex);
Assert.Equal(0, entry.ReferenceFrameIndex);
},
entry =>
{
AssertEdit(entry, RenderTreeEditType.UpdateText, 1);
Assert.Equal(1, entry.NewTreeIndex);
Assert.Equal(1, entry.ReferenceFrameIndex);
});
}
@ -303,7 +313,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
entry =>
{
AssertEdit(entry, RenderTreeEditType.PrependFrame, 0);
Assert.Equal(0, entry.NewTreeIndex);
Assert.Equal(0, entry.ReferenceFrameIndex);
},
entry => AssertEdit(entry, RenderTreeEditType.RemoveFrame, 1));
}
@ -333,8 +343,8 @@ namespace Microsoft.AspNetCore.Blazor.Test
entry =>
{
AssertEdit(entry, RenderTreeEditType.PrependFrame, 0);
Assert.Equal(0, entry.NewTreeIndex);
Assert.IsType<FakeComponent2>(updatedComponent1.CurrentState.Array[0].Component);
Assert.Equal(0, entry.ReferenceFrameIndex);
Assert.IsType<FakeComponent2>(updatedComponent1.ReferenceFrames.Array[0].Component);
},
entry => AssertEdit(entry, RenderTreeEditType.RemoveFrame, 1));
@ -344,7 +354,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
entry =>
{
AssertEdit(entry, RenderTreeEditType.PrependFrame, 0);
Assert.Equal(0, entry.NewTreeIndex);
Assert.Equal(0, entry.ReferenceFrameIndex);
});
}
@ -368,8 +378,10 @@ namespace Microsoft.AspNetCore.Blazor.Test
entry =>
{
AssertEdit(entry, RenderTreeEditType.SetAttribute, 0);
Assert.Equal(2, entry.NewTreeIndex);
Assert.Equal(0, entry.ReferenceFrameIndex);
});
Assert.Collection(result.ReferenceFrames,
frame => AssertFrame.Attribute(frame, "added", "added value"));
}
[Fact]
@ -417,8 +429,10 @@ namespace Microsoft.AspNetCore.Blazor.Test
entry =>
{
AssertEdit(entry, RenderTreeEditType.SetAttribute, 0);
Assert.Equal(2, entry.NewTreeIndex);
Assert.Equal(0, entry.ReferenceFrameIndex);
});
Assert.Collection(result.ReferenceFrames,
frame => AssertFrame.Attribute(frame, "will change", "did change value"));
}
[Fact]
@ -445,8 +459,10 @@ namespace Microsoft.AspNetCore.Blazor.Test
entry =>
{
AssertEdit(entry, RenderTreeEditType.SetAttribute, 0);
Assert.Equal(2, entry.NewTreeIndex);
Assert.Equal(0, entry.ReferenceFrameIndex);
});
Assert.Collection(result.ReferenceFrames,
frame => AssertFrame.Attribute(frame, "will change", addedHandler));
}
[Fact]
@ -468,13 +484,15 @@ namespace Microsoft.AspNetCore.Blazor.Test
entry =>
{
AssertEdit(entry, RenderTreeEditType.SetAttribute, 0);
Assert.Equal(1, entry.NewTreeIndex);
Assert.Equal(0, entry.ReferenceFrameIndex);
},
entry =>
{
AssertEdit(entry, RenderTreeEditType.RemoveAttribute, 0);
Assert.Equal("oldname", entry.RemovedAttributeName);
});
Assert.Collection(result.ReferenceFrames,
frame => AssertFrame.Attribute(frame, "newname", "same value"));
}
[Fact]
@ -510,11 +528,13 @@ namespace Microsoft.AspNetCore.Blazor.Test
entry =>
{
AssertEdit(entry, RenderTreeEditType.UpdateText, 0);
Assert.Equal(4, entry.NewTreeIndex);
Assert.Equal(0, entry.ReferenceFrameIndex);
},
entry => AssertEdit(entry, RenderTreeEditType.StepOut, 0),
entry => AssertEdit(entry, RenderTreeEditType.StepOut, 0),
entry => AssertEdit(entry, RenderTreeEditType.StepOut, 0));
Assert.Collection(result.ReferenceFrames,
frame => AssertFrame.Text(frame, "grandchild new text", 13));
}
[Fact]
@ -548,9 +568,11 @@ namespace Microsoft.AspNetCore.Blazor.Test
entry =>
{
AssertEdit(entry, RenderTreeEditType.UpdateText, 0);
Assert.Equal(1, entry.NewTreeIndex);
Assert.Equal(0, entry.ReferenceFrameIndex);
},
entry => AssertEdit(entry, RenderTreeEditType.StepOut, 0));
Assert.Collection(result.ReferenceFrames,
frame => AssertFrame.Text(frame, "Text that has changed", 11));
}
[Fact]
@ -574,8 +596,10 @@ namespace Microsoft.AspNetCore.Blazor.Test
entry =>
{
AssertEdit(entry, RenderTreeEditType.UpdateText, 1);
Assert.Equal(1, entry.NewTreeIndex);
Assert.Equal(0, entry.ReferenceFrameIndex);
});
Assert.Collection(result.ReferenceFrames,
frame => AssertFrame.Text(frame, "text2modified", 11));
}
[Fact]
@ -606,35 +630,30 @@ namespace Microsoft.AspNetCore.Blazor.Test
entry =>
{
AssertEdit(entry, RenderTreeEditType.PrependFrame, 0);
Assert.Equal(2, entry.NewTreeIndex);
var newTreeFrame = newTree.GetFrames().Array[entry.NewTreeIndex];
Assert.Equal(0, newTreeFrame.ComponentId);
Assert.IsType<FakeComponent>(newTreeFrame.Component);
Assert.Equal(0, entry.ReferenceFrameIndex);
},
entry =>
{
AssertEdit(entry, RenderTreeEditType.PrependFrame, 1);
Assert.Equal(3, entry.NewTreeIndex);
var newTreeFrame = newTree.GetFrames().Array[entry.NewTreeIndex];
Assert.Equal(1, newTreeFrame.ComponentId);
Assert.IsType<FakeComponent2>(newTreeFrame.Component);
Assert.Equal(1, entry.ReferenceFrameIndex);
},
entry => AssertEdit(entry, RenderTreeEditType.StepOut, 0));
Assert.Collection(firstComponentDiff.ReferenceFrames,
frame => AssertFrame.ComponentWithInstance<FakeComponent>(frame, 0, 12),
frame => AssertFrame.ComponentWithInstance<FakeComponent2>(frame, 1, 13));
// 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 frames
Assert.Empty(secondComponentDiff.CurrentState); // Because FakeComponent produces no frames
Assert.Empty(secondComponentDiff.ReferenceFrames); // Because FakeComponent produces no frames
// 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.PrependFrame, 0));
Assert.Collection(thirdComponentDiff.CurrentState,
Assert.Collection(thirdComponentDiff.ReferenceFrames,
frame => AssertFrame.Text(frame, $"Hello from {nameof(FakeComponent2)}"));
}
@ -776,7 +795,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
var diffForChildComponent = batch.UpdatedComponents.Array[1];
// Assert
Assert.Collection(diffForChildComponent.CurrentState,
Assert.Collection(diffForChildComponent.ReferenceFrames,
frame => AssertFrame.Text(frame, "Notifications: 1", 0));
}
@ -799,10 +818,10 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Act/Assert 0: Initial render
var batch0 = GetRenderedBatch(new RenderTreeBuilder(renderer), oldTree);
var diffForChildComponent0 = batch0.UpdatedComponents.Array[1];
var childComponentFrame = batch0.UpdatedComponents.Array[0].CurrentState.Array[0];
var childComponentFrame = batch0.UpdatedComponents.Array[0].ReferenceFrames.Array[0];
var childComponentInstance = (HandlePropertiesChangedComponent)childComponentFrame.Component;
Assert.Equal(1, childComponentInstance.NotificationsCount);
Assert.Collection(diffForChildComponent0.CurrentState,
Assert.Collection(diffForChildComponent0.ReferenceFrames,
frame => AssertFrame.Text(frame, "Notifications: 1", 0));
// Act/Assert 1: If properties didn't change, we don't notify
@ -813,7 +832,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
var batch2 = GetRenderedBatch(newTree1, newTree2);
var diffForChildComponent2 = batch2.UpdatedComponents.Array[1];
Assert.Equal(2, childComponentInstance.NotificationsCount);
Assert.Collection(diffForChildComponent2.CurrentState,
Assert.Collection(diffForChildComponent2.ReferenceFrames,
frame => AssertFrame.Text(frame, "Notifications: 2", 0));
}

View File

@ -31,7 +31,14 @@ namespace Microsoft.AspNetCore.Blazor.Test
renderer.RenderNewBatch(componentId);
// Assert
Assert.Collection(renderer.Batches.Single().RenderTreesByComponentId[componentId],
var diff = renderer.Batches.Single().DiffsByComponentId[componentId].Single();
Assert.Collection(diff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
});
Assert.Collection(diff.ReferenceFrames,
frame => AssertFrame.Element(frame, "my element", 2),
frame => AssertFrame.Text(frame, "some text"));
}
@ -52,15 +59,23 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Act/Assert
var componentId = renderer.AssignComponentId(component);
renderer.RenderNewBatch(componentId);
var componentFrame = renderer.Batches.Single().RenderTreesByComponentId[componentId]
var batch = renderer.Batches.Single();
var componentFrame = batch.DiffsByComponentId[componentId].Single().ReferenceFrames
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
var nestedComponentId = componentFrame.ComponentId;
var nestedComponentDiff = batch.DiffsByComponentId[nestedComponentId].Single();
// The nested component exists
Assert.IsType<MessageComponent>(componentFrame.Component);
// The nested component was rendered as part of the batch
Assert.Collection(renderer.Batches.Single().RenderTreesByComponentId[nestedComponentId],
Assert.Collection(nestedComponentDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
});
Assert.Collection(nestedComponentDiff.ReferenceFrames,
frame => AssertFrame.Text(frame, "Nested component output"));
}
@ -74,13 +89,27 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Act/Assert: first render
renderer.RenderNewBatch(componentId);
Assert.Collection(renderer.Batches.Single().RenderTreesByComponentId[componentId],
var firstDiff = renderer.Batches.Single().DiffsByComponentId[componentId].Single();
Assert.Collection(firstDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
});
Assert.Collection(firstDiff.ReferenceFrames,
frame => AssertFrame.Text(frame, "Initial message"));
// Act/Assert: second render
component.Message = "Modified message";
renderer.RenderNewBatch(componentId);
Assert.Collection(renderer.Batches[1].RenderTreesByComponentId[componentId],
var secondDiff = renderer.Batches.Skip(1).Single().DiffsByComponentId[componentId].Single();
Assert.Collection(firstDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
});
Assert.Collection(firstDiff.ReferenceFrames,
frame => AssertFrame.Text(frame, "Modified message"));
}
@ -96,21 +125,37 @@ namespace Microsoft.AspNetCore.Blazor.Test
});
var parentComponentId = renderer.AssignComponentId(parentComponent);
renderer.RenderNewBatch(parentComponentId);
var nestedComponentFrame = renderer.Batches.Single().RenderTreesByComponentId[parentComponentId]
var nestedComponentFrame = renderer.Batches.Single().DiffsByComponentId[parentComponentId]
.Single()
.ReferenceFrames
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
var nestedComponent = (MessageComponent)nestedComponentFrame.Component;
var nestedComponentId = nestedComponentFrame.ComponentId;
// Act/Assert: inital render
// Assert: inital render
nestedComponent.Message = "Render 1";
renderer.RenderNewBatch(nestedComponentId);
Assert.Collection(renderer.Batches[1].RenderTreesByComponentId[nestedComponentId],
var firstDiff = renderer.Batches[1].DiffsByComponentId[nestedComponentId].Single();
Assert.Collection(firstDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
});
Assert.Collection(firstDiff.ReferenceFrames,
frame => AssertFrame.Text(frame, "Render 1"));
// Act/Assert: re-render
nestedComponent.Message = "Render 2";
renderer.RenderNewBatch(nestedComponentId);
Assert.Collection(renderer.Batches[2].RenderTreesByComponentId[nestedComponentId],
var secondDiff = renderer.Batches[2].DiffsByComponentId[nestedComponentId].Single();
Assert.Collection(firstDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
});
Assert.Collection(firstDiff.ReferenceFrames,
frame => AssertFrame.Text(frame, "Render 2"));
}
@ -129,7 +174,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
renderer.RenderNewBatch(componentId);
var (eventHandlerFrameIndex, _) = FirstWithIndex(
renderer.Batches.Single().RenderTreesByComponentId[componentId],
renderer.Batches.Single().DiffsByComponentId[componentId].Single().ReferenceFrames,
frame => frame.AttributeValue != null);
// Assert: Event not yet fired
@ -157,7 +202,9 @@ namespace Microsoft.AspNetCore.Blazor.Test
renderer.RenderNewBatch(parentComponentId);
// Arrange: Render nested component
var nestedComponentFrame = renderer.Batches.Single().RenderTreesByComponentId[parentComponentId]
var nestedComponentFrame = renderer.Batches.Single().DiffsByComponentId[parentComponentId]
.Single()
.ReferenceFrames
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
var nestedComponent = (EventComponent)nestedComponentFrame.Component;
nestedComponent.Handler = args => { receivedArgs = args; };
@ -166,7 +213,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Find nested component's event handler ndoe
var (eventHandlerFrameIndex, _) = FirstWithIndex(
renderer.Batches[1].RenderTreesByComponentId[nestedComponentId],
renderer.Batches[1].DiffsByComponentId[nestedComponentId].Single().ReferenceFrames,
frame => frame.AttributeValue != null);
// Assert: Event not yet fired
@ -217,12 +264,12 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Act/Assert: Render component in renderer1
renderer1.RenderNewBatch(renderer1ComponentId);
Assert.True(renderer1.Batches.Single().RenderTreesByComponentId.ContainsKey(renderer1ComponentId));
Assert.True(renderer1.Batches.Single().DiffsByComponentId.ContainsKey(renderer1ComponentId));
Assert.Empty(renderer2.Batches);
// Act/Assert: Render same component in renderer2
renderer2.RenderNewBatch(renderer2ComponentId);
Assert.True(renderer2.Batches.Single().RenderTreesByComponentId.ContainsKey(renderer2ComponentId));
Assert.True(renderer2.Batches.Single().DiffsByComponentId.ContainsKey(renderer2ComponentId));
}
[Fact]
@ -313,18 +360,29 @@ namespace Microsoft.AspNetCore.Blazor.Test
var rootComponentId = renderer.AssignComponentId(component);
renderer.RenderNewBatch(rootComponentId);
var nestedComponentInstance = (MessageComponent)renderer.Batches.Single().RenderTreesByComponentId[rootComponentId]
.Single(frame => frame.FrameType == RenderTreeFrameType.Component)
.Component;
var nestedComponentFrame = renderer.Batches.Single()
.DiffsByComponentId[rootComponentId]
.Single()
.ReferenceFrames
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
var nestedComponentInstance = (MessageComponent)nestedComponentFrame.Component;
// Act: Second render
message = "Modified message";
renderer.RenderNewBatch(rootComponentId);
// Assert
Assert.Collection(renderer.Batches[1].RenderTreesByComponentId[rootComponentId],
frame => AssertFrame.Text(frame, "Modified message"),
frame => Assert.Same(nestedComponentInstance, frame.Component));
var batch = renderer.Batches[1];
var diff = batch.DiffsByComponentId[rootComponentId].Single();
Assert.Collection(diff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
});
Assert.Collection(diff.ReferenceFrames,
frame => AssertFrame.Text(frame, "Modified message"));
Assert.False(batch.DiffsByComponentId.ContainsKey(nestedComponentFrame.ComponentId));
}
[Fact]
@ -346,28 +404,25 @@ namespace Microsoft.AspNetCore.Blazor.Test
var rootComponentId = renderer.AssignComponentId(component);
renderer.RenderNewBatch(rootComponentId);
var originalComponentInstance = (FakeComponent)renderer.Batches.Single().RenderTreesByComponentId[rootComponentId]
.Single(frame => frame.FrameType == RenderTreeFrameType.Component)
.Component;
var originalComponentFrame = renderer.Batches.Single().DiffsByComponentId[rootComponentId]
.Single()
.ReferenceFrames
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
var childComponentInstance = (FakeComponent)originalComponentFrame.Component;
// Assert 1: properties were assigned
Assert.Equal(123, originalComponentInstance.IntProperty);
Assert.Equal("String that will change", originalComponentInstance.StringProperty);
Assert.Same(objectThatWillNotChange, originalComponentInstance.ObjectProperty);
Assert.Equal(123, childComponentInstance.IntProperty);
Assert.Equal("String that will change", childComponentInstance.StringProperty);
Assert.Same(objectThatWillNotChange, childComponentInstance.ObjectProperty);
// Act: Second render
firstRender = false;
renderer.RenderNewBatch(rootComponentId);
var updatedComponentInstance = (FakeComponent)renderer.Batches[1].RenderTreesByComponentId[rootComponentId]
.Single(frame => frame.FrameType == RenderTreeFrameType.Component)
.Component;
// Assert
Assert.Same(originalComponentInstance, updatedComponentInstance);
Assert.Equal(256, updatedComponentInstance.IntProperty);
Assert.Equal("String that did change", updatedComponentInstance.StringProperty);
Assert.Same(objectThatWillNotChange, updatedComponentInstance.ObjectProperty);
Assert.Equal(256, childComponentInstance.IntProperty);
Assert.Equal("String that did change", childComponentInstance.StringProperty);
Assert.Same(objectThatWillNotChange, childComponentInstance.ObjectProperty);
}
[Fact]
@ -386,19 +441,25 @@ namespace Microsoft.AspNetCore.Blazor.Test
var rootComponentId = renderer.AssignComponentId(component);
renderer.RenderNewBatch(rootComponentId);
var childComponentId = renderer.Batches.Single().RenderTreesByComponentId[rootComponentId]
var childComponentId = renderer.Batches.Single().DiffsByComponentId[rootComponentId]
.Single()
.ReferenceFrames
.Single(frame => frame.FrameType == RenderTreeFrameType.Component)
.ComponentId;
// Act: Second render
firstRender = false;
renderer.RenderNewBatch(rootComponentId);
var updatedComponentFrame = renderer.Batches[1].RenderTreesByComponentId[rootComponentId]
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
var diff = renderer.Batches[1].DiffsByComponentId[childComponentId].Single();
// Assert
Assert.Collection(renderer.Batches[1].RenderTreesByComponentId[updatedComponentFrame.ComponentId],
Assert.Collection(diff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
});
Assert.Collection(diff.ReferenceFrames,
frame => AssertFrame.Text(frame, "second"));
}
@ -427,7 +488,9 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Act/Assert 1: First render, capturing child component IDs
renderer.RenderNewBatch(rootComponentId);
var childComponentIds = renderer.Batches.Single().RenderTreesByComponentId[rootComponentId]
var childComponentIds = renderer.Batches.Single().DiffsByComponentId[rootComponentId]
.Single()
.ReferenceFrames
.Where(frame => frame.FrameType == RenderTreeFrameType.Component)
.Select(frame => frame.ComponentId)
.ToList();
@ -476,8 +539,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
for (var i = 0; i < renderBatch.UpdatedComponents.Count; i++)
{
ref var renderTreeDiff = ref renderBatch.UpdatedComponents.Array[i];
capturedBatch.RenderTreesByComponentId[renderTreeDiff.ComponentId] =
renderTreeDiff.CurrentState.ToArray();
capturedBatch.AddDiff(renderTreeDiff);
}
capturedBatch.DisposedComponentIDs = renderBatch.DisposedComponentIDs.ToList();
@ -486,10 +548,20 @@ namespace Microsoft.AspNetCore.Blazor.Test
private class CapturedBatch
{
public IDictionary<int, RenderTreeFrame[]> RenderTreesByComponentId { get; }
= new Dictionary<int, RenderTreeFrame[]>();
public IDictionary<int, List<RenderTreeDiff>> DiffsByComponentId { get; }
= new Dictionary<int, List<RenderTreeDiff>>();
public IList<int> DisposedComponentIDs { get; set; }
internal void AddDiff(RenderTreeDiff diff)
{
var componentId = diff.ComponentId;
if (!DiffsByComponentId.ContainsKey(componentId))
{
DiffsByComponentId.Add(componentId, new List<RenderTreeDiff>());
}
DiffsByComponentId[componentId].Add(diff);
}
}
private class TestComponent : IComponent

View File

@ -60,6 +60,13 @@ namespace Microsoft.AspNetCore.Blazor.Test.Shared
AssertFrame.Sequence(frame, sequence);
}
public static void ComponentWithInstance<T>(RenderTreeFrame frame, int componentId, int? sequence = null) where T : IComponent
{
AssertFrame.Component<T>(frame, sequence);
Assert.IsType<T>(frame.Component);
Assert.Equal(componentId, frame.ComponentId);
}
public static void Whitespace(RenderTreeFrame frame, int? sequence = null)
{
Assert.Equal(RenderTreeFrameType.Text, frame.FrameType);

View File

@ -1,6 +1,6 @@
@using System.Collections.Generic
@using Microsoft.AspNetCore.Blazor.RenderTree
Type here: <input onkeypress=@OnKeyPressed />
Type here: <input onkeypress=@CreateNewDelegateInstance() />
<ul>
@foreach (var key in keysPressed)
{
@ -11,7 +11,22 @@ Type here: <input onkeypress=@OnKeyPressed />
@functions {
List<string> keysPressed = new List<string>();
void OnKeyPressed(UIEventArgs eventArgs)
// TODO: Fix this
// Currently, you can only trigger an event handler whose value changed in the most
// recent render cycle. That's because we reference the event handlers by their index
// into the current diff's ReferenceFrames array. We need some better mechanism of
// locating the delegates that is independent of whether the corresponding attribute
// changed in the last diff, and not assuming the attribute in the original render
// tree is still at the same index.
// Once that's fixed, remove the 'CreateNewDelegateInstance' method entirely and
// the 'irrelevantObject' arg from below, and simplify to onkeypress=@OnKeyPressed
UIEventHandler CreateNewDelegateInstance()
{
var irrelevantObject = new object();
return args => OnKeyPressed(args, irrelevantObject);
}
void OnKeyPressed(UIEventArgs eventArgs, object irrelevantObject)
{
keysPressed.Add(((UIKeyboardEventArgs)eventArgs).Key);
}