Major refactor of responsibilities in rendering code. Not quite done

with this yet either.
This commit is contained in:
Steve Sanderson 2018-02-09 12:20:09 +00:00
parent 91314ee8c8
commit 15ddcd03b0
12 changed files with 434 additions and 419 deletions

View File

@ -15,31 +15,32 @@ export class BrowserRenderer {
this.childComponentLocations[componentId] = element;
}
public updateComponent(componentId: number, edits: System_Array<RenderTreeEditPointer>, editsLength: number, referenceTree: System_Array<RenderTreeFramePointer>) {
public updateComponent(componentId: number, edits: System_Array<RenderTreeEditPointer>, editsOffset: number, editsLength: number, referenceFrames: System_Array<RenderTreeFramePointer>) {
const element = this.childComponentLocations[componentId];
if (!element) {
throw new Error(`No element is currently associated with component ${componentId}`);
}
this.applyEdits(componentId, element, 0, edits, editsLength, referenceTree);
this.applyEdits(componentId, element, 0, edits, editsOffset, editsLength, referenceFrames);
}
public disposeComponent(componentId: number) {
delete this.childComponentLocations[componentId];
}
applyEdits(componentId: number, parent: Element, childIndex: number, edits: System_Array<RenderTreeEditPointer>, editsLength: number, referenceTree: System_Array<RenderTreeFramePointer>) {
applyEdits(componentId: number, parent: Element, childIndex: number, edits: System_Array<RenderTreeEditPointer>, editsOffset: number, editsLength: number, referenceFrames: System_Array<RenderTreeFramePointer>) {
let currentDepth = 0;
let childIndexAtCurrentDepth = childIndex;
for (let editIndex = 0; editIndex < editsLength; editIndex++) {
const maxEditIndexExcl = editsOffset + editsLength;
for (let editIndex = editsOffset; editIndex < maxEditIndexExcl; editIndex++) {
const edit = getRenderTreeEditPtr(edits, editIndex);
const editType = renderTreeEdit.type(edit);
switch (editType) {
case EditType.prependFrame: {
const frameIndex = renderTreeEdit.newTreeIndex(edit);
const frame = getTreeFramePtr(referenceTree, frameIndex);
const frame = getTreeFramePtr(referenceFrames, frameIndex);
const siblingIndex = renderTreeEdit.siblingIndex(edit);
this.insertFrame(componentId, parent, childIndexAtCurrentDepth + siblingIndex, referenceTree, frame, frameIndex);
this.insertFrame(componentId, parent, childIndexAtCurrentDepth + siblingIndex, referenceFrames, frame, frameIndex);
break;
}
case EditType.removeFrame: {
@ -49,7 +50,7 @@ export class BrowserRenderer {
}
case EditType.setAttribute: {
const frameIndex = renderTreeEdit.newTreeIndex(edit);
const frame = getTreeFramePtr(referenceTree, frameIndex);
const frame = getTreeFramePtr(referenceFrames, frameIndex);
const siblingIndex = renderTreeEdit.siblingIndex(edit);
const element = parent.childNodes[childIndexAtCurrentDepth + siblingIndex] as HTMLElement;
this.applyAttribute(componentId, element, frame);
@ -62,7 +63,7 @@ export class BrowserRenderer {
}
case EditType.updateText: {
const frameIndex = renderTreeEdit.newTreeIndex(edit);
const frame = getTreeFramePtr(referenceTree, frameIndex);
const frame = getTreeFramePtr(referenceFrames, frameIndex);
const siblingIndex = renderTreeEdit.siblingIndex(edit);
const domTextNode = parent.childNodes[childIndexAtCurrentDepth + siblingIndex] as Text;
domTextNode.textContent = renderTreeFrame.textContent(frame);

View File

@ -7,7 +7,8 @@ import { RenderTreeEditPointer } from './RenderTreeEdit';
export const renderBatch = {
updatedComponents: (obj: RenderBatchPointer) => platform.readStructField<ArrayRangePointer<RenderTreeDiffPointer>>(obj, 0),
disposedComponentIds: (obj: RenderBatchPointer) => platform.readStructField<ArrayRangePointer<number>>(obj, arrayRangeStructLength),
referenceFrames: (obj: RenderBatchPointer) => platform.readStructField<ArrayRangePointer<RenderTreeFramePointer>>(obj, arrayRangeStructLength),
disposedComponentIds: (obj: RenderBatchPointer) => platform.readStructField<ArrayRangePointer<number>>(obj, arrayRangeStructLength + arrayRangeStructLength),
};
const arrayRangeStructLength = 8;
@ -16,15 +17,22 @@ export const arrayRange = {
count: <T>(obj: ArrayRangePointer<T>) => platform.readInt32Field(obj, 4),
};
export const renderTreeDiffStructLength = 4 + 2 * arrayRangeStructLength;
const arraySegmentStructLength = 12;
export const arraySegment = {
array: <T>(obj: ArraySegmentPointer<T>) => platform.readObjectField<System_Array<T>>(obj, 0),
offset: <T>(obj: ArraySegmentPointer<T>) => platform.readInt32Field(obj, 4),
count: <T>(obj: ArraySegmentPointer<T>) => platform.readInt32Field(obj, 8),
};
export const renderTreeDiffStructLength = 4 + arraySegmentStructLength;
export const renderTreeDiff = {
componentId: (obj: RenderTreeDiffPointer) => platform.readInt32Field(obj, 0),
edits: (obj: RenderTreeDiffPointer) => platform.readStructField<ArrayRangePointer<RenderTreeEditPointer>>(obj, 4),
currentState: (obj: RenderTreeDiffPointer) => platform.readStructField<ArrayRangePointer<RenderTreeFramePointer>>(obj, 4 + arrayRangeStructLength),
edits: (obj: RenderTreeDiffPointer) => platform.readStructField<ArraySegmentPointer<RenderTreeEditPointer>>(obj, 4),
};
// Nominal types to ensure only valid pointers are passed to the functions above.
// At runtime the values are just numbers.
export interface RenderBatchPointer extends Pointer { RenderBatchPointer__DO_NOT_IMPLEMENT: any }
export interface ArrayRangePointer<T> extends Pointer { ArrayRangePointer__DO_NOT_IMPLEMENT: any }
export interface ArraySegmentPointer<T> extends Pointer { ArraySegmentPointer__DO_NOT_IMPLEMENT: any }
export interface RenderTreeDiffPointer extends Pointer { RenderTreeDiffPointer__DO_NOT_IMPLEMENT: any }

View File

@ -1,6 +1,6 @@
import { System_Object, System_String, System_Array, MethodHandle, Pointer } from '../Platform/Platform';
import { platform } from '../Environment';
import { renderBatch as renderBatchStruct, arrayRange, renderTreeDiffStructLength, renderTreeDiff, RenderBatchPointer, RenderTreeDiffPointer } from './RenderBatch';
import { renderBatch as renderBatchStruct, arrayRange, arraySegment, renderTreeDiffStructLength, renderTreeDiff, RenderBatchPointer, RenderTreeDiffPointer } from './RenderBatch';
import { BrowserRenderer } from './BrowserRenderer';
type BrowserRendererRegistry = { [browserRendererId: number]: BrowserRenderer };
@ -30,17 +30,19 @@ export function renderBatch(browserRendererId: number, batch: RenderBatchPointer
const updatedComponents = renderBatchStruct.updatedComponents(batch);
const updatedComponentsLength = arrayRange.count(updatedComponents);
const updatedComponentsArray = arrayRange.array(updatedComponents);
const referenceFramesStruct = renderBatchStruct.referenceFrames(batch);
const referenceFrames = arrayRange.array(referenceFramesStruct);
for (let i = 0; i < updatedComponentsLength; i++) {
const diff = platform.getArrayEntryPtr(updatedComponentsArray, i, renderTreeDiffStructLength);
const componentId = renderTreeDiff.componentId(diff);
const editsArrayRange = renderTreeDiff.edits(diff);
const currentStateArrayRange = renderTreeDiff.currentState(diff);
const editsArraySegment = renderTreeDiff.edits(diff);
const edits = arraySegment.array(editsArraySegment);
const editsOffset = arraySegment.offset(editsArraySegment);
const editsLength = arraySegment.count(editsArraySegment);
const edits = arrayRange.array(editsArrayRange);
const editsLength = arrayRange.count(editsArrayRange);
const tree = arrayRange.array(currentStateArrayRange);
browserRenderer.updateComponent(componentId, edits, editsLength, tree);
browserRenderer.updateComponent(componentId, edits, editsOffset, editsLength, referenceFrames);
}
const disposedComponentIds = renderBatchStruct.disposedComponentIds(batch);

View File

@ -132,6 +132,15 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
public ArrayRange<T> ToRange()
=> new ArrayRange<T>(_items, _itemsInUse);
/// <summary>
/// Produces an <see cref="ArraySegment{T}"/> structure describing the selected contents.
/// </summary>
/// <param name="fromIndexInclusive">The index of the first item in the segment.</param>
/// <param name="toIndexExclusive">One plus the index of the last item in the segment.</param>
/// <returns>The <see cref="ArraySegment{T}"/>.</returns>
public ArraySegment<T> ToSegment(int fromIndexInclusive, int toIndexExclusive)
=> new ArraySegment<T>(_items, fromIndexInclusive, toIndexExclusive - fromIndexInclusive);
private void SetCapacity(int desiredCapacity, bool preserveContents)
{
if (desiredCapacity < _itemsInUse)

View File

@ -1,6 +1,8 @@
// 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>
@ -16,23 +18,14 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
/// <summary>
/// Gets the changes to the render tree since a previous state.
/// </summary>
public readonly ArrayRange<RenderTreeEdit> Edits;
/// <summary>
/// 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 readonly ArrayRange<RenderTreeFrame> ReferenceFrames;
public readonly ArraySegment<RenderTreeEdit> Edits;
internal RenderTreeDiff(
int componentId,
ArrayRange<RenderTreeEdit> entries,
ArrayRange<RenderTreeFrame> referenceFrames)
ArraySegment<RenderTreeEdit> entries)
{
ComponentId = componentId;
Edits = entries;
ReferenceFrames = referenceFrames;
}
}
}

View File

@ -8,50 +8,69 @@ using Microsoft.AspNetCore.Blazor.Rendering;
namespace Microsoft.AspNetCore.Blazor.RenderTree
{
internal class RenderTreeDiffBuilder
internal static class RenderTreeDiffBuilder
{
private readonly Renderer _renderer;
private readonly ArrayBuilder<RenderTreeEdit> _entries = new ArrayBuilder<RenderTreeEdit>(10);
private readonly ArrayBuilder<RenderTreeFrame> _referenceFrames = new ArrayBuilder<RenderTreeFrame>(10);
public RenderTreeDiffBuilder(Renderer renderer)
private struct DiffContext
{
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
// Exists only so that the various methods in this class can call each other without
// constantly building up long lists of parameters. Is private to this class, so the
// fact that it's a mutable struct is manageable.
// Always pass by ref to avoid copying, and because the 'SiblingIndex' is mutable.
public readonly Renderer Renderer;
public readonly RenderBatchBuilder BatchBuilder;
public readonly RenderTreeFrame[] OldTree;
public readonly RenderTreeFrame[] NewTree;
public readonly ArrayBuilder<RenderTreeEdit> Edits;
public readonly ArrayBuilder<RenderTreeFrame> ReferenceFrames;
public int SiblingIndex;
public DiffContext(
Renderer renderer,
RenderBatchBuilder batchBuilder,
RenderTreeFrame[] oldTree, RenderTreeFrame[] newTree)
{
Renderer = renderer;
BatchBuilder = batchBuilder;
OldTree = oldTree;
NewTree = newTree;
Edits = batchBuilder.EditsBuffer;
ReferenceFrames = batchBuilder.ReferenceFramesBuffer;
SiblingIndex = 0;
}
}
/// <summary>
/// As well as computing the diff between the two trees, this method also has the side-effect
/// of instantiating child components on newly-inserted Component frames, and copying the existing
/// component instances onto retained Component frames. 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 void ApplyNewRenderTreeVersion(
public static RenderTreeDiff ComputeDiff(
Renderer renderer,
RenderBatchBuilder batchBuilder,
int componentId,
ArrayRange<RenderTreeFrame> oldTree,
ArrayRange<RenderTreeFrame> newTree)
{
_entries.Clear();
_referenceFrames.Clear();
var siblingIndex = 0;
var editsBuffer = batchBuilder.EditsBuffer;
var editsBufferStartLength = editsBuffer.Count;
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(), _referenceFrames.ToRange()));
var diffContext = new DiffContext(renderer, batchBuilder, oldTree.Array, newTree.Array);
AppendDiffEntriesForRange(ref diffContext, 0, oldTree.Count, 0, newTree.Count);
var editsSegment = editsBuffer.ToSegment(editsBufferStartLength, editsBuffer.Count);
return new RenderTreeDiff(componentId, editsSegment);
}
private void AppendDiffEntriesForRange(
RenderBatchBuilder batchBuilder,
RenderTreeFrame[] oldTree, int oldStartIndex, int oldEndIndexExcl,
RenderTreeFrame[] newTree, int newStartIndex, int newEndIndexExcl,
ref int siblingIndex)
public static void DisposeFrames(RenderBatchBuilder batchBuilder, ArrayRange<RenderTreeFrame> frames)
=> DisposeFramesInRange(batchBuilder, frames.Array, 0, frames.Count);
private static void AppendDiffEntriesForRange(
ref DiffContext diffContext,
int oldStartIndex, int oldEndIndexExcl,
int newStartIndex, int newEndIndexExcl)
{
var hasMoreOld = oldEndIndexExcl > oldStartIndex;
var hasMoreNew = newEndIndexExcl > newStartIndex;
var prevOldSeq = -1;
var prevNewSeq = -1;
var oldTree = diffContext.OldTree;
var newTree = diffContext.NewTree;
while (hasMoreOld || hasMoreNew)
{
var oldSeq = hasMoreOld ? oldTree[oldStartIndex].Sequence : int.MaxValue;
@ -59,7 +78,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
if (oldSeq == newSeq)
{
AppendDiffEntriesForFramesWithSameSequence(batchBuilder, oldTree, oldStartIndex, newTree, newStartIndex, ref siblingIndex);
AppendDiffEntriesForFramesWithSameSequence(ref diffContext, oldStartIndex, newStartIndex);
oldStartIndex = NextSiblingIndex(oldTree[oldStartIndex], oldStartIndex);
newStartIndex = NextSiblingIndex(newTree[newStartIndex], newStartIndex);
hasMoreOld = oldEndIndexExcl > oldStartIndex;
@ -134,14 +153,14 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
if (treatAsInsert)
{
InsertNewFrame(batchBuilder, newTree, newStartIndex, ref siblingIndex);
InsertNewFrame(ref diffContext, newStartIndex);
newStartIndex = NextSiblingIndex(newTree[newStartIndex], newStartIndex);
hasMoreNew = newEndIndexExcl > newStartIndex;
prevNewSeq = newSeq;
}
else
{
RemoveOldFrame(batchBuilder, oldTree, oldStartIndex, siblingIndex);
RemoveOldFrame(ref diffContext, oldStartIndex);
oldStartIndex = NextSiblingIndex(oldTree[oldStartIndex], oldStartIndex);
hasMoreOld = oldEndIndexExcl > oldStartIndex;
prevOldSeq = oldSeq;
@ -150,16 +169,18 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
}
}
private void UpdateRetainedChildComponent(
RenderBatchBuilder batchBuilder,
RenderTreeFrame[] oldTree, int oldComponentIndex,
RenderTreeFrame[] newTree, int newComponentIndex)
private static void UpdateRetainedChildComponent(
ref DiffContext diffContext,
int oldComponentIndex,
int newComponentIndex)
{
// The algorithm here is the same as in AppendDiffEntriesForRange, except that
// here we don't optimise for loops - we assume that both sequences are forward-only.
// That's because this is true for all currently supported scenarios, and it means
// fewer steps here.
var oldTree = diffContext.OldTree;
var newTree = diffContext.NewTree;
ref var oldComponentFrame = ref oldTree[oldComponentIndex];
ref var newComponentFrame = ref newTree[newComponentIndex];
var componentId = oldComponentFrame.ComponentId;
@ -241,7 +262,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
if (hasSetAnyProperty)
{
TriggerChildComponentRender(batchBuilder, newComponentFrame);
diffContext.BatchBuilder.ComponentRenderQueue.Enqueue(newComponentFrame.ComponentId);
}
}
@ -291,12 +312,13 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
return frameIndex + distanceToNextSibling;
}
private void AppendDiffEntriesForFramesWithSameSequence(
RenderBatchBuilder batchBuilder,
RenderTreeFrame[] oldTree, int oldFrameIndex,
RenderTreeFrame[] newTree, int newFrameIndex,
ref int siblingIndex)
private static void AppendDiffEntriesForFramesWithSameSequence(
ref DiffContext diffContext,
int oldFrameIndex,
int newFrameIndex)
{
var oldTree = diffContext.OldTree;
var newTree = diffContext.NewTree;
ref var oldFrame = ref oldTree[oldFrameIndex];
ref var newFrame = ref newTree[newFrameIndex];
@ -304,7 +326,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
// to the same sequence number (and if not, the behaviour is undefined).
// TODO: Consider supporting dissimilar types at same sequence for custom IComponent implementations.
// It should only be a matter of calling RemoveOldFrame+InsertNewFrame
switch (newTree[newFrameIndex].FrameType)
switch (newFrame.FrameType)
{
case RenderTreeFrameType.Text:
{
@ -312,10 +334,10 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
var newText = newFrame.TextContent;
if (!string.Equals(oldText, newText, StringComparison.Ordinal))
{
var referenceFrameIndex = _referenceFrames.Append(newFrame);
_entries.Append(RenderTreeEdit.UpdateText(siblingIndex, referenceFrameIndex));
var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame);
diffContext.Edits.Append(RenderTreeEdit.UpdateText(diffContext.SiblingIndex, referenceFrameIndex));
}
siblingIndex++;
diffContext.SiblingIndex++;
break;
}
@ -330,10 +352,9 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
// Diff the attributes
AppendDiffEntriesForRange(
batchBuilder,
oldTree, oldFrameIndex + 1, oldFrameAttributesEndIndexExcl,
newTree, newFrameIndex + 1, newFrameAttributesEndIndexExcl,
ref siblingIndex);
ref diffContext,
oldFrameIndex + 1, oldFrameAttributesEndIndexExcl,
newFrameIndex + 1, newFrameAttributesEndIndexExcl);
// Diff the children
var oldFrameChildrenEndIndexExcl = oldFrameIndex + oldFrame.ElementSubtreeLength;
@ -343,26 +364,26 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
newFrameChildrenEndIndexExcl > newFrameAttributesEndIndexExcl;
if (hasChildrenToProcess)
{
_entries.Append(RenderTreeEdit.StepIn(siblingIndex));
var childSiblingIndex = 0;
diffContext.Edits.Append(RenderTreeEdit.StepIn(diffContext.SiblingIndex));
var prevSiblingIndex = diffContext.SiblingIndex;
diffContext.SiblingIndex = 0;
AppendDiffEntriesForRange(
batchBuilder,
oldTree, oldFrameAttributesEndIndexExcl, oldFrameChildrenEndIndexExcl,
newTree, newFrameAttributesEndIndexExcl, newFrameChildrenEndIndexExcl,
ref childSiblingIndex);
AppendStepOut();
siblingIndex++;
ref diffContext,
oldFrameAttributesEndIndexExcl, oldFrameChildrenEndIndexExcl,
newFrameAttributesEndIndexExcl, newFrameChildrenEndIndexExcl);
AppendStepOut(ref diffContext);
diffContext.SiblingIndex = prevSiblingIndex + 1;
}
else
{
siblingIndex++;
diffContext.SiblingIndex++;
}
}
else
{
// Elements with different names are treated as completely unrelated
RemoveOldFrame(batchBuilder, oldTree, oldFrameIndex, siblingIndex);
InsertNewFrame(batchBuilder, newTree, newFrameIndex, ref siblingIndex);
RemoveOldFrame(ref diffContext, oldFrameIndex);
InsertNewFrame(ref diffContext, newFrameIndex);
}
break;
}
@ -372,16 +393,16 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
if (oldFrame.ComponentType == newFrame.ComponentType)
{
UpdateRetainedChildComponent(
batchBuilder,
oldTree, oldFrameIndex,
newTree, newFrameIndex);
siblingIndex++;
ref diffContext,
oldFrameIndex,
newFrameIndex);
diffContext.SiblingIndex++;
}
else
{
// Child components of different types are treated as completely unrelated
RemoveOldFrame(batchBuilder, oldTree, oldFrameIndex, siblingIndex);
InsertNewFrame(batchBuilder, newTree, newFrameIndex, ref siblingIndex);
RemoveOldFrame(ref diffContext, oldFrameIndex);
InsertNewFrame(ref diffContext, newFrameIndex);
}
break;
}
@ -398,11 +419,11 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
{
if (oldFrame.AttributeEventHandlerId > 0)
{
batchBuilder.AddDisposedEventHandlerId(oldFrame.AttributeEventHandlerId);
diffContext.BatchBuilder.AddDisposedEventHandlerId(oldFrame.AttributeEventHandlerId);
}
InitializeNewAttributeFrame(ref newFrame);
var referenceFrameIndex = _referenceFrames.Append(newFrame);
_entries.Append(RenderTreeEdit.SetAttribute(siblingIndex, referenceFrameIndex));
InitializeNewAttributeFrame(ref diffContext, ref newFrame);
var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame);
diffContext.Edits.Append(RenderTreeEdit.SetAttribute(diffContext.SiblingIndex, referenceFrameIndex));
}
else if (oldFrame.AttributeEventHandlerId > 0)
{
@ -415,8 +436,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
RemoveOldFrame(batchBuilder, oldTree, oldFrameIndex, siblingIndex);
InsertNewFrame(batchBuilder, newTree, newFrameIndex, ref siblingIndex);
RemoveOldFrame(ref diffContext, oldFrameIndex);
InsertNewFrame(ref diffContext, newFrameIndex);
}
break;
}
@ -426,48 +447,50 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
}
}
private void InsertNewFrame(RenderBatchBuilder batchBuilder, RenderTreeFrame[] newTree, int newFrameIndex, ref int siblingIndex)
private static void InsertNewFrame(ref DiffContext diffContext, int newFrameIndex)
{
var newTree = diffContext.NewTree;
ref var newFrame = ref newTree[newFrameIndex];
switch (newFrame.FrameType)
{
case RenderTreeFrameType.Attribute:
{
InitializeNewAttributeFrame(ref newFrame);
var referenceFrameIndex = _referenceFrames.Append(newFrame);
_entries.Append(RenderTreeEdit.SetAttribute(siblingIndex, referenceFrameIndex));
InitializeNewAttributeFrame(ref diffContext, ref newFrame);
var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame);
diffContext.Edits.Append(RenderTreeEdit.SetAttribute(diffContext.SiblingIndex, referenceFrameIndex));
break;
}
case RenderTreeFrameType.Component:
case RenderTreeFrameType.Element:
{
InitializeNewSubtree(batchBuilder, newTree, newFrameIndex);
var referenceFrameIndex = _referenceFrames.Append(newTree, newFrameIndex, newFrame.ElementSubtreeLength);
_entries.Append(RenderTreeEdit.PrependFrame(siblingIndex, referenceFrameIndex));
siblingIndex++;
InitializeNewSubtree(ref diffContext, newFrameIndex);
var referenceFrameIndex = diffContext.ReferenceFrames.Append(newTree, newFrameIndex, newFrame.ElementSubtreeLength);
diffContext.Edits.Append(RenderTreeEdit.PrependFrame(diffContext.SiblingIndex, referenceFrameIndex));
diffContext.SiblingIndex++;
break;
}
case RenderTreeFrameType.Text:
{
var referenceFrameIndex = _referenceFrames.Append(newFrame);
_entries.Append(RenderTreeEdit.PrependFrame(siblingIndex, referenceFrameIndex));
siblingIndex++;
var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame);
diffContext.Edits.Append(RenderTreeEdit.PrependFrame(diffContext.SiblingIndex, referenceFrameIndex));
diffContext.SiblingIndex++;
break;
}
}
}
private void RemoveOldFrame(RenderBatchBuilder batchBuilder, RenderTreeFrame[] oldTree, int oldFrameIndex, int siblingIndex)
private static void RemoveOldFrame(ref DiffContext diffContext, int oldFrameIndex)
{
var oldTree = diffContext.OldTree;
ref var oldFrame = ref oldTree[oldFrameIndex];
switch (oldFrame.FrameType)
{
case RenderTreeFrameType.Attribute:
{
_entries.Append(RenderTreeEdit.RemoveAttribute(siblingIndex, oldFrame.AttributeName));
diffContext.Edits.Append(RenderTreeEdit.RemoveAttribute(diffContext.SiblingIndex, oldFrame.AttributeName));
if (oldFrame.AttributeEventHandlerId > 0)
{
batchBuilder.AddDisposedEventHandlerId(oldFrame.AttributeEventHandlerId);
diffContext.BatchBuilder.AddDisposedEventHandlerId(oldFrame.AttributeEventHandlerId);
}
break;
}
@ -475,13 +498,13 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
case RenderTreeFrameType.Element:
{
var endIndexExcl = oldFrameIndex + oldFrame.ElementSubtreeLength;
DisposeFramesInRange(batchBuilder, oldTree, oldFrameIndex, endIndexExcl);
_entries.Append(RenderTreeEdit.RemoveFrame(siblingIndex));
DisposeFramesInRange(diffContext.BatchBuilder, oldTree, oldFrameIndex, endIndexExcl);
diffContext.Edits.Append(RenderTreeEdit.RemoveFrame(diffContext.SiblingIndex));
break;
}
case RenderTreeFrameType.Text:
{
_entries.Append(RenderTreeEdit.RemoveFrame(siblingIndex));
diffContext.Edits.Append(RenderTreeEdit.RemoveFrame(diffContext.SiblingIndex));
break;
}
}
@ -502,22 +525,23 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
return index;
}
private void AppendStepOut()
private static void AppendStepOut(ref DiffContext diffContext)
{
// If the preceding frame is a StepIn, then the StepOut cancels it out
var previousIndex = _entries.Count - 1;
if (previousIndex >= 0 && _entries.Buffer[previousIndex].Type == RenderTreeEditType.StepIn)
var previousIndex = diffContext.Edits.Count - 1;
if (previousIndex >= 0 && diffContext.Edits.Buffer[previousIndex].Type == RenderTreeEditType.StepIn)
{
_entries.RemoveLast();
diffContext.Edits.RemoveLast();
}
else
{
_entries.Append(RenderTreeEdit.StepOut());
diffContext.Edits.Append(RenderTreeEdit.StepOut());
}
}
private void InitializeNewSubtree(RenderBatchBuilder batchBuilder, RenderTreeFrame[] frames, int frameIndex)
private static void InitializeNewSubtree(ref DiffContext diffContext, int frameIndex)
{
var frames = diffContext.NewTree;
var endIndexExcl = frameIndex + frames[frameIndex].ElementSubtreeLength;
for (var i = frameIndex; i < endIndexExcl; i++)
{
@ -525,17 +549,18 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
switch (frame.FrameType)
{
case RenderTreeFrameType.Component:
InitializeNewComponentFrame(batchBuilder, frames, i);
InitializeNewComponentFrame(ref diffContext, i);
break;
case RenderTreeFrameType.Attribute:
InitializeNewAttributeFrame(ref frame);
InitializeNewAttributeFrame(ref diffContext, ref frame);
break;
}
}
}
private void InitializeNewComponentFrame(RenderBatchBuilder batchBuilder, RenderTreeFrame[] frames, int frameIndex)
private static void InitializeNewComponentFrame(ref DiffContext diffContext, int frameIndex)
{
var frames = diffContext.NewTree;
ref var frame = ref frames[frameIndex];
if (frame.Component != null)
@ -543,7 +568,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
throw new InvalidOperationException($"Child component already exists during {nameof(InitializeNewComponentFrame)}");
}
_renderer.InstantiateChildComponent(ref frame);
diffContext.Renderer.InstantiateChildComponent(ref frame);
var childComponentInstance = frame.Component;
// All descendants of a component are its properties
@ -557,45 +582,25 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
attributeFrame.AttributeValue);
}
TriggerChildComponentRender(batchBuilder, frame);
diffContext.BatchBuilder.ComponentRenderQueue.Enqueue(frame.ComponentId);
}
private void InitializeNewAttributeFrame(ref RenderTreeFrame newFrame)
private static void InitializeNewAttributeFrame(ref DiffContext diffContext, ref RenderTreeFrame newFrame)
{
if (newFrame.AttributeValue is UIEventHandler)
{
_renderer.AssignEventHandlerId(ref newFrame);
diffContext.Renderer.AssignEventHandlerId(ref newFrame);
}
}
private void TriggerChildComponentRender(RenderBatchBuilder batchBuilder, in RenderTreeFrame frame)
{
if (frame.Component is IHandlePropertiesChanged notifyableComponent)
{
// TODO: Ensure any exceptions thrown here are handled equivalently to
// unhandled exceptions during rendering.
notifyableComponent.OnPropertiesChanged();
}
// TODO: Consider moving the responsibility for triggering re-rendering
// into the OnPropertiesChanged handler (if implemented) so that components
// can control whether any given set of property changes cause re-rendering.
// Not doing so yet because it's unclear that the usage patterns would be
// good to use.
_renderer.RenderInExistingBatch(batchBuilder, frame.ComponentId);
}
internal void DisposeFrames(RenderBatchBuilder batchBuilder, ArrayRange<RenderTreeFrame> frames)
=> DisposeFramesInRange(batchBuilder, frames.Array, 0, frames.Count);
private void DisposeFramesInRange(RenderBatchBuilder batchBuilder, RenderTreeFrame[] frames, int startIndex, int endIndexExcl)
private static void DisposeFramesInRange(RenderBatchBuilder batchBuilder, RenderTreeFrame[] frames, int startIndex, int endIndexExcl)
{
for (var i = startIndex; i < endIndexExcl; i++)
{
ref var frame = ref frames[i];
if (frame.FrameType == RenderTreeFrameType.Component && frame.Component != null)
{
_renderer.DisposeInExistingBatch(batchBuilder, frame.ComponentId);
batchBuilder.ComponentDisposalQueue.Enqueue(frame.ComponentId);
}
else if (frame.FrameType == RenderTreeFrameType.Attribute && frame.AttributeEventHandlerId > 0)
{

View File

@ -17,7 +17,6 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
private readonly int _componentId; // TODO: Change the type to 'long' when the Mono runtime has more complete support for passing longs in .NET->JS calls
private readonly IComponent _component;
private readonly Renderer _renderer;
private readonly RenderTreeDiffBuilder _diffBuilder;
private RenderTreeBuilder _renderTreeBuilderCurrent;
private RenderTreeBuilder _renderTreeBuilderPrevious;
@ -32,7 +31,6 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
_componentId = componentId;
_component = component ?? throw new ArgumentNullException(nameof(component));
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
_diffBuilder = new RenderTreeDiffBuilder(renderer);
_renderTreeBuilderCurrent = new RenderTreeBuilder(renderer);
_renderTreeBuilderPrevious = new RenderTreeBuilder(renderer);
}
@ -41,18 +39,33 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
/// Regenerates the <see cref="RenderTree"/> and adds the changes to the
/// <paramref name="batchBuilder"/>.
/// </summary>
public void Render(RenderBatchBuilder batchBuilder)
public void Render(Renderer renderer, RenderBatchBuilder batchBuilder)
{
if (_component is IHandlePropertiesChanged notifyableComponent)
{
notifyableComponent.OnPropertiesChanged();
}
// Swap the old and new tree builders
(_renderTreeBuilderCurrent, _renderTreeBuilderPrevious) = (_renderTreeBuilderPrevious, _renderTreeBuilderCurrent);
_renderTreeBuilderCurrent.Clear();
_component.BuildRenderTree(_renderTreeBuilderCurrent);
_diffBuilder.ApplyNewRenderTreeVersion(
var diff = RenderTreeDiffBuilder.ComputeDiff(
_renderer,
batchBuilder,
_componentId,
_renderTreeBuilderPrevious.GetFrames(),
_renderTreeBuilderCurrent.GetFrames());
batchBuilder.UpdatedComponentDiffs.Append(diff);
// Process disposal queue now in case it causes further component renders to be enqueued
while (batchBuilder.ComponentDisposalQueue.Count > 0)
{
var disposeComponentId = batchBuilder.ComponentDisposalQueue.Dequeue();
renderer.DisposeInExistingBatch(batchBuilder, disposeComponentId);
}
}
/// <summary>
@ -66,7 +79,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
disposable.Dispose();
}
_diffBuilder.DisposeFrames(batchBuilder, _renderTreeBuilderCurrent.GetFrames());
RenderTreeDiffBuilder.DisposeFrames(batchBuilder, _renderTreeBuilderCurrent.GetFrames());
}
}
}

