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

View File

@ -7,7 +7,8 @@ import { RenderTreeEditPointer } from './RenderTreeEdit';
export const renderBatch = { export const renderBatch = {
updatedComponents: (obj: RenderBatchPointer) => platform.readStructField<ArrayRangePointer<RenderTreeDiffPointer>>(obj, 0), 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; const arrayRangeStructLength = 8;
@ -16,15 +17,22 @@ export const arrayRange = {
count: <T>(obj: ArrayRangePointer<T>) => platform.readInt32Field(obj, 4), 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 = { export const renderTreeDiff = {
componentId: (obj: RenderTreeDiffPointer) => platform.readInt32Field(obj, 0), componentId: (obj: RenderTreeDiffPointer) => platform.readInt32Field(obj, 0),
edits: (obj: RenderTreeDiffPointer) => platform.readStructField<ArrayRangePointer<RenderTreeEditPointer>>(obj, 4), edits: (obj: RenderTreeDiffPointer) => platform.readStructField<ArraySegmentPointer<RenderTreeEditPointer>>(obj, 4),
currentState: (obj: RenderTreeDiffPointer) => platform.readStructField<ArrayRangePointer<RenderTreeFramePointer>>(obj, 4 + arrayRangeStructLength),
}; };
// Nominal types to ensure only valid pointers are passed to the functions above. // Nominal types to ensure only valid pointers are passed to the functions above.
// At runtime the values are just numbers. // At runtime the values are just numbers.
export interface RenderBatchPointer extends Pointer { RenderBatchPointer__DO_NOT_IMPLEMENT: any } export interface RenderBatchPointer extends Pointer { RenderBatchPointer__DO_NOT_IMPLEMENT: any }
export interface ArrayRangePointer<T> extends Pointer { ArrayRangePointer__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 } 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 { System_Object, System_String, System_Array, MethodHandle, Pointer } from '../Platform/Platform';
import { platform } from '../Environment'; 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'; import { BrowserRenderer } from './BrowserRenderer';
type BrowserRendererRegistry = { [browserRendererId: number]: BrowserRenderer }; type BrowserRendererRegistry = { [browserRendererId: number]: BrowserRenderer };
@ -30,17 +30,19 @@ export function renderBatch(browserRendererId: number, batch: RenderBatchPointer
const updatedComponents = renderBatchStruct.updatedComponents(batch); const updatedComponents = renderBatchStruct.updatedComponents(batch);
const updatedComponentsLength = arrayRange.count(updatedComponents); const updatedComponentsLength = arrayRange.count(updatedComponents);
const updatedComponentsArray = arrayRange.array(updatedComponents); const updatedComponentsArray = arrayRange.array(updatedComponents);
const referenceFramesStruct = renderBatchStruct.referenceFrames(batch);
const referenceFrames = arrayRange.array(referenceFramesStruct);
for (let i = 0; i < updatedComponentsLength; i++) { for (let i = 0; i < updatedComponentsLength; i++) {
const diff = platform.getArrayEntryPtr(updatedComponentsArray, i, renderTreeDiffStructLength); const diff = platform.getArrayEntryPtr(updatedComponentsArray, i, renderTreeDiffStructLength);
const componentId = renderTreeDiff.componentId(diff); const componentId = renderTreeDiff.componentId(diff);
const editsArrayRange = renderTreeDiff.edits(diff); const editsArraySegment = renderTreeDiff.edits(diff);
const currentStateArrayRange = renderTreeDiff.currentState(diff); const edits = arraySegment.array(editsArraySegment);
const editsOffset = arraySegment.offset(editsArraySegment);
const editsLength = arraySegment.count(editsArraySegment);
const edits = arrayRange.array(editsArrayRange); browserRenderer.updateComponent(componentId, edits, editsOffset, editsLength, referenceFrames);
const editsLength = arrayRange.count(editsArrayRange);
const tree = arrayRange.array(currentStateArrayRange);
browserRenderer.updateComponent(componentId, edits, editsLength, tree);
} }
const disposedComponentIds = renderBatchStruct.disposedComponentIds(batch); const disposedComponentIds = renderBatchStruct.disposedComponentIds(batch);

View File

@ -132,6 +132,15 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
public ArrayRange<T> ToRange() public ArrayRange<T> ToRange()
=> new ArrayRange<T>(_items, _itemsInUse); => 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) private void SetCapacity(int desiredCapacity, bool preserveContents)
{ {
if (desiredCapacity < _itemsInUse) if (desiredCapacity < _itemsInUse)

View File

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

View File

@ -8,50 +8,69 @@ using Microsoft.AspNetCore.Blazor.Rendering;
namespace Microsoft.AspNetCore.Blazor.RenderTree namespace Microsoft.AspNetCore.Blazor.RenderTree
{ {
internal class RenderTreeDiffBuilder internal static class RenderTreeDiffBuilder
{ {
private readonly Renderer _renderer; private struct DiffContext
private readonly ArrayBuilder<RenderTreeEdit> _entries = new ArrayBuilder<RenderTreeEdit>(10);
private readonly ArrayBuilder<RenderTreeFrame> _referenceFrames = new ArrayBuilder<RenderTreeFrame>(10);
public RenderTreeDiffBuilder(Renderer renderer)
{ {
_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> public static RenderTreeDiff ComputeDiff(
/// As well as computing the diff between the two trees, this method also has the side-effect Renderer renderer,
/// 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(
RenderBatchBuilder batchBuilder, RenderBatchBuilder batchBuilder,
int componentId, int componentId,
ArrayRange<RenderTreeFrame> oldTree, ArrayRange<RenderTreeFrame> oldTree,
ArrayRange<RenderTreeFrame> newTree) ArrayRange<RenderTreeFrame> newTree)
{ {
_entries.Clear(); var editsBuffer = batchBuilder.EditsBuffer;
_referenceFrames.Clear(); var editsBufferStartLength = editsBuffer.Count;
var siblingIndex = 0;
var slotId = batchBuilder.ReserveUpdatedComponentSlotId(); var diffContext = new DiffContext(renderer, batchBuilder, oldTree.Array, newTree.Array);
AppendDiffEntriesForRange(batchBuilder, oldTree.Array, 0, oldTree.Count, newTree.Array, 0, newTree.Count, ref siblingIndex); AppendDiffEntriesForRange(ref diffContext, 0, oldTree.Count, 0, newTree.Count);
batchBuilder.SetUpdatedComponent(
slotId, var editsSegment = editsBuffer.ToSegment(editsBufferStartLength, editsBuffer.Count);
new RenderTreeDiff(componentId, _entries.ToRange(), _referenceFrames.ToRange())); return new RenderTreeDiff(componentId, editsSegment);
} }
private void AppendDiffEntriesForRange( public static void DisposeFrames(RenderBatchBuilder batchBuilder, ArrayRange<RenderTreeFrame> frames)
RenderBatchBuilder batchBuilder, => DisposeFramesInRange(batchBuilder, frames.Array, 0, frames.Count);
RenderTreeFrame[] oldTree, int oldStartIndex, int oldEndIndexExcl,
RenderTreeFrame[] newTree, int newStartIndex, int newEndIndexExcl, private static void AppendDiffEntriesForRange(
ref int siblingIndex) ref DiffContext diffContext,
int oldStartIndex, int oldEndIndexExcl,
int newStartIndex, int newEndIndexExcl)
{ {
var hasMoreOld = oldEndIndexExcl > oldStartIndex; var hasMoreOld = oldEndIndexExcl > oldStartIndex;
var hasMoreNew = newEndIndexExcl > newStartIndex; var hasMoreNew = newEndIndexExcl > newStartIndex;
var prevOldSeq = -1; var prevOldSeq = -1;
var prevNewSeq = -1; var prevNewSeq = -1;
var oldTree = diffContext.OldTree;
var newTree = diffContext.NewTree;
while (hasMoreOld || hasMoreNew) while (hasMoreOld || hasMoreNew)
{ {
var oldSeq = hasMoreOld ? oldTree[oldStartIndex].Sequence : int.MaxValue; var oldSeq = hasMoreOld ? oldTree[oldStartIndex].Sequence : int.MaxValue;
@ -59,7 +78,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
if (oldSeq == newSeq) if (oldSeq == newSeq)
{ {
AppendDiffEntriesForFramesWithSameSequence(batchBuilder, oldTree, oldStartIndex, newTree, newStartIndex, ref siblingIndex); AppendDiffEntriesForFramesWithSameSequence(ref diffContext, oldStartIndex, newStartIndex);
oldStartIndex = NextSiblingIndex(oldTree[oldStartIndex], oldStartIndex); oldStartIndex = NextSiblingIndex(oldTree[oldStartIndex], oldStartIndex);
newStartIndex = NextSiblingIndex(newTree[newStartIndex], newStartIndex); newStartIndex = NextSiblingIndex(newTree[newStartIndex], newStartIndex);
hasMoreOld = oldEndIndexExcl > oldStartIndex; hasMoreOld = oldEndIndexExcl > oldStartIndex;
@ -134,14 +153,14 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
if (treatAsInsert) if (treatAsInsert)
{ {
InsertNewFrame(batchBuilder, newTree, newStartIndex, ref siblingIndex); InsertNewFrame(ref diffContext, newStartIndex);
newStartIndex = NextSiblingIndex(newTree[newStartIndex], newStartIndex); newStartIndex = NextSiblingIndex(newTree[newStartIndex], newStartIndex);
hasMoreNew = newEndIndexExcl > newStartIndex; hasMoreNew = newEndIndexExcl > newStartIndex;
prevNewSeq = newSeq; prevNewSeq = newSeq;
} }
else else
{ {
RemoveOldFrame(batchBuilder, oldTree, oldStartIndex, siblingIndex); RemoveOldFrame(ref diffContext, oldStartIndex);
oldStartIndex = NextSiblingIndex(oldTree[oldStartIndex], oldStartIndex); oldStartIndex = NextSiblingIndex(oldTree[oldStartIndex], oldStartIndex);
hasMoreOld = oldEndIndexExcl > oldStartIndex; hasMoreOld = oldEndIndexExcl > oldStartIndex;
prevOldSeq = oldSeq; prevOldSeq = oldSeq;
@ -150,16 +169,18 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
} }
} }
private void UpdateRetainedChildComponent( private static void UpdateRetainedChildComponent(
RenderBatchBuilder batchBuilder, ref DiffContext diffContext,
RenderTreeFrame[] oldTree, int oldComponentIndex, int oldComponentIndex,
RenderTreeFrame[] newTree, int newComponentIndex) int newComponentIndex)
{ {
// The algorithm here is the same as in AppendDiffEntriesForRange, except that // 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. // 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 // That's because this is true for all currently supported scenarios, and it means
// fewer steps here. // fewer steps here.
var oldTree = diffContext.OldTree;
var newTree = diffContext.NewTree;
ref var oldComponentFrame = ref oldTree[oldComponentIndex]; ref var oldComponentFrame = ref oldTree[oldComponentIndex];
ref var newComponentFrame = ref newTree[newComponentIndex]; ref var newComponentFrame = ref newTree[newComponentIndex];
var componentId = oldComponentFrame.ComponentId; var componentId = oldComponentFrame.ComponentId;
@ -241,7 +262,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
if (hasSetAnyProperty) if (hasSetAnyProperty)
{ {
TriggerChildComponentRender(batchBuilder, newComponentFrame); diffContext.BatchBuilder.ComponentRenderQueue.Enqueue(newComponentFrame.ComponentId);
} }
} }
@ -291,12 +312,13 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
return frameIndex + distanceToNextSibling; return frameIndex + distanceToNextSibling;
} }
private void AppendDiffEntriesForFramesWithSameSequence( private static void AppendDiffEntriesForFramesWithSameSequence(
RenderBatchBuilder batchBuilder, ref DiffContext diffContext,
RenderTreeFrame[] oldTree, int oldFrameIndex, int oldFrameIndex,
RenderTreeFrame[] newTree, int newFrameIndex, int newFrameIndex)
ref int siblingIndex)
{ {
var oldTree = diffContext.OldTree;
var newTree = diffContext.NewTree;
ref var oldFrame = ref oldTree[oldFrameIndex]; ref var oldFrame = ref oldTree[oldFrameIndex];
ref var newFrame = ref newTree[newFrameIndex]; 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). // to the same sequence number (and if not, the behaviour is undefined).
// TODO: Consider supporting dissimilar types at same sequence for custom IComponent implementations. // TODO: Consider supporting dissimilar types at same sequence for custom IComponent implementations.
// It should only be a matter of calling RemoveOldFrame+InsertNewFrame // It should only be a matter of calling RemoveOldFrame+InsertNewFrame
switch (newTree[newFrameIndex].FrameType) switch (newFrame.FrameType)
{ {
case RenderTreeFrameType.Text: case RenderTreeFrameType.Text:
{ {
@ -312,10 +334,10 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
var newText = newFrame.TextContent; var newText = newFrame.TextContent;
if (!string.Equals(oldText, newText, StringComparison.Ordinal)) if (!string.Equals(oldText, newText, StringComparison.Ordinal))
{ {
var referenceFrameIndex = _referenceFrames.Append(newFrame); var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame);
_entries.Append(RenderTreeEdit.UpdateText(siblingIndex, referenceFrameIndex)); diffContext.Edits.Append(RenderTreeEdit.UpdateText(diffContext.SiblingIndex, referenceFrameIndex));
} }
siblingIndex++; diffContext.SiblingIndex++;
break; break;
} }
@ -330,10 +352,9 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
// Diff the attributes // Diff the attributes
AppendDiffEntriesForRange( AppendDiffEntriesForRange(
batchBuilder, ref diffContext,
oldTree, oldFrameIndex + 1, oldFrameAttributesEndIndexExcl, oldFrameIndex + 1, oldFrameAttributesEndIndexExcl,
newTree, newFrameIndex + 1, newFrameAttributesEndIndexExcl, newFrameIndex + 1, newFrameAttributesEndIndexExcl);
ref siblingIndex);
// Diff the children // Diff the children
var oldFrameChildrenEndIndexExcl = oldFrameIndex + oldFrame.ElementSubtreeLength; var oldFrameChildrenEndIndexExcl = oldFrameIndex + oldFrame.ElementSubtreeLength;
@ -343,26 +364,26 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
newFrameChildrenEndIndexExcl > newFrameAttributesEndIndexExcl; newFrameChildrenEndIndexExcl > newFrameAttributesEndIndexExcl;
if (hasChildrenToProcess) if (hasChildrenToProcess)
{ {
_entries.Append(RenderTreeEdit.StepIn(siblingIndex)); diffContext.Edits.Append(RenderTreeEdit.StepIn(diffContext.SiblingIndex));
var childSiblingIndex = 0; var prevSiblingIndex = diffContext.SiblingIndex;
diffContext.SiblingIndex = 0;
AppendDiffEntriesForRange( AppendDiffEntriesForRange(
batchBuilder, ref diffContext,
oldTree, oldFrameAttributesEndIndexExcl, oldFrameChildrenEndIndexExcl, oldFrameAttributesEndIndexExcl, oldFrameChildrenEndIndexExcl,
newTree, newFrameAttributesEndIndexExcl, newFrameChildrenEndIndexExcl, newFrameAttributesEndIndexExcl, newFrameChildrenEndIndexExcl);
ref childSiblingIndex); AppendStepOut(ref diffContext);
AppendStepOut(); diffContext.SiblingIndex = prevSiblingIndex + 1;
siblingIndex++;
} }
else else
{ {
siblingIndex++; diffContext.SiblingIndex++;
} }
} }
else else
{ {
// Elements with different names are treated as completely unrelated // Elements with different names are treated as completely unrelated
RemoveOldFrame(batchBuilder, oldTree, oldFrameIndex, siblingIndex); RemoveOldFrame(ref diffContext, oldFrameIndex);
InsertNewFrame(batchBuilder, newTree, newFrameIndex, ref siblingIndex); InsertNewFrame(ref diffContext, newFrameIndex);
} }
break; break;
} }
@ -372,16 +393,16 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
if (oldFrame.ComponentType == newFrame.ComponentType) if (oldFrame.ComponentType == newFrame.ComponentType)
{ {
UpdateRetainedChildComponent( UpdateRetainedChildComponent(
batchBuilder, ref diffContext,
oldTree, oldFrameIndex, oldFrameIndex,
newTree, newFrameIndex); newFrameIndex);
siblingIndex++; diffContext.SiblingIndex++;
} }
else else
{ {
// Child components of different types are treated as completely unrelated // Child components of different types are treated as completely unrelated
RemoveOldFrame(batchBuilder, oldTree, oldFrameIndex, siblingIndex); RemoveOldFrame(ref diffContext, oldFrameIndex);
InsertNewFrame(batchBuilder, newTree, newFrameIndex, ref siblingIndex); InsertNewFrame(ref diffContext, newFrameIndex);
} }
break; break;
} }
@ -398,11 +419,11 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
{ {
if (oldFrame.AttributeEventHandlerId > 0) if (oldFrame.AttributeEventHandlerId > 0)
{ {
batchBuilder.AddDisposedEventHandlerId(oldFrame.AttributeEventHandlerId); diffContext.BatchBuilder.AddDisposedEventHandlerId(oldFrame.AttributeEventHandlerId);
} }
InitializeNewAttributeFrame(ref newFrame); InitializeNewAttributeFrame(ref diffContext, ref newFrame);
var referenceFrameIndex = _referenceFrames.Append(newFrame); var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame);
_entries.Append(RenderTreeEdit.SetAttribute(siblingIndex, referenceFrameIndex)); diffContext.Edits.Append(RenderTreeEdit.SetAttribute(diffContext.SiblingIndex, referenceFrameIndex));
} }
else if (oldFrame.AttributeEventHandlerId > 0) 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 // 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 // can't have two different attribute names from the same source sequence), we
// could consider removing the 'name equality' check entirely for perf // could consider removing the 'name equality' check entirely for perf
RemoveOldFrame(batchBuilder, oldTree, oldFrameIndex, siblingIndex); RemoveOldFrame(ref diffContext, oldFrameIndex);
InsertNewFrame(batchBuilder, newTree, newFrameIndex, ref siblingIndex); InsertNewFrame(ref diffContext, newFrameIndex);
} }
break; 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]; ref var newFrame = ref newTree[newFrameIndex];
switch (newFrame.FrameType) switch (newFrame.FrameType)
{ {
case RenderTreeFrameType.Attribute: case RenderTreeFrameType.Attribute:
{ {
InitializeNewAttributeFrame(ref newFrame); InitializeNewAttributeFrame(ref diffContext, ref newFrame);
var referenceFrameIndex = _referenceFrames.Append(newFrame); var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame);
_entries.Append(RenderTreeEdit.SetAttribute(siblingIndex, referenceFrameIndex)); diffContext.Edits.Append(RenderTreeEdit.SetAttribute(diffContext.SiblingIndex, referenceFrameIndex));
break; break;
} }
case RenderTreeFrameType.Component: case RenderTreeFrameType.Component:
case RenderTreeFrameType.Element: case RenderTreeFrameType.Element:
{ {
InitializeNewSubtree(batchBuilder, newTree, newFrameIndex); InitializeNewSubtree(ref diffContext, newFrameIndex);
var referenceFrameIndex = _referenceFrames.Append(newTree, newFrameIndex, newFrame.ElementSubtreeLength); var referenceFrameIndex = diffContext.ReferenceFrames.Append(newTree, newFrameIndex, newFrame.ElementSubtreeLength);
_entries.Append(RenderTreeEdit.PrependFrame(siblingIndex, referenceFrameIndex)); diffContext.Edits.Append(RenderTreeEdit.PrependFrame(diffContext.SiblingIndex, referenceFrameIndex));
siblingIndex++; diffContext.SiblingIndex++;
break; break;
} }
case RenderTreeFrameType.Text: case RenderTreeFrameType.Text:
{ {
var referenceFrameIndex = _referenceFrames.Append(newFrame); var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame);
_entries.Append(RenderTreeEdit.PrependFrame(siblingIndex, referenceFrameIndex)); diffContext.Edits.Append(RenderTreeEdit.PrependFrame(diffContext.SiblingIndex, referenceFrameIndex));
siblingIndex++; diffContext.SiblingIndex++;
break; 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]; ref var oldFrame = ref oldTree[oldFrameIndex];
switch (oldFrame.FrameType) switch (oldFrame.FrameType)
{ {
case RenderTreeFrameType.Attribute: case RenderTreeFrameType.Attribute:
{ {
_entries.Append(RenderTreeEdit.RemoveAttribute(siblingIndex, oldFrame.AttributeName)); diffContext.Edits.Append(RenderTreeEdit.RemoveAttribute(diffContext.SiblingIndex, oldFrame.AttributeName));
if (oldFrame.AttributeEventHandlerId > 0) if (oldFrame.AttributeEventHandlerId > 0)
{ {
batchBuilder.AddDisposedEventHandlerId(oldFrame.AttributeEventHandlerId); diffContext.BatchBuilder.AddDisposedEventHandlerId(oldFrame.AttributeEventHandlerId);
} }
break; break;
} }
@ -475,13 +498,13 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
case RenderTreeFrameType.Element: case RenderTreeFrameType.Element:
{ {
var endIndexExcl = oldFrameIndex + oldFrame.ElementSubtreeLength; var endIndexExcl = oldFrameIndex + oldFrame.ElementSubtreeLength;
DisposeFramesInRange(batchBuilder, oldTree, oldFrameIndex, endIndexExcl); DisposeFramesInRange(diffContext.BatchBuilder, oldTree, oldFrameIndex, endIndexExcl);
_entries.Append(RenderTreeEdit.RemoveFrame(siblingIndex)); diffContext.Edits.Append(RenderTreeEdit.RemoveFrame(diffContext.SiblingIndex));
break; break;
} }
case RenderTreeFrameType.Text: case RenderTreeFrameType.Text:
{ {
_entries.Append(RenderTreeEdit.RemoveFrame(siblingIndex)); diffContext.Edits.Append(RenderTreeEdit.RemoveFrame(diffContext.SiblingIndex));
break; break;
} }
} }
@ -502,22 +525,23 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
return index; 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 // If the preceding frame is a StepIn, then the StepOut cancels it out
var previousIndex = _entries.Count - 1; var previousIndex = diffContext.Edits.Count - 1;
if (previousIndex >= 0 && _entries.Buffer[previousIndex].Type == RenderTreeEditType.StepIn) if (previousIndex >= 0 && diffContext.Edits.Buffer[previousIndex].Type == RenderTreeEditType.StepIn)
{ {
_entries.RemoveLast(); diffContext.Edits.RemoveLast();
} }
else 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; var endIndexExcl = frameIndex + frames[frameIndex].ElementSubtreeLength;
for (var i = frameIndex; i < endIndexExcl; i++) for (var i = frameIndex; i < endIndexExcl; i++)
{ {
@ -525,17 +549,18 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
switch (frame.FrameType) switch (frame.FrameType)
{ {
case RenderTreeFrameType.Component: case RenderTreeFrameType.Component:
InitializeNewComponentFrame(batchBuilder, frames, i); InitializeNewComponentFrame(ref diffContext, i);
break; break;
case RenderTreeFrameType.Attribute: case RenderTreeFrameType.Attribute:
InitializeNewAttributeFrame(ref frame); InitializeNewAttributeFrame(ref diffContext, ref frame);
break; 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]; ref var frame = ref frames[frameIndex];
if (frame.Component != null) if (frame.Component != null)
@ -543,7 +568,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
throw new InvalidOperationException($"Child component already exists during {nameof(InitializeNewComponentFrame)}"); throw new InvalidOperationException($"Child component already exists during {nameof(InitializeNewComponentFrame)}");
} }
_renderer.InstantiateChildComponent(ref frame); diffContext.Renderer.InstantiateChildComponent(ref frame);
var childComponentInstance = frame.Component; var childComponentInstance = frame.Component;
// All descendants of a component are its properties // All descendants of a component are its properties
@ -557,45 +582,25 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
attributeFrame.AttributeValue); 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) if (newFrame.AttributeValue is UIEventHandler)
{ {
_renderer.AssignEventHandlerId(ref newFrame); diffContext.Renderer.AssignEventHandlerId(ref newFrame);
} }
} }
private void TriggerChildComponentRender(RenderBatchBuilder batchBuilder, in RenderTreeFrame frame) private static void DisposeFramesInRange(RenderBatchBuilder batchBuilder, RenderTreeFrame[] frames, int startIndex, int endIndexExcl)
{
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)
{ {
for (var i = startIndex; i < endIndexExcl; i++) for (var i = startIndex; i < endIndexExcl; i++)
{ {
ref var frame = ref frames[i]; ref var frame = ref frames[i];
if (frame.FrameType == RenderTreeFrameType.Component && frame.Component != null) 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) 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 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 IComponent _component;
private readonly Renderer _renderer; private readonly Renderer _renderer;
private readonly RenderTreeDiffBuilder _diffBuilder;
private RenderTreeBuilder _renderTreeBuilderCurrent; private RenderTreeBuilder _renderTreeBuilderCurrent;
private RenderTreeBuilder _renderTreeBuilderPrevious; private RenderTreeBuilder _renderTreeBuilderPrevious;
@ -32,7 +31,6 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
_componentId = componentId; _componentId = componentId;
_component = component ?? throw new ArgumentNullException(nameof(component)); _component = component ?? throw new ArgumentNullException(nameof(component));
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
_diffBuilder = new RenderTreeDiffBuilder(renderer);
_renderTreeBuilderCurrent = new RenderTreeBuilder(renderer); _renderTreeBuilderCurrent = new RenderTreeBuilder(renderer);
_renderTreeBuilderPrevious = 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 /// Regenerates the <see cref="RenderTree"/> and adds the changes to the
/// <paramref name="batchBuilder"/>. /// <paramref name="batchBuilder"/>.
/// </summary> /// </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 // Swap the old and new tree builders
(_renderTreeBuilderCurrent, _renderTreeBuilderPrevious) = (_renderTreeBuilderPrevious, _renderTreeBuilderCurrent); (_renderTreeBuilderCurrent, _renderTreeBuilderPrevious) = (_renderTreeBuilderPrevious, _renderTreeBuilderCurrent);
_renderTreeBuilderCurrent.Clear(); _renderTreeBuilderCurrent.Clear();
_component.BuildRenderTree(_renderTreeBuilderCurrent); _component.BuildRenderTree(_renderTreeBuilderCurrent);
_diffBuilder.ApplyNewRenderTreeVersion(
var diff = RenderTreeDiffBuilder.ComputeDiff(
_renderer,
batchBuilder, batchBuilder,
_componentId, _componentId,
_renderTreeBuilderPrevious.GetFrames(), _renderTreeBuilderPrevious.GetFrames(),
_renderTreeBuilderCurrent.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> /// <summary>
@ -66,7 +79,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
disposable.Dispose(); disposable.Dispose();
} }
_diffBuilder.DisposeFrames(batchBuilder, _renderTreeBuilderCurrent.GetFrames()); RenderTreeDiffBuilder.DisposeFrames(batchBuilder, _renderTreeBuilderCurrent.GetFrames());
} }
} }
} }

View File

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

View File

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

View File

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

View File

@ -17,14 +17,12 @@ namespace Microsoft.AspNetCore.Blazor.Test
private readonly Renderer renderer; private readonly Renderer renderer;
private readonly RenderTreeBuilder oldTree; private readonly RenderTreeBuilder oldTree;
private readonly RenderTreeBuilder newTree; private readonly RenderTreeBuilder newTree;
private RenderTreeDiffBuilder diff;
public RenderTreeDiffBuilderTest() public RenderTreeDiffBuilderTest()
{ {
renderer = new FakeRenderer(); renderer = new FakeRenderer();
oldTree = new RenderTreeBuilder(renderer); oldTree = new RenderTreeBuilder(renderer);
newTree = new RenderTreeBuilder(renderer); newTree = new RenderTreeBuilder(renderer);
diff = new RenderTreeDiffBuilder(renderer);
} }
[Theory] [Theory]
@ -36,7 +34,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
appendAction(newTree); appendAction(newTree);
// Act // Act
var result = GetSingleUpdatedComponent(); var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Empty(result.Edits); Assert.Empty(result.Edits);
@ -67,7 +65,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(2, "text2"); newTree.AddText(2, "text2");
// Act // Act
var result = GetSingleUpdatedComponent(); var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -75,7 +73,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
{ {
AssertEdit(entry, RenderTreeEditType.PrependFrame, 1); AssertEdit(entry, RenderTreeEditType.PrependFrame, 1);
Assert.Equal(0, entry.ReferenceFrameIndex); 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"); newTree.AddText(2, "text2");
// Act // Act
var result = GetSingleUpdatedComponent(); var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -109,7 +107,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(0, "x"); // Loop start newTree.AddText(0, "x"); // Loop start
// Act // Act
var result = GetSingleUpdatedComponent(); var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -129,7 +127,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(10, "x"); // Loop start newTree.AddText(10, "x"); // Loop start
// Act // Act
var result = GetSingleUpdatedComponent(); var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -143,9 +141,8 @@ namespace Microsoft.AspNetCore.Blazor.Test
AssertEdit(entry, RenderTreeEditType.PrependFrame, 2); AssertEdit(entry, RenderTreeEditType.PrependFrame, 2);
Assert.Equal(1, entry.ReferenceFrameIndex); Assert.Equal(1, entry.ReferenceFrameIndex);
}); });
Assert.Collection(result.ReferenceFrames, AssertFrame.Text(referenceFrames[0], "x", 11);
frame => AssertFrame.Text(frame, "x", 11), AssertFrame.Text(referenceFrames[1], "x", 12);
frame => AssertFrame.Text(frame, "x", 12));
} }
[Fact] [Fact]
@ -160,7 +157,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(1, "x"); newTree.AddText(1, "x");
// Act // Act
var result = GetSingleUpdatedComponent(); var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -180,7 +177,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(11, "x"); // Will be added newTree.AddText(11, "x"); // Will be added
// Act // Act
var result = GetSingleUpdatedComponent(); var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -194,9 +191,8 @@ namespace Microsoft.AspNetCore.Blazor.Test
AssertEdit(entry, RenderTreeEditType.PrependFrame, 3); AssertEdit(entry, RenderTreeEditType.PrependFrame, 3);
Assert.Equal(1, entry.ReferenceFrameIndex); Assert.Equal(1, entry.ReferenceFrameIndex);
}); });
Assert.Collection(result.ReferenceFrames, AssertFrame.Text(referenceFrames[0], "x", 10);
frame => AssertFrame.Text(frame, "x", 10), AssertFrame.Text(referenceFrames[1], "x", 11);
frame => AssertFrame.Text(frame, "x", 11));
} }
[Fact] [Fact]
@ -211,7 +207,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(12, "x"); newTree.AddText(12, "x");
// Act // Act
var result = GetSingleUpdatedComponent(); var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -225,9 +221,8 @@ namespace Microsoft.AspNetCore.Blazor.Test
AssertEdit(entry, RenderTreeEditType.PrependFrame, 2); AssertEdit(entry, RenderTreeEditType.PrependFrame, 2);
Assert.Equal(1, entry.ReferenceFrameIndex); Assert.Equal(1, entry.ReferenceFrameIndex);
}); });
Assert.Collection(result.ReferenceFrames, AssertFrame.Text(referenceFrames[0], "x", 10);
frame => AssertFrame.Text(frame, "x", 10), AssertFrame.Text(referenceFrames[1], "x", 11);
frame => AssertFrame.Text(frame, "x", 11));
} }
[Fact] [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 newTree.AddText(2, "x"); // Note that the '0' and '1' items are not present on this iteration
// Act // Act
var result = GetSingleUpdatedComponent(); var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -258,7 +253,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(1, "text"); newTree.AddText(1, "text");
// Act // Act
var result = GetSingleUpdatedComponent(); var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -276,7 +271,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(182, "new text 2"); newTree.AddText(182, "new text 2");
// Act // Act
var result = GetSingleUpdatedComponent(); var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -306,7 +301,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement(); newTree.CloseElement();
// Act // Act
var result = GetSingleUpdatedComponent(); var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -327,34 +322,22 @@ namespace Microsoft.AspNetCore.Blazor.Test
GetRenderedBatch(new RenderTreeBuilder(renderer), oldTree); // Assign initial IDs GetRenderedBatch(new RenderTreeBuilder(renderer), oldTree); // Assign initial IDs
newTree.OpenComponent<FakeComponent2>(123); newTree.OpenComponent<FakeComponent2>(123);
newTree.CloseComponent(); newTree.CloseComponent();
var batchBuilder = new RenderBatchBuilder();
// Act // Act
var renderBatch = GetRenderedBatch(); var diff = RenderTreeDiffBuilder.ComputeDiff(renderer, batchBuilder, 0, oldTree.GetFrames(), newTree.GetFrames());
// Assert // Assert: We're going to dispose the old component, and render the new one
Assert.Collection(renderBatch.DisposedComponentIDs, Assert.Equal(new[] { 0 }, batchBuilder.ComponentDisposalQueue);
disposedComponentId => Assert.Equal(0, disposedComponentId)); Assert.Equal(new[] { 1 }, batchBuilder.ComponentRenderQueue);
// Assert: First updated component is the root with one child being // Assert: Got correct info in diff
// prepended, and its earlier incarnation being removed Assert.Collection(diff.Edits,
Assert.Equal(2, renderBatch.UpdatedComponents.Count);
var updatedComponent1 = renderBatch.UpdatedComponents.Array[0];
Assert.Collection(updatedComponent1.Edits,
entry => AssertEdit(entry, RenderTreeEditType.RemoveFrame, 0), entry => AssertEdit(entry, RenderTreeEditType.RemoveFrame, 0),
entry => entry =>
{ {
AssertEdit(entry, RenderTreeEditType.PrependFrame, 0); AssertEdit(entry, RenderTreeEditType.PrependFrame, 0);
Assert.Equal(0, entry.ReferenceFrameIndex); Assert.IsType<FakeComponent2>(batchBuilder.ReferenceFramesBuffer.Buffer[entry.ReferenceFrameIndex].Component);
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);
}); });
} }
@ -371,7 +354,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement(); newTree.CloseElement();
// Act // Act
var result = GetSingleUpdatedComponent(); var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -380,8 +363,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
AssertEdit(entry, RenderTreeEditType.SetAttribute, 0); AssertEdit(entry, RenderTreeEditType.SetAttribute, 0);
Assert.Equal(0, entry.ReferenceFrameIndex); Assert.Equal(0, entry.ReferenceFrameIndex);
}); });
Assert.Collection(result.ReferenceFrames, AssertFrame.Attribute(referenceFrames[0], "added", "added value");
frame => AssertFrame.Attribute(frame, "added", "added value"));
} }
[Fact] [Fact]
@ -397,7 +379,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement(); newTree.CloseElement();
// Act // Act
var result = GetSingleUpdatedComponent(); var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -422,7 +404,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement(); newTree.CloseElement();
// Act // Act
var result = GetSingleUpdatedComponent(); var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -431,8 +413,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
AssertEdit(entry, RenderTreeEditType.SetAttribute, 0); AssertEdit(entry, RenderTreeEditType.SetAttribute, 0);
Assert.Equal(0, entry.ReferenceFrameIndex); Assert.Equal(0, entry.ReferenceFrameIndex);
}); });
Assert.Collection(result.ReferenceFrames, AssertFrame.Attribute(referenceFrames[0], "will change", "did change value");
frame => AssertFrame.Attribute(frame, "will change", "did change value"));
} }
[Fact] [Fact]
@ -452,7 +433,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement(); newTree.CloseElement();
// Act // Act
var result = GetSingleUpdatedComponent(); var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -461,8 +442,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
AssertEdit(entry, RenderTreeEditType.SetAttribute, 0); AssertEdit(entry, RenderTreeEditType.SetAttribute, 0);
Assert.Equal(0, entry.ReferenceFrameIndex); Assert.Equal(0, entry.ReferenceFrameIndex);
}); });
Assert.Collection(result.ReferenceFrames, AssertFrame.Attribute(referenceFrames[0], "will change", addedHandler);
frame => AssertFrame.Attribute(frame, "will change", addedHandler));
} }
[Fact] [Fact]
@ -477,7 +457,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement(); newTree.CloseElement();
// Act // Act
var result = GetSingleUpdatedComponent(); var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -491,8 +471,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
AssertEdit(entry, RenderTreeEditType.SetAttribute, 0); AssertEdit(entry, RenderTreeEditType.SetAttribute, 0);
Assert.Equal(0, entry.ReferenceFrameIndex); Assert.Equal(0, entry.ReferenceFrameIndex);
}); });
Assert.Collection(result.ReferenceFrames, AssertFrame.Attribute(referenceFrames[0], "newname", "same value");
frame => AssertFrame.Attribute(frame, "newname", "same value"));
} }
[Fact] [Fact]
@ -518,7 +497,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement(); newTree.CloseElement();
// Act // Act
var result = GetSingleUpdatedComponent(); var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, 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), entry => AssertEdit(entry, RenderTreeEditType.StepOut, 0),
entry => AssertEdit(entry, RenderTreeEditType.StepOut, 0)); entry => AssertEdit(entry, RenderTreeEditType.StepOut, 0));
Assert.Collection(result.ReferenceFrames, AssertFrame.Text(referenceFrames[0], "grandchild new text", 13);
frame => AssertFrame.Text(frame, "grandchild new text", 13));
} }
[Fact] [Fact]
@ -560,7 +538,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseElement(); newTree.CloseElement();
// Act // Act
var result = GetSingleUpdatedComponent(); var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -571,8 +549,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
Assert.Equal(0, entry.ReferenceFrameIndex); Assert.Equal(0, entry.ReferenceFrameIndex);
}, },
entry => AssertEdit(entry, RenderTreeEditType.StepOut, 0)); entry => AssertEdit(entry, RenderTreeEditType.StepOut, 0));
Assert.Collection(result.ReferenceFrames, AssertFrame.Text(referenceFrames[0], "Text that has changed", 11);
frame => AssertFrame.Text(frame, "Text that has changed", 11));
} }
[Fact] [Fact]
@ -589,7 +566,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddText(13, "text4"); newTree.AddText(13, "text4");
// Act // Act
var result = GetSingleUpdatedComponent(); var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert // Assert
Assert.Collection(result.Edits, Assert.Collection(result.Edits,
@ -598,8 +575,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
AssertEdit(entry, RenderTreeEditType.UpdateText, 1); AssertEdit(entry, RenderTreeEditType.UpdateText, 1);
Assert.Equal(0, entry.ReferenceFrameIndex); Assert.Equal(0, entry.ReferenceFrameIndex);
}); });
Assert.Collection(result.ReferenceFrames, AssertFrame.Text(referenceFrames[0], "text2modified", 11);
frame => AssertFrame.Text(frame, "text2modified", 11));
} }
[Fact] [Fact]
@ -621,11 +597,8 @@ namespace Microsoft.AspNetCore.Blazor.Test
var renderBatch = GetRenderedBatch(); var renderBatch = GetRenderedBatch();
// Assert // Assert
Assert.Equal(3, renderBatch.UpdatedComponents.Count); var diff = renderBatch.UpdatedComponents.Single();
Assert.Collection(diff.Edits,
// First component is the root one
var firstComponentDiff = renderBatch.UpdatedComponents.Array[0];
Assert.Collection(firstComponentDiff.Edits,
entry => AssertEdit(entry, RenderTreeEditType.StepIn, 1), entry => AssertEdit(entry, RenderTreeEditType.StepIn, 1),
entry => entry =>
{ {
@ -638,23 +611,8 @@ namespace Microsoft.AspNetCore.Blazor.Test
Assert.Equal(1, entry.ReferenceFrameIndex); Assert.Equal(1, entry.ReferenceFrameIndex);
}, },
entry => AssertEdit(entry, RenderTreeEditType.StepOut, 0)); entry => AssertEdit(entry, RenderTreeEditType.StepOut, 0));
Assert.Collection(firstComponentDiff.ReferenceFrames, AssertFrame.ComponentWithInstance<FakeComponent>(renderBatch.ReferenceFrames.Array[0], 0, 12);
frame => AssertFrame.ComponentWithInstance<FakeComponent>(frame, 0, 12), AssertFrame.ComponentWithInstance<FakeComponent2>(renderBatch.ReferenceFrames.Array[1], 1, 13);
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)}"));
} }
[Fact] [Fact]
@ -673,8 +631,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
var componentInstance = newTree.GetFrames().First().Component as FakeComponent; var componentInstance = newTree.GetFrames().First().Component as FakeComponent;
// Assert // Assert
Assert.Equal(2, renderBatch.UpdatedComponents.Count); Assert.Equal(1, renderBatch.UpdatedComponents.Count);
var rootComponentDiff = renderBatch.UpdatedComponents.Array[0]; var rootComponentDiff = renderBatch.UpdatedComponents.Array[0];
AssertEdit(rootComponentDiff.Edits.Single(), RenderTreeEditType.PrependFrame, 0); AssertEdit(rootComponentDiff.Edits.Single(), RenderTreeEditType.PrependFrame, 0);
Assert.NotNull(componentInstance); Assert.NotNull(componentInstance);
@ -695,7 +652,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Act/Assert // Act/Assert
var ex = Assert.Throws<InvalidOperationException>(() => 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); 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 // Act/Assert
var ex = Assert.Throws<InvalidOperationException>(() => 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 " + Assert.StartsWith($"Unable to set property '{nameof(FakeComponent.ReadonlyProperty)}' on " +
$"component of type '{typeof(FakeComponent).FullName}'.", ex.Message); $"component of type '{typeof(FakeComponent).FullName}'.", ex.Message);
@ -735,14 +692,14 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.CloseComponent(); // </FakeComponent> newTree.CloseComponent(); // </FakeComponent>
newTree.OpenComponent<FakeComponent2>(13); // 3: <FakeComponent2> newTree.OpenComponent<FakeComponent2>(13); // 3: <FakeComponent2>
newTree.CloseComponent(); // </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 originalFakeComponentInstance = oldTree.GetFrames().Array[2].Component;
var originalFakeComponent2Instance = oldTree.GetFrames().Array[3].Component; var originalFakeComponent2Instance = oldTree.GetFrames().Array[3].Component;
// Act // Act
var result = GetSingleUpdatedComponent(); var (result, referenceFrames) = GetSingleUpdatedComponent();
var newFrame1 = newTree.GetFrames().Array[2]; var newFrame1 = newTree.GetFrames().Array[2];
var newFrame2 = newTree.GetFrames().Array[3]; var newFrame2 = newTree.GetFrames().Array[3];
@ -768,7 +725,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.AddAttribute(14, nameof(FakeComponent.ObjectProperty), objectWillNotChange); newTree.AddAttribute(14, nameof(FakeComponent.ObjectProperty), objectWillNotChange);
newTree.CloseComponent(); 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; var originalComponentInstance = (FakeComponent)oldTree.GetFrames().Array[0].Component;
originalComponentInstance.ObjectProperty = null; // So we can see it doesn't get reassigned originalComponentInstance.ObjectProperty = null; // So we can see it doesn't get reassigned
@ -777,67 +734,14 @@ namespace Microsoft.AspNetCore.Blazor.Test
var newComponentInstance = (FakeComponent)oldTree.GetFrames().Array[0].Component; var newComponentInstance = (FakeComponent)oldTree.GetFrames().Array[0].Component;
// Assert // 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.Same(originalComponentInstance, newComponentInstance);
Assert.Equal("String did change", newComponentInstance.StringProperty); Assert.Equal("String did change", newComponentInstance.StringProperty);
Assert.Null(newComponentInstance.ObjectProperty); // To observe that the property wasn't even written, we nulled it out on the original Assert.Null(newComponentInstance.ObjectProperty); // To observe that the property wasn't even written, we nulled it out on the original
} }
[Fact] [Fact]
public void NotifiesIHandlePropertiesChangedBeforeFirstRender() public void QueuesRemovedChildComponentsForDisposal()
{
// 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()
{ {
// Arrange // Arrange
oldTree.OpenComponent<DisposableComponent>(10); // <DisposableComponent> oldTree.OpenComponent<DisposableComponent>(10); // <DisposableComponent>
@ -849,29 +753,23 @@ namespace Microsoft.AspNetCore.Blazor.Test
newTree.OpenComponent<DisposableComponent>(30); // <DisposableComponent> newTree.OpenComponent<DisposableComponent>(30); // <DisposableComponent>
newTree.CloseComponent(); // </DisposableComponent> newTree.CloseComponent(); // </DisposableComponent>
diff.ApplyNewRenderTreeVersion(new RenderBatchBuilder(), 0, new RenderTreeBuilder(renderer).GetFrames(), oldTree.GetFrames()); var batchBuilder = new RenderBatchBuilder();
var disposableComponent1 = (DisposableComponent)oldTree.GetFrames().Array[0].Component; RenderTreeDiffBuilder.ComputeDiff(renderer, batchBuilder, 0, new RenderTreeBuilder(renderer).GetFrames(), oldTree.GetFrames());
var nonDisposableComponent = (NonDisposableComponent)oldTree.GetFrames().Array[1].Component;
var disposableComponent2 = (DisposableComponent)oldTree.GetFrames().Array[2].Component;
// Act // Act/Assert
var renderedBatch = GetRenderedBatch(); // 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: We track NonDisposableComponent was disposed even though it's not IDisposable Assert.Empty(batchBuilder.ComponentDisposalQueue);
Assert.Equal(renderedBatch.DisposedComponentIDs, new[] { 0, 1 }); RenderTreeDiffBuilder.ComputeDiff(renderer, batchBuilder, 0, oldTree.GetFrames(), newTree.GetFrames());
Assert.Equal(new[] { 0, 1 }, batchBuilder.ComponentDisposalQueue);
// 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);
} }
private RenderTreeDiff GetSingleUpdatedComponent() private (RenderTreeDiff, RenderTreeFrame[]) GetSingleUpdatedComponent()
{ {
var diffsInBatch = GetRenderedBatch().UpdatedComponents; var batch = GetRenderedBatch();
var diffsInBatch = batch.UpdatedComponents;
Assert.Equal(1, diffsInBatch.Count); Assert.Equal(1, diffsInBatch.Count);
return diffsInBatch.Array[0]; return (diffsInBatch.Array[0], batch.ReferenceFrames.ToArray());
} }
private RenderBatch GetRenderedBatch() private RenderBatch GetRenderedBatch()
@ -880,7 +778,8 @@ namespace Microsoft.AspNetCore.Blazor.Test
private RenderBatch GetRenderedBatch(RenderTreeBuilder from, RenderTreeBuilder to) private RenderBatch GetRenderedBatch(RenderTreeBuilder from, RenderTreeBuilder to)
{ {
var batchBuilder = new RenderBatchBuilder(); 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(); 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 private class DisposableComponent : IComponent, IDisposable
{ {
public int DisposalCount { get; private set; } public int DisposalCount { get; private set; }

View File

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