View File

@ -15,6 +15,13 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
/// </summary>
public ArrayRange<RenderTreeDiff> UpdatedComponents { get; }
/// <summary>
/// Gets render frames that may be referenced by entries in <see cref="UpdatedComponents"/>.
/// 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> ReferenceFrames { get; }
/// <summary>
/// Gets the IDs of the components that were disposed.
/// </summary>
@ -22,9 +29,11 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
internal RenderBatch(
ArrayRange<RenderTreeDiff> updatedComponents,
ArrayRange<RenderTreeFrame> referenceFrames,
ArrayRange<int> disposedComponentIDs)
{
UpdatedComponents = updatedComponents;
ReferenceFrames = referenceFrames;
DisposedComponentIDs = disposedComponentIDs;
}
}

View File

@ -1,43 +1,47 @@
// 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;
using System.Collections.Generic;
using Microsoft.AspNetCore.Blazor.RenderTree;
namespace Microsoft.AspNetCore.Blazor.Rendering
{
internal class RenderBatchBuilder
{
private ArrayBuilder<RenderTreeDiff> _updatedComponentDiffs = new ArrayBuilder<RenderTreeDiff>();
private ArrayBuilder<int> _disposedComponentIds = new ArrayBuilder<int>();
private ArrayBuilder<int> _disposedEventHandlerIds = new ArrayBuilder<int>();
public ArrayBuilder<RenderTreeEdit> EditsBuffer { get; } = new ArrayBuilder<RenderTreeEdit>();
public ArrayBuilder<RenderTreeFrame> ReferenceFramesBuffer { get; } = new ArrayBuilder<RenderTreeFrame>();
public int ReserveUpdatedComponentSlotId()
{
int id = _updatedComponentDiffs.Count;
_updatedComponentDiffs.Append(default);
return id;
}
public Queue<int> ComponentRenderQueue { get; } = new Queue<int>();
public void SetUpdatedComponent(int updatedComponentSlotId, RenderTreeDiff diff)
=> _updatedComponentDiffs.Overwrite(updatedComponentSlotId, diff);
public Queue<int> ComponentDisposalQueue { get; } = new Queue<int>();
public ArrayBuilder<RenderTreeDiff> UpdatedComponentDiffs { get; set; }
= new ArrayBuilder<RenderTreeDiff>();
private readonly ArrayBuilder<int> _disposedComponentIds = new ArrayBuilder<int>();
private readonly ArrayBuilder<int> _disposedEventHandlerIds = new ArrayBuilder<int>();
public ArrayRange<int> GetDisposedEventHandlerIds()
=> _disposedEventHandlerIds.ToRange();
public void Clear()
{
_updatedComponentDiffs.Clear();
EditsBuffer.Clear();
ReferenceFramesBuffer.Clear();
ComponentRenderQueue.Clear();
UpdatedComponentDiffs.Clear();
_disposedComponentIds.Clear();
_disposedEventHandlerIds.Clear();
}
public RenderBatch ToBatch()
=> new RenderBatch(
_updatedComponentDiffs.ToRange(),
UpdatedComponentDiffs.ToRange(),
ReferenceFramesBuffer.ToRange(),
_disposedComponentIds.ToRange());
public void AddDisposedComponent(int componentId)
public void AddDisposedComponentId(int componentId)
=> _disposedComponentIds.Append(componentId);
public void AddDisposedEventHandlerId(int attributeEventHandlerId)

View File

@ -87,6 +87,14 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
try
{
RenderInExistingBatch(_sharedRenderBatchBuilder, componentId);
// Process
while (_sharedRenderBatchBuilder.ComponentRenderQueue.Count > 0)
{
var nextComponentIdToRender = _sharedRenderBatchBuilder.ComponentRenderQueue.Dequeue();
RenderInExistingBatch(_sharedRenderBatchBuilder, nextComponentIdToRender);
}
UpdateDisplay(_sharedRenderBatchBuilder.ToBatch());
RemoveEventHandlerIds(_sharedRenderBatchBuilder.GetDisposedEventHandlerIds());
}
@ -98,14 +106,12 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
}
internal void RenderInExistingBatch(RenderBatchBuilder batchBuilder, int componentId)
{
GetRequiredComponentState(componentId).Render(batchBuilder);
}
=> GetRequiredComponentState(componentId).Render(this, batchBuilder);
internal void DisposeInExistingBatch(RenderBatchBuilder batchBuilder, int componentId)
{
GetRequiredComponentState(componentId).NotifyDisposed(batchBuilder);
batchBuilder.AddDisposedComponent(componentId);
batchBuilder.AddDisposedComponentId(componentId);
}
/// <summary>

View File

@ -17,14 +17,12 @@ namespace Microsoft.AspNetCore.Blazor.Test
private readonly Renderer renderer;
private readonly RenderTreeBuilder oldTree;
private readonly RenderTreeBuilder newTree;
private RenderTreeDiffBuilder diff;
public RenderTreeDiffBuilderTest()
{
renderer = new FakeRenderer();
oldTree = new RenderTreeBuilder(renderer);
newTree = new RenderTreeBuilder(renderer);
diff = new RenderTreeDiffBuilder(renderer);
}
[Theory]
@ -36,7 +34,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
appendAction(newTree);
// Act
var result = GetSingleUpdatedComponent();
var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert
Assert.Empty(result.Edits);
@ -67,7 +65,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(2, "text2");
// Act
var result = GetSingleUpdatedComponent();
var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert
Assert.Collection(result.Edits,
@ -75,7 +73,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
{
AssertEdit(entry, RenderTreeEditType.PrependFrame, 1);
Assert.Equal(0, entry.ReferenceFrameIndex);
AssertFrame.Text(result.ReferenceFrames.Array[0], "text1", 1);
AssertFrame.Text(referenceFrames[0], "text1", 1);
});
}
@ -90,7 +88,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(2, "text2");
// Act
var result = GetSingleUpdatedComponent();
var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert
Assert.Collection(result.Edits,
@ -109,7 +107,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(0, "x"); // Loop start
// Act
var result = GetSingleUpdatedComponent();
var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert
Assert.Collection(result.Edits,
@ -129,7 +127,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(10, "x"); // Loop start
// Act
var result = GetSingleUpdatedComponent();
var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert
Assert.Collection(result.Edits,
@ -143,9 +141,8 @@ namespace Microsoft.AspNetCore.Blazor.Test
AssertEdit(entry, RenderTreeEditType.PrependFrame, 2);
Assert.Equal(1, entry.ReferenceFrameIndex);
});
Assert.Collection(result.ReferenceFrames,
frame => AssertFrame.Text(frame, "x", 11),
frame => AssertFrame.Text(frame, "x", 12));
AssertFrame.Text(referenceFrames[0], "x", 11);
AssertFrame.Text(referenceFrames[1], "x", 12);
}
[Fact]
@ -160,7 +157,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(1, "x");
// Act
var result = GetSingleUpdatedComponent();
var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert
Assert.Collection(result.Edits,
@ -180,7 +177,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(11, "x"); // Will be added
// Act
var result = GetSingleUpdatedComponent();
var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert
Assert.Collection(result.Edits,
@ -194,9 +191,8 @@ namespace Microsoft.AspNetCore.Blazor.Test
AssertEdit(entry, RenderTreeEditType.PrependFrame, 3);
Assert.Equal(1, entry.ReferenceFrameIndex);
});
Assert.Collection(result.ReferenceFrames,
frame => AssertFrame.Text(frame, "x", 10),
frame => AssertFrame.Text(frame, "x", 11));
AssertFrame.Text(referenceFrames[0], "x", 10);
AssertFrame.Text(referenceFrames[1], "x", 11);
}
[Fact]
@ -211,7 +207,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(12, "x");
// Act
var result = GetSingleUpdatedComponent();
var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert
Assert.Collection(result.Edits,
@ -225,9 +221,8 @@ namespace Microsoft.AspNetCore.Blazor.Test
AssertEdit(entry, RenderTreeEditType.PrependFrame, 2);
Assert.Equal(1, entry.ReferenceFrameIndex);
});
Assert.Collection(result.ReferenceFrames,
frame => AssertFrame.Text(frame, "x", 10),
frame => AssertFrame.Text(frame, "x", 11));
AssertFrame.Text(referenceFrames[0], "x", 10);
AssertFrame.Text(referenceFrames[1], "x", 11);
}
[Fact]
@ -242,7 +237,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 = GetSingleUpdatedComponent();
var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert
Assert.Collection(result.Edits,
@ -258,7 +253,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(1, "text");
// Act
var result = GetSingleUpdatedComponent();
var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert
Assert.Collection(result.Edits,
@ -276,7 +271,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(182, "new text 2");
// Act
var result = GetSingleUpdatedComponent();
var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert
Assert.Collection(result.Edits,
@ -306,7 +301,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement();
// Act
var result = GetSingleUpdatedComponent();
var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert
Assert.Collection(result.Edits,
@ -327,34 +322,22 @@ namespace Microsoft.AspNetCore.Blazor.Test
GetRenderedBatch(new RenderTreeBuilder(renderer), oldTree); // Assign initial IDs
newTree.OpenComponent<FakeComponent2>(123);
newTree.CloseComponent();
var batchBuilder = new RenderBatchBuilder();
// Act
var renderBatch = GetRenderedBatch();
var diff = RenderTreeDiffBuilder.ComputeDiff(renderer, batchBuilder, 0, oldTree.GetFrames(), newTree.GetFrames());
// Assert
Assert.Collection(renderBatch.DisposedComponentIDs,
disposedComponentId => Assert.Equal(0, disposedComponentId));
// Assert: We're going to dispose the old component, and render the new one
Assert.Equal(new[] { 0 }, batchBuilder.ComponentDisposalQueue);
Assert.Equal(new[] { 1 }, batchBuilder.ComponentRenderQueue);
// Assert: First updated component is the root with one child being
// prepended, and its earlier incarnation being removed
Assert.Equal(2, renderBatch.UpdatedComponents.Count);
var updatedComponent1 = renderBatch.UpdatedComponents.Array[0];
Assert.Collection(updatedComponent1.Edits,
// Assert: Got correct info in diff
Assert.Collection(diff.Edits,
entry => AssertEdit(entry, RenderTreeEditType.RemoveFrame, 0),
entry =>
{
AssertEdit(entry, RenderTreeEditType.PrependFrame, 0);
Assert.Equal(0, entry.ReferenceFrameIndex);
Assert.IsType<FakeComponent2>(updatedComponent1.ReferenceFrames.Array[0].Component);
});
// Assert: Second updated component is the new FakeComponent2
var updatedComponent2 = renderBatch.UpdatedComponents.Array[1];
Assert.Collection(updatedComponent2.Edits,
entry =>
{
AssertEdit(entry, RenderTreeEditType.PrependFrame, 0);
Assert.Equal(0, entry.ReferenceFrameIndex);
Assert.IsType<FakeComponent2>(batchBuilder.ReferenceFramesBuffer.Buffer[entry.ReferenceFrameIndex].Component);
});
}
@ -371,7 +354,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement();
// Act
var result = GetSingleUpdatedComponent();
var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert
Assert.Collection(result.Edits,
@ -380,8 +363,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
AssertEdit(entry, RenderTreeEditType.SetAttribute, 0);
Assert.Equal(0, entry.ReferenceFrameIndex);
});
Assert.Collection(result.ReferenceFrames,
frame => AssertFrame.Attribute(frame, "added", "added value"));
AssertFrame.Attribute(referenceFrames[0], "added", "added value");
}
[Fact]
@ -397,7 +379,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement();
// Act
var result = GetSingleUpdatedComponent();
var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert
Assert.Collection(result.Edits,
@ -422,7 +404,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement();
// Act
var result = GetSingleUpdatedComponent();
var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert
Assert.Collection(result.Edits,
@ -431,8 +413,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
AssertEdit(entry, RenderTreeEditType.SetAttribute, 0);
Assert.Equal(0, entry.ReferenceFrameIndex);
});
Assert.Collection(result.ReferenceFrames,
frame => AssertFrame.Attribute(frame, "will change", "did change value"));
AssertFrame.Attribute(referenceFrames[0], "will change", "did change value");
}
[Fact]
@ -452,7 +433,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement();
// Act
var result = GetSingleUpdatedComponent();
var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert
Assert.Collection(result.Edits,
@ -461,8 +442,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
AssertEdit(entry, RenderTreeEditType.SetAttribute, 0);
Assert.Equal(0, entry.ReferenceFrameIndex);
});
Assert.Collection(result.ReferenceFrames,
frame => AssertFrame.Attribute(frame, "will change", addedHandler));
AssertFrame.Attribute(referenceFrames[0], "will change", addedHandler);
}
[Fact]
@ -477,7 +457,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement();
// Act
var result = GetSingleUpdatedComponent();
var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert
Assert.Collection(result.Edits,
@ -491,8 +471,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
AssertEdit(entry, RenderTreeEditType.SetAttribute, 0);
Assert.Equal(0, entry.ReferenceFrameIndex);
});
Assert.Collection(result.ReferenceFrames,
frame => AssertFrame.Attribute(frame, "newname", "same value"));
AssertFrame.Attribute(referenceFrames[0], "newname", "same value");
}
[Fact]
@ -518,7 +497,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement();
// Act
var result = GetSingleUpdatedComponent();
var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert
Assert.Collection(result.Edits,
@ -533,8 +512,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
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));
AssertFrame.Text(referenceFrames[0], "grandchild new text", 13);
}
[Fact]
@ -560,7 +538,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement();
// Act
var result = GetSingleUpdatedComponent();
var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert
Assert.Collection(result.Edits,
@ -571,8 +549,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
Assert.Equal(0, entry.ReferenceFrameIndex);
},
entry => AssertEdit(entry, RenderTreeEditType.StepOut, 0));
Assert.Collection(result.ReferenceFrames,
frame => AssertFrame.Text(frame, "Text that has changed", 11));
AssertFrame.Text(referenceFrames[0], "Text that has changed", 11);
}
[Fact]
@ -589,7 +566,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(13, "text4");
// Act
var result = GetSingleUpdatedComponent();
var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert
Assert.Collection(result.Edits,
@ -598,8 +575,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
AssertEdit(entry, RenderTreeEditType.UpdateText, 1);
Assert.Equal(0, entry.ReferenceFrameIndex);
});
Assert.Collection(result.ReferenceFrames,
frame => AssertFrame.Text(frame, "text2modified", 11));
AssertFrame.Text(referenceFrames[0], "text2modified", 11);
}
[Fact]
@ -621,11 +597,8 @@ namespace Microsoft.AspNetCore.Blazor.Test
var renderBatch = GetRenderedBatch();
// Assert
Assert.Equal(3, renderBatch.UpdatedComponents.Count);
// First component is the root one
var firstComponentDiff = renderBatch.UpdatedComponents.Array[0];
Assert.Collection(firstComponentDiff.Edits,
var diff = renderBatch.UpdatedComponents.Single();
Assert.Collection(diff.Edits,
entry => AssertEdit(entry, RenderTreeEditType.StepIn, 1),
entry =>
{
@ -638,23 +611,8 @@ namespace Microsoft.AspNetCore.Blazor.Test
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.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.ReferenceFrames,
frame => AssertFrame.Text(frame, $"Hello from {nameof(FakeComponent2)}"));
AssertFrame.ComponentWithInstance<FakeComponent>(renderBatch.ReferenceFrames.Array[0], 0, 12);
AssertFrame.ComponentWithInstance<FakeComponent2>(renderBatch.ReferenceFrames.Array[1], 1, 13);
}
[Fact]
@ -673,8 +631,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
var componentInstance = newTree.GetFrames().First().Component as FakeComponent;
// Assert
Assert.Equal(2, renderBatch.UpdatedComponents.Count);
Assert.Equal(1, renderBatch.UpdatedComponents.Count);
var rootComponentDiff = renderBatch.UpdatedComponents.Array[0];
AssertEdit(rootComponentDiff.Edits.Single(), RenderTreeEditType.PrependFrame, 0);
Assert.NotNull(componentInstance);
@ -695,7 +652,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Act/Assert
var ex = Assert.Throws<InvalidOperationException>(() =>
{
diff.ApplyNewRenderTreeVersion(new RenderBatchBuilder(), 0, oldTree.GetFrames(), newTree.GetFrames());
RenderTreeDiffBuilder.ComputeDiff(renderer, new RenderBatchBuilder(), 0, oldTree.GetFrames(), newTree.GetFrames());
});
Assert.Equal($"Component of type '{typeof(FakeComponent).FullName}' does not have a property matching the name 'SomeUnknownProperty'.", ex.Message);
}
@ -712,7 +669,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Act/Assert
var ex = Assert.Throws<InvalidOperationException>(() =>
{
diff.ApplyNewRenderTreeVersion(new RenderBatchBuilder(), 0, oldTree.GetFrames(), newTree.GetFrames());
RenderTreeDiffBuilder.ComputeDiff(renderer, new RenderBatchBuilder(), 0, oldTree.GetFrames(), newTree.GetFrames());
});
Assert.StartsWith($"Unable to set property '{nameof(FakeComponent.ReadonlyProperty)}' on " +
$"component of type '{typeof(FakeComponent).FullName}'.", ex.Message);
@ -735,14 +692,14 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseComponent(); // </FakeComponent>
newTree.OpenComponent<FakeComponent2>(13); // 3: <FakeComponent2>
newTree.CloseComponent(); // </FakeComponent2>
newTree.CloseElement(); // </container
newTree.CloseElement(); // </container>
diff.ApplyNewRenderTreeVersion(new RenderBatchBuilder(), 0, new RenderTreeBuilder(renderer).GetFrames(), oldTree.GetFrames());
RenderTreeDiffBuilder.ComputeDiff(renderer, new RenderBatchBuilder(), 0, new RenderTreeBuilder(renderer).GetFrames(), oldTree.GetFrames());
var originalFakeComponentInstance = oldTree.GetFrames().Array[2].Component;
var originalFakeComponent2Instance = oldTree.GetFrames().Array[3].Component;
// Act
var result = GetSingleUpdatedComponent();
var (result, referenceFrames) = GetSingleUpdatedComponent();
var newFrame1 = newTree.GetFrames().Array[2];
var newFrame2 = newTree.GetFrames().Array[3];
@ -768,7 +725,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddAttribute(14, nameof(FakeComponent.ObjectProperty), objectWillNotChange);
newTree.CloseComponent();
diff.ApplyNewRenderTreeVersion(new RenderBatchBuilder(), 0, new RenderTreeBuilder(renderer).GetFrames(), oldTree.GetFrames());
RenderTreeDiffBuilder.ComputeDiff(renderer, new RenderBatchBuilder(), 0, new RenderTreeBuilder(renderer).GetFrames(), oldTree.GetFrames());
var originalComponentInstance = (FakeComponent)oldTree.GetFrames().Array[0].Component;
originalComponentInstance.ObjectProperty = null; // So we can see it doesn't get reassigned
@ -777,67 +734,14 @@ namespace Microsoft.AspNetCore.Blazor.Test
var newComponentInstance = (FakeComponent)oldTree.GetFrames().Array[0].Component;
// Assert
Assert.Equal(2, renderBatch.UpdatedComponents.Count);
Assert.Equal(1, renderBatch.UpdatedComponents.Count); // Because the diff builder only queues child component renders; it doesn't actually perfom them itself
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
}
[Fact]
public void NotifiesIHandlePropertiesChangedBeforeFirstRender()
{
// Arrange
newTree.OpenComponent<HandlePropertiesChangedComponent>(0);
newTree.CloseComponent();
// Act
var batch = GetRenderedBatch();
var diffForChildComponent = batch.UpdatedComponents.Array[1];
// Assert
Assert.Collection(diffForChildComponent.ReferenceFrames,
frame => AssertFrame.Text(frame, "Notifications: 1", 0));
}
[Fact]
public void NotifiesIHandlePropertiesChangedWhenChanged()
{
// Arrange
var newTree1 = new RenderTreeBuilder(renderer);
var newTree2 = new RenderTreeBuilder(renderer);
oldTree.OpenComponent<HandlePropertiesChangedComponent>(0);
oldTree.AddAttribute(1, nameof(HandlePropertiesChangedComponent.IntProperty), 123);
oldTree.CloseComponent();
newTree1.OpenComponent<HandlePropertiesChangedComponent>(0);
newTree1.AddAttribute(1, nameof(HandlePropertiesChangedComponent.IntProperty), 123);
newTree1.CloseComponent();
newTree2.OpenComponent<HandlePropertiesChangedComponent>(0);
newTree2.AddAttribute(1, nameof(HandlePropertiesChangedComponent.IntProperty), 456);
newTree2.CloseComponent();
// Act/Assert 0: Initial render
var batch0 = GetRenderedBatch(new RenderTreeBuilder(renderer), oldTree);
var diffForChildComponent0 = batch0.UpdatedComponents.Array[1];
var childComponentFrame = batch0.UpdatedComponents.Array[0].ReferenceFrames.Array[0];
var childComponentInstance = (HandlePropertiesChangedComponent)childComponentFrame.Component;
Assert.Equal(1, childComponentInstance.NotificationsCount);
Assert.Collection(diffForChildComponent0.ReferenceFrames,
frame => AssertFrame.Text(frame, "Notifications: 1", 0));
// Act/Assert 1: If properties didn't change, we don't notify
GetRenderedBatch(oldTree, newTree1);
Assert.Equal(1, childComponentInstance.NotificationsCount);
// Act/Assert 2: If properties did change, we do notify
var batch2 = GetRenderedBatch(newTree1, newTree2);
var diffForChildComponent2 = batch2.UpdatedComponents.Array[1];
Assert.Equal(2, childComponentInstance.NotificationsCount);
Assert.Collection(diffForChildComponent2.ReferenceFrames,
frame => AssertFrame.Text(frame, "Notifications: 2", 0));
}
[Fact]
public void CallsDisposeOnlyOnRemovedChildComponents()
public void QueuesRemovedChildComponentsForDisposal()
{
// Arrange
oldTree.OpenComponent<DisposableComponent>(10); // <DisposableComponent>
@ -849,29 +753,23 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.OpenComponent<DisposableComponent>(30); // <DisposableComponent>
newTree.CloseComponent(); // </DisposableComponent>
diff.ApplyNewRenderTreeVersion(new RenderBatchBuilder(), 0, new RenderTreeBuilder(renderer).GetFrames(), oldTree.GetFrames());
var disposableComponent1 = (DisposableComponent)oldTree.GetFrames().Array[0].Component;
var nonDisposableComponent = (NonDisposableComponent)oldTree.GetFrames().Array[1].Component;
var disposableComponent2 = (DisposableComponent)oldTree.GetFrames().Array[2].Component;
var batchBuilder = new RenderBatchBuilder();
RenderTreeDiffBuilder.ComputeDiff(renderer, batchBuilder, 0, new RenderTreeBuilder(renderer).GetFrames(), oldTree.GetFrames());
// Act
var renderedBatch = GetRenderedBatch();
// Assert: We track NonDisposableComponent was disposed even though it's not IDisposable
Assert.Equal(renderedBatch.DisposedComponentIDs, new[] { 0, 1 });
// Assert: We did call Dispose on the disposed DisposableComponent
Assert.Equal(1, disposableComponent1.DisposalCount);
// Assert: We didn't dispose the retained component
Assert.Equal(0, disposableComponent2.DisposalCount);
// Act/Assert
// Note that we track NonDisposableComponent was disposed even though it's not IDisposable,
// because it's up to the upstream renderer to decide what "disposing" a component means
Assert.Empty(batchBuilder.ComponentDisposalQueue);
RenderTreeDiffBuilder.ComputeDiff(renderer, batchBuilder, 0, oldTree.GetFrames(), newTree.GetFrames());
Assert.Equal(new[] { 0, 1 }, batchBuilder.ComponentDisposalQueue);
}
private RenderTreeDiff GetSingleUpdatedComponent()
private (RenderTreeDiff, RenderTreeFrame[]) GetSingleUpdatedComponent()
{
var diffsInBatch = GetRenderedBatch().UpdatedComponents;
var batch = GetRenderedBatch();
var diffsInBatch = batch.UpdatedComponents;
Assert.Equal(1, diffsInBatch.Count);
return diffsInBatch.Array[0];
return (diffsInBatch.Array[0], batch.ReferenceFrames.ToArray());
}
private RenderBatch GetRenderedBatch()
@ -880,7 +778,8 @@ namespace Microsoft.AspNetCore.Blazor.Test
private RenderBatch GetRenderedBatch(RenderTreeBuilder from, RenderTreeBuilder to)
{
var batchBuilder = new RenderBatchBuilder();
diff.ApplyNewRenderTreeVersion(batchBuilder, 0, from.GetFrames(), to.GetFrames());
var diff = RenderTreeDiffBuilder.ComputeDiff(renderer, batchBuilder, 0, from.GetFrames(), to.GetFrames());
batchBuilder.UpdatedComponentDiffs.Append(diff);
return batchBuilder.ToBatch();
}
@ -912,23 +811,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
}
}
private class HandlePropertiesChangedComponent : IComponent, IHandlePropertiesChanged
{
public int NotificationsCount { get; private set; }
public int IntProperty { get; set; }
public void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddText(0, $"Notifications: {NotificationsCount}");
}
public void OnPropertiesChanged()
{
NotificationsCount++;
}
}
private class DisposableComponent : IComponent, IDisposable
{
public int DisposalCount { get; private set; }

View File

@ -31,16 +31,16 @@ namespace Microsoft.AspNetCore.Blazor.Test
renderer.RenderNewBatch(componentId);
// Assert
var diff = renderer.Batches.Single().DiffsByComponentId[componentId].Single();
var batch = renderer.Batches.Single();
var diff = batch.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"));
AssertFrame.Element(batch.ReferenceFrames[0], "my element", 2);
AssertFrame.Text(batch.ReferenceFrames[1], "some text");
}
[Fact]
@ -60,11 +60,14 @@ namespace Microsoft.AspNetCore.Blazor.Test
var componentId = renderer.AssignComponentId(component);
renderer.RenderNewBatch(componentId);
var batch = renderer.Batches.Single();
var componentFrame = batch.DiffsByComponentId[componentId].Single().ReferenceFrames
var componentFrame = batch.ReferenceFrames
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
var nestedComponentId = componentFrame.ComponentId;
var nestedComponentDiff = batch.DiffsByComponentId[nestedComponentId].Single();
// We rendered both components
Assert.Equal(2, batch.DiffsByComponentId.Count);
// The nested component exists
Assert.IsType<MessageComponent>(componentFrame.Component);
@ -73,10 +76,10 @@ namespace Microsoft.AspNetCore.Blazor.Test
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
AssertFrame.Text(
batch.ReferenceFrames[edit.ReferenceFrameIndex],
"Nested component output");
});
Assert.Collection(nestedComponentDiff.ReferenceFrames,
frame => AssertFrame.Text(frame, "Nested component output"));
}
[Fact]
@ -89,28 +92,28 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Act/Assert: first render
renderer.RenderNewBatch(componentId);
var firstDiff = renderer.Batches.Single().DiffsByComponentId[componentId].Single();
var batch = renderer.Batches.Single();
var firstDiff = batch.DiffsByComponentId[componentId].Single();
Assert.Collection(firstDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
AssertFrame.Text(batch.ReferenceFrames[0], "Initial message");
});
Assert.Collection(firstDiff.ReferenceFrames,
frame => AssertFrame.Text(frame, "Initial message"));
// Act/Assert: second render
component.Message = "Modified message";
renderer.RenderNewBatch(componentId);
var secondDiff = renderer.Batches.Skip(1).Single().DiffsByComponentId[componentId].Single();
Assert.Collection(firstDiff.Edits,
var secondBatch = renderer.Batches.Skip(1).Single();
var secondDiff = secondBatch.DiffsByComponentId[componentId].Single();
Assert.Collection(secondDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
AssertFrame.Text(secondBatch.ReferenceFrames[0], "Modified message");
});
Assert.Collection(firstDiff.ReferenceFrames,
frame => AssertFrame.Text(frame, "Modified message"));
}
[Fact]
@ -125,8 +128,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
});
var parentComponentId = renderer.AssignComponentId(parentComponent);
renderer.RenderNewBatch(parentComponentId);
var nestedComponentFrame = renderer.Batches.Single().DiffsByComponentId[parentComponentId]
.Single()
var nestedComponentFrame = renderer.Batches.Single()
.ReferenceFrames
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
var nestedComponent = (MessageComponent)nestedComponentFrame.Component;
@ -135,28 +137,28 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Assert: inital render
nestedComponent.Message = "Render 1";
renderer.RenderNewBatch(nestedComponentId);
var firstDiff = renderer.Batches[1].DiffsByComponentId[nestedComponentId].Single();
var batch = renderer.Batches[1];
var firstDiff = batch.DiffsByComponentId[nestedComponentId].Single();
Assert.Collection(firstDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
AssertFrame.Text(batch.ReferenceFrames[0], "Render 1");
});
Assert.Collection(firstDiff.ReferenceFrames,
frame => AssertFrame.Text(frame, "Render 1"));
// Act/Assert: re-render
nestedComponent.Message = "Render 2";
renderer.RenderNewBatch(nestedComponentId);
var secondDiff = renderer.Batches[2].DiffsByComponentId[nestedComponentId].Single();
Assert.Collection(firstDiff.Edits,
var secondBatch = renderer.Batches[2];
var secondDiff = secondBatch.DiffsByComponentId[nestedComponentId].Single();
Assert.Collection(secondDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
AssertFrame.Text(secondBatch.ReferenceFrames[0], "Render 2");
});
Assert.Collection(firstDiff.ReferenceFrames,
frame => AssertFrame.Text(frame, "Render 2"));
}
[Fact]
@ -173,7 +175,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
var componentId = renderer.AssignComponentId(component);
renderer.RenderNewBatch(componentId);
var eventHandlerId = renderer.Batches.Single().DiffsByComponentId[componentId].Single()
var eventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.First(frame => frame.AttributeValue != null)
.AttributeEventHandlerId;
@ -203,8 +205,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
renderer.RenderNewBatch(parentComponentId);
// Arrange: Render nested component
var nestedComponentFrame = renderer.Batches.Single().DiffsByComponentId[parentComponentId]
.Single()
var nestedComponentFrame = renderer.Batches.Single()
.ReferenceFrames
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
var nestedComponent = (EventComponent)nestedComponentFrame.Component;
@ -213,7 +214,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
renderer.RenderNewBatch(nestedComponentId);
// Find nested component's event handler ID
var eventHandlerId = renderer.Batches[1].DiffsByComponentId[nestedComponentId].Single()
var eventHandlerId = renderer.Batches[1]
.ReferenceFrames
.First(frame => frame.AttributeValue != null)
.AttributeEventHandlerId;
@ -363,8 +364,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
renderer.RenderNewBatch(rootComponentId);
var nestedComponentFrame = renderer.Batches.Single()
.DiffsByComponentId[rootComponentId]
.Single()
.ReferenceFrames
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
var nestedComponentInstance = (MessageComponent)nestedComponentFrame.Component;
@ -382,8 +381,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
});
Assert.Collection(diff.ReferenceFrames,
frame => AssertFrame.Text(frame, "Modified message"));
AssertFrame.Text(batch.ReferenceFrames[0], "Modified message");
Assert.False(batch.DiffsByComponentId.ContainsKey(nestedComponentFrame.ComponentId));
}
@ -406,8 +404,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
var rootComponentId = renderer.AssignComponentId(component);
renderer.RenderNewBatch(rootComponentId);
var originalComponentFrame = renderer.Batches.Single().DiffsByComponentId[rootComponentId]
.Single()
var originalComponentFrame = renderer.Batches.Single()
.ReferenceFrames
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
var childComponentInstance = (FakeComponent)originalComponentFrame.Component;
@ -443,8 +440,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
var rootComponentId = renderer.AssignComponentId(component);
renderer.RenderNewBatch(rootComponentId);
var childComponentId = renderer.Batches.Single().DiffsByComponentId[rootComponentId]
.Single()
var childComponentId = renderer.Batches.Single()
.ReferenceFrames
.Single(frame => frame.FrameType == RenderTreeFrameType.Component)
.ComponentId;
@ -461,8 +457,67 @@ namespace Microsoft.AspNetCore.Blazor.Test
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex);
});
Assert.Collection(diff.ReferenceFrames,
frame => AssertFrame.Text(frame, "second"));
AssertFrame.Text(renderer.Batches[1].ReferenceFrames[0], "second");
}
[Fact]
public void NotifiesIHandlePropertiesChangedBeforeFirstRender()
{
// Arrange
var renderer = new TestRenderer();
var component = new HandlePropertiesChangedComponent();
var rootComponentId = renderer.AssignComponentId(component);
// Act
renderer.RenderNewBatch(rootComponentId);
// Assert
AssertFrame.Text(renderer.Batches.Single().ReferenceFrames[0], "Notifications: 1", 0);
}
[Fact]
public void NotifiesIHandlePropertiesChangedWhenChanged()
{
// Arrange
var renderer = new TestRenderer();
var component = new ConditionalParentComponent<HandlePropertiesChangedComponent>
{
IncludeChild = true,
ChildParameters = new Dictionary<string, object>
{
{ nameof(HandlePropertiesChangedComponent.IntProperty), 123 }
}
};
var rootComponentId = renderer.AssignComponentId(component);
// Act/Assert 0: Initial render
renderer.RenderNewBatch(rootComponentId);
var batch1 = renderer.Batches.Single();
var childComponentFrame = batch1
.ReferenceFrames.Where(frame => frame.FrameType == RenderTreeFrameType.Component).Single();
var childComponentId = childComponentFrame.ComponentId;
var diffForChildComponent0 = batch1.DiffsByComponentId[childComponentId].Single();
var childComponentInstance = (HandlePropertiesChangedComponent)childComponentFrame.Component;
Assert.Equal(1, childComponentInstance.NotificationsCount);
Assert.Collection(diffForChildComponent0.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Text(batch1.ReferenceFrames[edit.ReferenceFrameIndex], "Notifications: 1", 0);
});
// Act/Assert 1: If properties didn't change, we don't notify
renderer.RenderNewBatch(rootComponentId);
Assert.Equal(1, childComponentInstance.NotificationsCount);
// Act/Assert 2: If properties did change, we do notify
component.ChildParameters[nameof(HandlePropertiesChangedComponent.IntProperty)] = 456;
renderer.RenderNewBatch(rootComponentId);
Assert.Equal(2, childComponentInstance.NotificationsCount);
var batch3 = renderer.Batches.Skip(2).Single();
var diffForChildComponent2 = batch3.DiffsByComponentId[childComponentId].Single();
Assert.Equal(2, childComponentInstance.NotificationsCount);
AssertFrame.Text(batch3.ReferenceFrames[0], "Notifications: 2", 0);
}
[Fact]
@ -473,7 +528,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
var firstRender = true;
var component = new TestComponent(builder =>
{
builder.OpenElement(7, "some element");
if (firstRender)
{
// Nested descendants
@ -483,27 +537,28 @@ namespace Microsoft.AspNetCore.Blazor.Test
}
builder.OpenComponent<FakeComponent>(200);
builder.CloseComponent();
builder.CloseElement();
});
var rootComponentId = renderer.AssignComponentId(component);
// Act/Assert 1: First render, capturing child component IDs
renderer.RenderNewBatch(rootComponentId);
var childComponentIds = renderer.Batches.Single().DiffsByComponentId[rootComponentId]
.Single()
.ReferenceFrames
var batch = renderer.Batches.Single();
var rootComponentDiff = batch.DiffsByComponentId[rootComponentId].Single();
var childComponentIds = rootComponentDiff
.Edits
.Select(edit => batch.ReferenceFrames[edit.ReferenceFrameIndex])
.Where(frame => frame.FrameType == RenderTreeFrameType.Component)
.Select(frame => frame.ComponentId)
.ToList();
Assert.Equal(new[] { 1, 3 }, childComponentIds);
Assert.Equal(new[] { 1, 2 }, childComponentIds);
// Act: Second render
firstRender = false;
renderer.RenderNewBatch(rootComponentId);
// Assert: Applicable children are included in disposal list
Assert.Equal(new[] { 2, 1 }, renderer.Batches[1].DisposedComponentIDs);
Assert.Equal(new[] { 1, 3 }, renderer.Batches[1].DisposedComponentIDs);
}
[Fact]
@ -516,7 +571,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
var component = new EventComponent { Handler = origEventHandler };
var componentId = renderer.AssignComponentId(component);
renderer.RenderNewBatch(componentId);
var origEventHandlerId = renderer.Batches.Single().DiffsByComponentId[componentId].Single()
var origEventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.Where(f => f.FrameType == RenderTreeFrameType.Attribute)
.Single(f => f.AttributeEventHandlerId != 0)
@ -553,7 +608,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
var component = new EventComponent { Handler = origEventHandler };
var componentId = renderer.AssignComponentId(component);
renderer.RenderNewBatch(componentId);
var origEventHandlerId = renderer.Batches.Single().DiffsByComponentId[componentId].Single()
var origEventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.Where(f => f.FrameType == RenderTreeFrameType.Attribute)
.Single(f => f.AttributeEventHandlerId != 0)
@ -593,13 +648,17 @@ namespace Microsoft.AspNetCore.Blazor.Test
};
var rootComponentId = renderer.AssignComponentId(component);
renderer.RenderNewBatch(rootComponentId);
var childComponentId = renderer.Batches.Single().DiffsByComponentId[rootComponentId].Single()
.ReferenceFrames
var batch = renderer.Batches.Single();
var rootComponentDiff = batch.DiffsByComponentId[rootComponentId].Single();
var rootComponentFrame = batch.ReferenceFrames[0];
var childComponentFrame = rootComponentDiff.Edits
.Select(e => batch.ReferenceFrames[e.ReferenceFrameIndex])
.Where(f => f.FrameType == RenderTreeFrameType.Component)
.Single()
.ComponentId;
var eventHandlerId = renderer.Batches.Single().DiffsByComponentId[childComponentId].Single()
.ReferenceFrames
.Single();
var childComponentId = childComponentFrame.ComponentId;
var childComponentDiff = batch.DiffsByComponentId[childComponentFrame.ComponentId].Single();
var eventHandlerId = batch.ReferenceFrames
.Skip(childComponentDiff.Edits[0].ReferenceFrameIndex) // Search from where the child component frames start
.Where(f => f.FrameType == RenderTreeFrameType.Attribute)
.Single(f => f.AttributeEventHandlerId != 0)
.AttributeEventHandlerId;
@ -631,7 +690,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
var component = new EventComponent { Handler = origEventHandler };
var componentId = renderer.AssignComponentId(component);
renderer.RenderNewBatch(componentId);
var origEventHandlerId = renderer.Batches.Single().DiffsByComponentId[componentId].Single()
var origEventHandlerId = renderer.Batches.Single()
.ReferenceFrames
.Where(f => f.FrameType == RenderTreeFrameType.Attribute)
.Single(f => f.AttributeEventHandlerId != 0)
@ -692,6 +751,8 @@ namespace Microsoft.AspNetCore.Blazor.Test
capturedBatch.AddDiff(renderTreeDiff);
}
// Clone other data, as underlying storage will get reused by later batches
capturedBatch.ReferenceFrames = renderBatch.ReferenceFrames.ToArray();
capturedBatch.DisposedComponentIDs = renderBatch.DisposedComponentIDs.ToList();
}
}
@ -702,6 +763,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
= new Dictionary<int, List<RenderTreeDiff>>();
public IList<int> DisposedComponentIDs { get; set; }
public RenderTreeFrame[] ReferenceFrames { get; set; }
internal void AddDiff(RenderTreeDiff diff)
{
@ -710,7 +772,11 @@ namespace Microsoft.AspNetCore.Blazor.Test
{
DiffsByComponentId.Add(componentId, new List<RenderTreeDiff>());
}
DiffsByComponentId[componentId].Add(diff);
// Clone the diff, because its underlying storage will get reused in subsequent batches
DiffsByComponentId[componentId].Add(new RenderTreeDiff(
diff.ComponentId,
new ArraySegment<RenderTreeEdit>(diff.Edits.ToArray())));
}
}
@ -796,6 +862,23 @@ namespace Microsoft.AspNetCore.Blazor.Test
}
}
private class HandlePropertiesChangedComponent : IComponent, IHandlePropertiesChanged
{
public int NotificationsCount { get; private set; }
public int IntProperty { get; set; }
public void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddText(0, $"Notifications: {NotificationsCount}");
}
public void OnPropertiesChanged()
{
NotificationsCount++;
}
}
void AssertCanBeCollected(Func<object> targetFactory)
{
// We have to construct the WeakReference in a separate scope