diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts index 506846af88..4b118fb490 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts @@ -15,31 +15,32 @@ export class BrowserRenderer { this.childComponentLocations[componentId] = element; } - public updateComponent(componentId: number, edits: System_Array, editsLength: number, referenceTree: System_Array) { + public updateComponent(componentId: number, edits: System_Array, editsOffset: number, editsLength: number, referenceFrames: System_Array) { const element = this.childComponentLocations[componentId]; if (!element) { throw new Error(`No element is currently associated with component ${componentId}`); } - this.applyEdits(componentId, element, 0, edits, editsLength, referenceTree); + this.applyEdits(componentId, element, 0, edits, editsOffset, editsLength, referenceFrames); } public disposeComponent(componentId: number) { delete this.childComponentLocations[componentId]; } - applyEdits(componentId: number, parent: Element, childIndex: number, edits: System_Array, editsLength: number, referenceTree: System_Array) { + applyEdits(componentId: number, parent: Element, childIndex: number, edits: System_Array, editsOffset: number, editsLength: number, referenceFrames: System_Array) { let currentDepth = 0; let childIndexAtCurrentDepth = childIndex; - for (let editIndex = 0; editIndex < editsLength; editIndex++) { + const maxEditIndexExcl = editsOffset + editsLength; + for (let editIndex = editsOffset; editIndex < maxEditIndexExcl; editIndex++) { const edit = getRenderTreeEditPtr(edits, editIndex); const editType = renderTreeEdit.type(edit); switch (editType) { case EditType.prependFrame: { const frameIndex = renderTreeEdit.newTreeIndex(edit); - const frame = getTreeFramePtr(referenceTree, frameIndex); + const frame = getTreeFramePtr(referenceFrames, frameIndex); const siblingIndex = renderTreeEdit.siblingIndex(edit); - this.insertFrame(componentId, parent, childIndexAtCurrentDepth + siblingIndex, referenceTree, frame, frameIndex); + this.insertFrame(componentId, parent, childIndexAtCurrentDepth + siblingIndex, referenceFrames, frame, frameIndex); break; } case EditType.removeFrame: { @@ -49,7 +50,7 @@ export class BrowserRenderer { } case EditType.setAttribute: { const frameIndex = renderTreeEdit.newTreeIndex(edit); - const frame = getTreeFramePtr(referenceTree, frameIndex); + const frame = getTreeFramePtr(referenceFrames, frameIndex); const siblingIndex = renderTreeEdit.siblingIndex(edit); const element = parent.childNodes[childIndexAtCurrentDepth + siblingIndex] as HTMLElement; this.applyAttribute(componentId, element, frame); @@ -62,7 +63,7 @@ export class BrowserRenderer { } case EditType.updateText: { const frameIndex = renderTreeEdit.newTreeIndex(edit); - const frame = getTreeFramePtr(referenceTree, frameIndex); + const frame = getTreeFramePtr(referenceFrames, frameIndex); const siblingIndex = renderTreeEdit.siblingIndex(edit); const domTextNode = parent.childNodes[childIndexAtCurrentDepth + siblingIndex] as Text; domTextNode.textContent = renderTreeFrame.textContent(frame); diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderBatch.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderBatch.ts index bb7d8cb7a2..6beff2ec87 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderBatch.ts +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderBatch.ts @@ -7,7 +7,8 @@ import { RenderTreeEditPointer } from './RenderTreeEdit'; export const renderBatch = { updatedComponents: (obj: RenderBatchPointer) => platform.readStructField>(obj, 0), - disposedComponentIds: (obj: RenderBatchPointer) => platform.readStructField>(obj, arrayRangeStructLength), + referenceFrames: (obj: RenderBatchPointer) => platform.readStructField>(obj, arrayRangeStructLength), + disposedComponentIds: (obj: RenderBatchPointer) => platform.readStructField>(obj, arrayRangeStructLength + arrayRangeStructLength), }; const arrayRangeStructLength = 8; @@ -16,15 +17,22 @@ export const arrayRange = { count: (obj: ArrayRangePointer) => platform.readInt32Field(obj, 4), }; -export const renderTreeDiffStructLength = 4 + 2 * arrayRangeStructLength; +const arraySegmentStructLength = 12; +export const arraySegment = { + array: (obj: ArraySegmentPointer) => platform.readObjectField>(obj, 0), + offset: (obj: ArraySegmentPointer) => platform.readInt32Field(obj, 4), + count: (obj: ArraySegmentPointer) => platform.readInt32Field(obj, 8), +}; + +export const renderTreeDiffStructLength = 4 + arraySegmentStructLength; export const renderTreeDiff = { componentId: (obj: RenderTreeDiffPointer) => platform.readInt32Field(obj, 0), - edits: (obj: RenderTreeDiffPointer) => platform.readStructField>(obj, 4), - currentState: (obj: RenderTreeDiffPointer) => platform.readStructField>(obj, 4 + arrayRangeStructLength), + edits: (obj: RenderTreeDiffPointer) => platform.readStructField>(obj, 4), }; // Nominal types to ensure only valid pointers are passed to the functions above. // At runtime the values are just numbers. export interface RenderBatchPointer extends Pointer { RenderBatchPointer__DO_NOT_IMPLEMENT: any } export interface ArrayRangePointer extends Pointer { ArrayRangePointer__DO_NOT_IMPLEMENT: any } +export interface ArraySegmentPointer extends Pointer { ArraySegmentPointer__DO_NOT_IMPLEMENT: any } export interface RenderTreeDiffPointer extends Pointer { RenderTreeDiffPointer__DO_NOT_IMPLEMENT: any } diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/Renderer.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/Renderer.ts index 15a2a4b17a..bae0f92a05 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/Renderer.ts +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/Renderer.ts @@ -1,6 +1,6 @@ import { System_Object, System_String, System_Array, MethodHandle, Pointer } from '../Platform/Platform'; import { platform } from '../Environment'; -import { renderBatch as renderBatchStruct, arrayRange, renderTreeDiffStructLength, renderTreeDiff, RenderBatchPointer, RenderTreeDiffPointer } from './RenderBatch'; +import { renderBatch as renderBatchStruct, arrayRange, arraySegment, renderTreeDiffStructLength, renderTreeDiff, RenderBatchPointer, RenderTreeDiffPointer } from './RenderBatch'; import { BrowserRenderer } from './BrowserRenderer'; type BrowserRendererRegistry = { [browserRendererId: number]: BrowserRenderer }; @@ -30,17 +30,19 @@ export function renderBatch(browserRendererId: number, batch: RenderBatchPointer const updatedComponents = renderBatchStruct.updatedComponents(batch); const updatedComponentsLength = arrayRange.count(updatedComponents); const updatedComponentsArray = arrayRange.array(updatedComponents); + const referenceFramesStruct = renderBatchStruct.referenceFrames(batch); + const referenceFrames = arrayRange.array(referenceFramesStruct); + for (let i = 0; i < updatedComponentsLength; i++) { const diff = platform.getArrayEntryPtr(updatedComponentsArray, i, renderTreeDiffStructLength); const componentId = renderTreeDiff.componentId(diff); - const editsArrayRange = renderTreeDiff.edits(diff); - const currentStateArrayRange = renderTreeDiff.currentState(diff); + const editsArraySegment = renderTreeDiff.edits(diff); + const edits = arraySegment.array(editsArraySegment); + const editsOffset = arraySegment.offset(editsArraySegment); + const editsLength = arraySegment.count(editsArraySegment); - const edits = arrayRange.array(editsArrayRange); - const editsLength = arrayRange.count(editsArrayRange); - const tree = arrayRange.array(currentStateArrayRange); - browserRenderer.updateComponent(componentId, edits, editsLength, tree); + browserRenderer.updateComponent(componentId, edits, editsOffset, editsLength, referenceFrames); } const disposedComponentIds = renderBatchStruct.disposedComponentIds(batch); diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/ArrayBuilder.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/ArrayBuilder.cs index 1a68fcdf5a..0350c726e3 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/ArrayBuilder.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/ArrayBuilder.cs @@ -132,6 +132,15 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree public ArrayRange ToRange() => new ArrayRange(_items, _itemsInUse); + /// + /// Produces an structure describing the selected contents. + /// + /// The index of the first item in the segment. + /// One plus the index of the last item in the segment. + /// The . + public ArraySegment ToSegment(int fromIndexInclusive, int toIndexExclusive) + => new ArraySegment(_items, fromIndexInclusive, toIndexExclusive - fromIndexInclusive); + private void SetCapacity(int desiredCapacity, bool preserveContents) { if (desiredCapacity < _itemsInUse) diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiff.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiff.cs index 568b7029dd..e7351013aa 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiff.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiff.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; + namespace Microsoft.AspNetCore.Blazor.RenderTree { /// @@ -16,23 +18,14 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree /// /// Gets the changes to the render tree since a previous state. /// - public readonly ArrayRange Edits; - - /// - /// Gets render frames that may be referenced by entries in . - /// For example, edit entries of type - /// will point to an entry in this array to specify the subtree to be prepended. - /// - public readonly ArrayRange ReferenceFrames; + public readonly ArraySegment Edits; internal RenderTreeDiff( int componentId, - ArrayRange entries, - ArrayRange referenceFrames) + ArraySegment entries) { ComponentId = componentId; Edits = entries; - ReferenceFrames = referenceFrames; } } } diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs index f50cf34c11..6c16a78380 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs @@ -8,50 +8,69 @@ using Microsoft.AspNetCore.Blazor.Rendering; namespace Microsoft.AspNetCore.Blazor.RenderTree { - internal class RenderTreeDiffBuilder + internal static class RenderTreeDiffBuilder { - private readonly Renderer _renderer; - private readonly ArrayBuilder _entries = new ArrayBuilder(10); - private readonly ArrayBuilder _referenceFrames = new ArrayBuilder(10); - - public RenderTreeDiffBuilder(Renderer renderer) + private struct DiffContext { - _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); + // Exists only so that the various methods in this class can call each other without + // constantly building up long lists of parameters. Is private to this class, so the + // fact that it's a mutable struct is manageable. + // Always pass by ref to avoid copying, and because the 'SiblingIndex' is mutable. + + public readonly Renderer Renderer; + public readonly RenderBatchBuilder BatchBuilder; + public readonly RenderTreeFrame[] OldTree; + public readonly RenderTreeFrame[] NewTree; + public readonly ArrayBuilder Edits; + public readonly ArrayBuilder 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; + } } - /// - /// As well as computing the diff between the two trees, this method also has the side-effect - /// of instantiating child components on newly-inserted Component frames, and copying the existing - /// component instances onto retained Component frames. It's particularly convenient to do that - /// here because we have the right information and are already walking the trees to do the diff. - /// - public void ApplyNewRenderTreeVersion( + public static RenderTreeDiff ComputeDiff( + Renderer renderer, RenderBatchBuilder batchBuilder, int componentId, ArrayRange oldTree, ArrayRange newTree) { - _entries.Clear(); - _referenceFrames.Clear(); - var siblingIndex = 0; + var editsBuffer = batchBuilder.EditsBuffer; + var editsBufferStartLength = editsBuffer.Count; - var slotId = batchBuilder.ReserveUpdatedComponentSlotId(); - AppendDiffEntriesForRange(batchBuilder, oldTree.Array, 0, oldTree.Count, newTree.Array, 0, newTree.Count, ref siblingIndex); - batchBuilder.SetUpdatedComponent( - slotId, - new RenderTreeDiff(componentId, _entries.ToRange(), _referenceFrames.ToRange())); + var diffContext = new DiffContext(renderer, batchBuilder, oldTree.Array, newTree.Array); + AppendDiffEntriesForRange(ref diffContext, 0, oldTree.Count, 0, newTree.Count); + + var editsSegment = editsBuffer.ToSegment(editsBufferStartLength, editsBuffer.Count); + return new RenderTreeDiff(componentId, editsSegment); } - private void AppendDiffEntriesForRange( - RenderBatchBuilder batchBuilder, - RenderTreeFrame[] oldTree, int oldStartIndex, int oldEndIndexExcl, - RenderTreeFrame[] newTree, int newStartIndex, int newEndIndexExcl, - ref int siblingIndex) + public static void DisposeFrames(RenderBatchBuilder batchBuilder, ArrayRange frames) + => DisposeFramesInRange(batchBuilder, frames.Array, 0, frames.Count); + + private static void AppendDiffEntriesForRange( + ref DiffContext diffContext, + int oldStartIndex, int oldEndIndexExcl, + int newStartIndex, int newEndIndexExcl) { var hasMoreOld = oldEndIndexExcl > oldStartIndex; var hasMoreNew = newEndIndexExcl > newStartIndex; var prevOldSeq = -1; var prevNewSeq = -1; + var oldTree = diffContext.OldTree; + var newTree = diffContext.NewTree; while (hasMoreOld || hasMoreNew) { var oldSeq = hasMoreOld ? oldTree[oldStartIndex].Sequence : int.MaxValue; @@ -59,7 +78,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree if (oldSeq == newSeq) { - AppendDiffEntriesForFramesWithSameSequence(batchBuilder, oldTree, oldStartIndex, newTree, newStartIndex, ref siblingIndex); + AppendDiffEntriesForFramesWithSameSequence(ref diffContext, oldStartIndex, newStartIndex); oldStartIndex = NextSiblingIndex(oldTree[oldStartIndex], oldStartIndex); newStartIndex = NextSiblingIndex(newTree[newStartIndex], newStartIndex); hasMoreOld = oldEndIndexExcl > oldStartIndex; @@ -134,14 +153,14 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree if (treatAsInsert) { - InsertNewFrame(batchBuilder, newTree, newStartIndex, ref siblingIndex); + InsertNewFrame(ref diffContext, newStartIndex); newStartIndex = NextSiblingIndex(newTree[newStartIndex], newStartIndex); hasMoreNew = newEndIndexExcl > newStartIndex; prevNewSeq = newSeq; } else { - RemoveOldFrame(batchBuilder, oldTree, oldStartIndex, siblingIndex); + RemoveOldFrame(ref diffContext, oldStartIndex); oldStartIndex = NextSiblingIndex(oldTree[oldStartIndex], oldStartIndex); hasMoreOld = oldEndIndexExcl > oldStartIndex; prevOldSeq = oldSeq; @@ -150,16 +169,18 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree } } - private void UpdateRetainedChildComponent( - RenderBatchBuilder batchBuilder, - RenderTreeFrame[] oldTree, int oldComponentIndex, - RenderTreeFrame[] newTree, int newComponentIndex) + private static void UpdateRetainedChildComponent( + ref DiffContext diffContext, + int oldComponentIndex, + int newComponentIndex) { // The algorithm here is the same as in AppendDiffEntriesForRange, except that // here we don't optimise for loops - we assume that both sequences are forward-only. // That's because this is true for all currently supported scenarios, and it means // fewer steps here. + var oldTree = diffContext.OldTree; + var newTree = diffContext.NewTree; ref var oldComponentFrame = ref oldTree[oldComponentIndex]; ref var newComponentFrame = ref newTree[newComponentIndex]; var componentId = oldComponentFrame.ComponentId; @@ -241,7 +262,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree if (hasSetAnyProperty) { - TriggerChildComponentRender(batchBuilder, newComponentFrame); + diffContext.BatchBuilder.ComponentRenderQueue.Enqueue(newComponentFrame.ComponentId); } } @@ -291,12 +312,13 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree return frameIndex + distanceToNextSibling; } - private void AppendDiffEntriesForFramesWithSameSequence( - RenderBatchBuilder batchBuilder, - RenderTreeFrame[] oldTree, int oldFrameIndex, - RenderTreeFrame[] newTree, int newFrameIndex, - ref int siblingIndex) + private static void AppendDiffEntriesForFramesWithSameSequence( + ref DiffContext diffContext, + int oldFrameIndex, + int newFrameIndex) { + var oldTree = diffContext.OldTree; + var newTree = diffContext.NewTree; ref var oldFrame = ref oldTree[oldFrameIndex]; ref var newFrame = ref newTree[newFrameIndex]; @@ -304,7 +326,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree // to the same sequence number (and if not, the behaviour is undefined). // TODO: Consider supporting dissimilar types at same sequence for custom IComponent implementations. // It should only be a matter of calling RemoveOldFrame+InsertNewFrame - switch (newTree[newFrameIndex].FrameType) + switch (newFrame.FrameType) { case RenderTreeFrameType.Text: { @@ -312,10 +334,10 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree var newText = newFrame.TextContent; if (!string.Equals(oldText, newText, StringComparison.Ordinal)) { - var referenceFrameIndex = _referenceFrames.Append(newFrame); - _entries.Append(RenderTreeEdit.UpdateText(siblingIndex, referenceFrameIndex)); + var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame); + diffContext.Edits.Append(RenderTreeEdit.UpdateText(diffContext.SiblingIndex, referenceFrameIndex)); } - siblingIndex++; + diffContext.SiblingIndex++; break; } @@ -330,10 +352,9 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree // Diff the attributes AppendDiffEntriesForRange( - batchBuilder, - oldTree, oldFrameIndex + 1, oldFrameAttributesEndIndexExcl, - newTree, newFrameIndex + 1, newFrameAttributesEndIndexExcl, - ref siblingIndex); + ref diffContext, + oldFrameIndex + 1, oldFrameAttributesEndIndexExcl, + newFrameIndex + 1, newFrameAttributesEndIndexExcl); // Diff the children var oldFrameChildrenEndIndexExcl = oldFrameIndex + oldFrame.ElementSubtreeLength; @@ -343,26 +364,26 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree newFrameChildrenEndIndexExcl > newFrameAttributesEndIndexExcl; if (hasChildrenToProcess) { - _entries.Append(RenderTreeEdit.StepIn(siblingIndex)); - var childSiblingIndex = 0; + diffContext.Edits.Append(RenderTreeEdit.StepIn(diffContext.SiblingIndex)); + var prevSiblingIndex = diffContext.SiblingIndex; + diffContext.SiblingIndex = 0; AppendDiffEntriesForRange( - batchBuilder, - oldTree, oldFrameAttributesEndIndexExcl, oldFrameChildrenEndIndexExcl, - newTree, newFrameAttributesEndIndexExcl, newFrameChildrenEndIndexExcl, - ref childSiblingIndex); - AppendStepOut(); - siblingIndex++; + ref diffContext, + oldFrameAttributesEndIndexExcl, oldFrameChildrenEndIndexExcl, + newFrameAttributesEndIndexExcl, newFrameChildrenEndIndexExcl); + AppendStepOut(ref diffContext); + diffContext.SiblingIndex = prevSiblingIndex + 1; } else { - siblingIndex++; + diffContext.SiblingIndex++; } } else { // Elements with different names are treated as completely unrelated - RemoveOldFrame(batchBuilder, oldTree, oldFrameIndex, siblingIndex); - InsertNewFrame(batchBuilder, newTree, newFrameIndex, ref siblingIndex); + RemoveOldFrame(ref diffContext, oldFrameIndex); + InsertNewFrame(ref diffContext, newFrameIndex); } break; } @@ -372,16 +393,16 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree if (oldFrame.ComponentType == newFrame.ComponentType) { UpdateRetainedChildComponent( - batchBuilder, - oldTree, oldFrameIndex, - newTree, newFrameIndex); - siblingIndex++; + ref diffContext, + oldFrameIndex, + newFrameIndex); + diffContext.SiblingIndex++; } else { // Child components of different types are treated as completely unrelated - RemoveOldFrame(batchBuilder, oldTree, oldFrameIndex, siblingIndex); - InsertNewFrame(batchBuilder, newTree, newFrameIndex, ref siblingIndex); + RemoveOldFrame(ref diffContext, oldFrameIndex); + InsertNewFrame(ref diffContext, newFrameIndex); } break; } @@ -398,11 +419,11 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree { if (oldFrame.AttributeEventHandlerId > 0) { - batchBuilder.AddDisposedEventHandlerId(oldFrame.AttributeEventHandlerId); + diffContext.BatchBuilder.AddDisposedEventHandlerId(oldFrame.AttributeEventHandlerId); } - InitializeNewAttributeFrame(ref newFrame); - var referenceFrameIndex = _referenceFrames.Append(newFrame); - _entries.Append(RenderTreeEdit.SetAttribute(siblingIndex, referenceFrameIndex)); + InitializeNewAttributeFrame(ref diffContext, ref newFrame); + var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame); + diffContext.Edits.Append(RenderTreeEdit.SetAttribute(diffContext.SiblingIndex, referenceFrameIndex)); } else if (oldFrame.AttributeEventHandlerId > 0) { @@ -415,8 +436,8 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree // Since this code path is never reachable for Razor components (because you // can't have two different attribute names from the same source sequence), we // could consider removing the 'name equality' check entirely for perf - RemoveOldFrame(batchBuilder, oldTree, oldFrameIndex, siblingIndex); - InsertNewFrame(batchBuilder, newTree, newFrameIndex, ref siblingIndex); + RemoveOldFrame(ref diffContext, oldFrameIndex); + InsertNewFrame(ref diffContext, newFrameIndex); } break; } @@ -426,48 +447,50 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree } } - private void InsertNewFrame(RenderBatchBuilder batchBuilder, RenderTreeFrame[] newTree, int newFrameIndex, ref int siblingIndex) + private static void InsertNewFrame(ref DiffContext diffContext, int newFrameIndex) { + var newTree = diffContext.NewTree; ref var newFrame = ref newTree[newFrameIndex]; switch (newFrame.FrameType) { case RenderTreeFrameType.Attribute: { - InitializeNewAttributeFrame(ref newFrame); - var referenceFrameIndex = _referenceFrames.Append(newFrame); - _entries.Append(RenderTreeEdit.SetAttribute(siblingIndex, referenceFrameIndex)); + InitializeNewAttributeFrame(ref diffContext, ref newFrame); + var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame); + diffContext.Edits.Append(RenderTreeEdit.SetAttribute(diffContext.SiblingIndex, referenceFrameIndex)); break; } case RenderTreeFrameType.Component: case RenderTreeFrameType.Element: { - InitializeNewSubtree(batchBuilder, newTree, newFrameIndex); - var referenceFrameIndex = _referenceFrames.Append(newTree, newFrameIndex, newFrame.ElementSubtreeLength); - _entries.Append(RenderTreeEdit.PrependFrame(siblingIndex, referenceFrameIndex)); - siblingIndex++; + InitializeNewSubtree(ref diffContext, newFrameIndex); + var referenceFrameIndex = diffContext.ReferenceFrames.Append(newTree, newFrameIndex, newFrame.ElementSubtreeLength); + diffContext.Edits.Append(RenderTreeEdit.PrependFrame(diffContext.SiblingIndex, referenceFrameIndex)); + diffContext.SiblingIndex++; break; } case RenderTreeFrameType.Text: { - var referenceFrameIndex = _referenceFrames.Append(newFrame); - _entries.Append(RenderTreeEdit.PrependFrame(siblingIndex, referenceFrameIndex)); - siblingIndex++; + var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame); + diffContext.Edits.Append(RenderTreeEdit.PrependFrame(diffContext.SiblingIndex, referenceFrameIndex)); + diffContext.SiblingIndex++; break; } } } - private void RemoveOldFrame(RenderBatchBuilder batchBuilder, RenderTreeFrame[] oldTree, int oldFrameIndex, int siblingIndex) + private static void RemoveOldFrame(ref DiffContext diffContext, int oldFrameIndex) { + var oldTree = diffContext.OldTree; ref var oldFrame = ref oldTree[oldFrameIndex]; switch (oldFrame.FrameType) { case RenderTreeFrameType.Attribute: { - _entries.Append(RenderTreeEdit.RemoveAttribute(siblingIndex, oldFrame.AttributeName)); + diffContext.Edits.Append(RenderTreeEdit.RemoveAttribute(diffContext.SiblingIndex, oldFrame.AttributeName)); if (oldFrame.AttributeEventHandlerId > 0) { - batchBuilder.AddDisposedEventHandlerId(oldFrame.AttributeEventHandlerId); + diffContext.BatchBuilder.AddDisposedEventHandlerId(oldFrame.AttributeEventHandlerId); } break; } @@ -475,13 +498,13 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree case RenderTreeFrameType.Element: { var endIndexExcl = oldFrameIndex + oldFrame.ElementSubtreeLength; - DisposeFramesInRange(batchBuilder, oldTree, oldFrameIndex, endIndexExcl); - _entries.Append(RenderTreeEdit.RemoveFrame(siblingIndex)); + DisposeFramesInRange(diffContext.BatchBuilder, oldTree, oldFrameIndex, endIndexExcl); + diffContext.Edits.Append(RenderTreeEdit.RemoveFrame(diffContext.SiblingIndex)); break; } case RenderTreeFrameType.Text: { - _entries.Append(RenderTreeEdit.RemoveFrame(siblingIndex)); + diffContext.Edits.Append(RenderTreeEdit.RemoveFrame(diffContext.SiblingIndex)); break; } } @@ -502,22 +525,23 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree return index; } - private void AppendStepOut() + private static void AppendStepOut(ref DiffContext diffContext) { // If the preceding frame is a StepIn, then the StepOut cancels it out - var previousIndex = _entries.Count - 1; - if (previousIndex >= 0 && _entries.Buffer[previousIndex].Type == RenderTreeEditType.StepIn) + var previousIndex = diffContext.Edits.Count - 1; + if (previousIndex >= 0 && diffContext.Edits.Buffer[previousIndex].Type == RenderTreeEditType.StepIn) { - _entries.RemoveLast(); + diffContext.Edits.RemoveLast(); } else { - _entries.Append(RenderTreeEdit.StepOut()); + diffContext.Edits.Append(RenderTreeEdit.StepOut()); } } - private void InitializeNewSubtree(RenderBatchBuilder batchBuilder, RenderTreeFrame[] frames, int frameIndex) + private static void InitializeNewSubtree(ref DiffContext diffContext, int frameIndex) { + var frames = diffContext.NewTree; var endIndexExcl = frameIndex + frames[frameIndex].ElementSubtreeLength; for (var i = frameIndex; i < endIndexExcl; i++) { @@ -525,17 +549,18 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree switch (frame.FrameType) { case RenderTreeFrameType.Component: - InitializeNewComponentFrame(batchBuilder, frames, i); + InitializeNewComponentFrame(ref diffContext, i); break; case RenderTreeFrameType.Attribute: - InitializeNewAttributeFrame(ref frame); + InitializeNewAttributeFrame(ref diffContext, ref frame); break; } } } - private void InitializeNewComponentFrame(RenderBatchBuilder batchBuilder, RenderTreeFrame[] frames, int frameIndex) + private static void InitializeNewComponentFrame(ref DiffContext diffContext, int frameIndex) { + var frames = diffContext.NewTree; ref var frame = ref frames[frameIndex]; if (frame.Component != null) @@ -543,7 +568,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree throw new InvalidOperationException($"Child component already exists during {nameof(InitializeNewComponentFrame)}"); } - _renderer.InstantiateChildComponent(ref frame); + diffContext.Renderer.InstantiateChildComponent(ref frame); var childComponentInstance = frame.Component; // All descendants of a component are its properties @@ -557,45 +582,25 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree attributeFrame.AttributeValue); } - TriggerChildComponentRender(batchBuilder, frame); + diffContext.BatchBuilder.ComponentRenderQueue.Enqueue(frame.ComponentId); } - private void InitializeNewAttributeFrame(ref RenderTreeFrame newFrame) + private static void InitializeNewAttributeFrame(ref DiffContext diffContext, ref RenderTreeFrame newFrame) { if (newFrame.AttributeValue is UIEventHandler) { - _renderer.AssignEventHandlerId(ref newFrame); + diffContext.Renderer.AssignEventHandlerId(ref newFrame); } } - private void TriggerChildComponentRender(RenderBatchBuilder batchBuilder, in RenderTreeFrame frame) - { - if (frame.Component is IHandlePropertiesChanged notifyableComponent) - { - // TODO: Ensure any exceptions thrown here are handled equivalently to - // unhandled exceptions during rendering. - notifyableComponent.OnPropertiesChanged(); - } - - // TODO: Consider moving the responsibility for triggering re-rendering - // into the OnPropertiesChanged handler (if implemented) so that components - // can control whether any given set of property changes cause re-rendering. - // Not doing so yet because it's unclear that the usage patterns would be - // good to use. - _renderer.RenderInExistingBatch(batchBuilder, frame.ComponentId); - } - - internal void DisposeFrames(RenderBatchBuilder batchBuilder, ArrayRange frames) - => DisposeFramesInRange(batchBuilder, frames.Array, 0, frames.Count); - - private void DisposeFramesInRange(RenderBatchBuilder batchBuilder, RenderTreeFrame[] frames, int startIndex, int endIndexExcl) + private static void DisposeFramesInRange(RenderBatchBuilder batchBuilder, RenderTreeFrame[] frames, int startIndex, int endIndexExcl) { for (var i = startIndex; i < endIndexExcl; i++) { ref var frame = ref frames[i]; if (frame.FrameType == RenderTreeFrameType.Component && frame.Component != null) { - _renderer.DisposeInExistingBatch(batchBuilder, frame.ComponentId); + batchBuilder.ComponentDisposalQueue.Enqueue(frame.ComponentId); } else if (frame.FrameType == RenderTreeFrameType.Attribute && frame.AttributeEventHandlerId > 0) { diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs index d076e8f99b..33a03669ad 100644 --- a/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs @@ -17,7 +17,6 @@ namespace Microsoft.AspNetCore.Blazor.Rendering private readonly int _componentId; // TODO: Change the type to 'long' when the Mono runtime has more complete support for passing longs in .NET->JS calls private readonly IComponent _component; private readonly Renderer _renderer; - private readonly RenderTreeDiffBuilder _diffBuilder; private RenderTreeBuilder _renderTreeBuilderCurrent; private RenderTreeBuilder _renderTreeBuilderPrevious; @@ -32,7 +31,6 @@ namespace Microsoft.AspNetCore.Blazor.Rendering _componentId = componentId; _component = component ?? throw new ArgumentNullException(nameof(component)); _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); - _diffBuilder = new RenderTreeDiffBuilder(renderer); _renderTreeBuilderCurrent = new RenderTreeBuilder(renderer); _renderTreeBuilderPrevious = new RenderTreeBuilder(renderer); } @@ -41,18 +39,33 @@ namespace Microsoft.AspNetCore.Blazor.Rendering /// Regenerates the and adds the changes to the /// . /// - public void Render(RenderBatchBuilder batchBuilder) + public void Render(Renderer renderer, RenderBatchBuilder batchBuilder) { + if (_component is IHandlePropertiesChanged notifyableComponent) + { + notifyableComponent.OnPropertiesChanged(); + } + // Swap the old and new tree builders (_renderTreeBuilderCurrent, _renderTreeBuilderPrevious) = (_renderTreeBuilderPrevious, _renderTreeBuilderCurrent); _renderTreeBuilderCurrent.Clear(); _component.BuildRenderTree(_renderTreeBuilderCurrent); - _diffBuilder.ApplyNewRenderTreeVersion( + + var diff = RenderTreeDiffBuilder.ComputeDiff( + _renderer, batchBuilder, _componentId, _renderTreeBuilderPrevious.GetFrames(), _renderTreeBuilderCurrent.GetFrames()); + batchBuilder.UpdatedComponentDiffs.Append(diff); + + // Process disposal queue now in case it causes further component renders to be enqueued + while (batchBuilder.ComponentDisposalQueue.Count > 0) + { + var disposeComponentId = batchBuilder.ComponentDisposalQueue.Dequeue(); + renderer.DisposeInExistingBatch(batchBuilder, disposeComponentId); + } } /// @@ -66,7 +79,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering disposable.Dispose(); } - _diffBuilder.DisposeFrames(batchBuilder, _renderTreeBuilderCurrent.GetFrames()); + RenderTreeDiffBuilder.DisposeFrames(batchBuilder, _renderTreeBuilderCurrent.GetFrames()); } } } diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatch.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatch.cs index ad7fd9dd74..e3483f5819 100644 --- a/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatch.cs +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatch.cs @@ -15,6 +15,13 @@ namespace Microsoft.AspNetCore.Blazor.Rendering /// public ArrayRange UpdatedComponents { get; } + /// + /// Gets render frames that may be referenced by entries in . + /// For example, edit entries of type + /// will point to an entry in this array to specify the subtree to be prepended. + /// + public ArrayRange ReferenceFrames { get; } + /// /// Gets the IDs of the components that were disposed. /// @@ -22,9 +29,11 @@ namespace Microsoft.AspNetCore.Blazor.Rendering internal RenderBatch( ArrayRange updatedComponents, + ArrayRange referenceFrames, ArrayRange disposedComponentIDs) { UpdatedComponents = updatedComponents; + ReferenceFrames = referenceFrames; DisposedComponentIDs = disposedComponentIDs; } } diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs index 3b09fc9295..c6ca306f1e 100644 --- a/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs @@ -1,43 +1,47 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; +using System.Collections.Generic; using Microsoft.AspNetCore.Blazor.RenderTree; namespace Microsoft.AspNetCore.Blazor.Rendering { internal class RenderBatchBuilder { - private ArrayBuilder _updatedComponentDiffs = new ArrayBuilder(); - private ArrayBuilder _disposedComponentIds = new ArrayBuilder(); - private ArrayBuilder _disposedEventHandlerIds = new ArrayBuilder(); + public ArrayBuilder EditsBuffer { get; } = new ArrayBuilder(); + public ArrayBuilder ReferenceFramesBuffer { get; } = new ArrayBuilder(); - public int ReserveUpdatedComponentSlotId() - { - int id = _updatedComponentDiffs.Count; - _updatedComponentDiffs.Append(default); - return id; - } + public Queue ComponentRenderQueue { get; } = new Queue(); - public void SetUpdatedComponent(int updatedComponentSlotId, RenderTreeDiff diff) - => _updatedComponentDiffs.Overwrite(updatedComponentSlotId, diff); + public Queue ComponentDisposalQueue { get; } = new Queue(); + + public ArrayBuilder UpdatedComponentDiffs { get; set; } + = new ArrayBuilder(); + + private readonly ArrayBuilder _disposedComponentIds = new ArrayBuilder(); + + private readonly ArrayBuilder _disposedEventHandlerIds = new ArrayBuilder(); public ArrayRange GetDisposedEventHandlerIds() => _disposedEventHandlerIds.ToRange(); public void Clear() { - _updatedComponentDiffs.Clear(); + EditsBuffer.Clear(); + ReferenceFramesBuffer.Clear(); + ComponentRenderQueue.Clear(); + UpdatedComponentDiffs.Clear(); _disposedComponentIds.Clear(); _disposedEventHandlerIds.Clear(); } public RenderBatch ToBatch() => new RenderBatch( - _updatedComponentDiffs.ToRange(), + UpdatedComponentDiffs.ToRange(), + ReferenceFramesBuffer.ToRange(), _disposedComponentIds.ToRange()); - public void AddDisposedComponent(int componentId) + public void AddDisposedComponentId(int componentId) => _disposedComponentIds.Append(componentId); public void AddDisposedEventHandlerId(int attributeEventHandlerId) diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs index 4a9883bff2..9d9b761071 100644 --- a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs @@ -87,6 +87,14 @@ namespace Microsoft.AspNetCore.Blazor.Rendering try { RenderInExistingBatch(_sharedRenderBatchBuilder, componentId); + + // Process + while (_sharedRenderBatchBuilder.ComponentRenderQueue.Count > 0) + { + var nextComponentIdToRender = _sharedRenderBatchBuilder.ComponentRenderQueue.Dequeue(); + RenderInExistingBatch(_sharedRenderBatchBuilder, nextComponentIdToRender); + } + UpdateDisplay(_sharedRenderBatchBuilder.ToBatch()); RemoveEventHandlerIds(_sharedRenderBatchBuilder.GetDisposedEventHandlerIds()); } @@ -98,14 +106,12 @@ namespace Microsoft.AspNetCore.Blazor.Rendering } internal void RenderInExistingBatch(RenderBatchBuilder batchBuilder, int componentId) - { - GetRequiredComponentState(componentId).Render(batchBuilder); - } + => GetRequiredComponentState(componentId).Render(this, batchBuilder); internal void DisposeInExistingBatch(RenderBatchBuilder batchBuilder, int componentId) { GetRequiredComponentState(componentId).NotifyDisposed(batchBuilder); - batchBuilder.AddDisposedComponent(componentId); + batchBuilder.AddDisposedComponentId(componentId); } /// diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs index cf11a0ee3d..8e302a4572 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs @@ -17,14 +17,12 @@ namespace Microsoft.AspNetCore.Blazor.Test private readonly Renderer renderer; private readonly RenderTreeBuilder oldTree; private readonly RenderTreeBuilder newTree; - private RenderTreeDiffBuilder diff; public RenderTreeDiffBuilderTest() { renderer = new FakeRenderer(); oldTree = new RenderTreeBuilder(renderer); newTree = new RenderTreeBuilder(renderer); - diff = new RenderTreeDiffBuilder(renderer); } [Theory] @@ -36,7 +34,7 @@ namespace Microsoft.AspNetCore.Blazor.Test appendAction(newTree); // Act - var result = GetSingleUpdatedComponent(); + var (result, referenceFrames) = GetSingleUpdatedComponent(); // Assert Assert.Empty(result.Edits); @@ -67,7 +65,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.AddText(2, "text2"); // Act - var result = GetSingleUpdatedComponent(); + var (result, referenceFrames) = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -75,7 +73,7 @@ namespace Microsoft.AspNetCore.Blazor.Test { AssertEdit(entry, RenderTreeEditType.PrependFrame, 1); Assert.Equal(0, entry.ReferenceFrameIndex); - AssertFrame.Text(result.ReferenceFrames.Array[0], "text1", 1); + AssertFrame.Text(referenceFrames[0], "text1", 1); }); } @@ -90,7 +88,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.AddText(2, "text2"); // Act - var result = GetSingleUpdatedComponent(); + var (result, referenceFrames) = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -109,7 +107,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.AddText(0, "x"); // Loop start // Act - var result = GetSingleUpdatedComponent(); + var (result, referenceFrames) = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -129,7 +127,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.AddText(10, "x"); // Loop start // Act - var result = GetSingleUpdatedComponent(); + var (result, referenceFrames) = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -143,9 +141,8 @@ namespace Microsoft.AspNetCore.Blazor.Test AssertEdit(entry, RenderTreeEditType.PrependFrame, 2); Assert.Equal(1, entry.ReferenceFrameIndex); }); - Assert.Collection(result.ReferenceFrames, - frame => AssertFrame.Text(frame, "x", 11), - frame => AssertFrame.Text(frame, "x", 12)); + AssertFrame.Text(referenceFrames[0], "x", 11); + AssertFrame.Text(referenceFrames[1], "x", 12); } [Fact] @@ -160,7 +157,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.AddText(1, "x"); // Act - var result = GetSingleUpdatedComponent(); + var (result, referenceFrames) = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -180,7 +177,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.AddText(11, "x"); // Will be added // Act - var result = GetSingleUpdatedComponent(); + var (result, referenceFrames) = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -194,9 +191,8 @@ namespace Microsoft.AspNetCore.Blazor.Test AssertEdit(entry, RenderTreeEditType.PrependFrame, 3); Assert.Equal(1, entry.ReferenceFrameIndex); }); - Assert.Collection(result.ReferenceFrames, - frame => AssertFrame.Text(frame, "x", 10), - frame => AssertFrame.Text(frame, "x", 11)); + AssertFrame.Text(referenceFrames[0], "x", 10); + AssertFrame.Text(referenceFrames[1], "x", 11); } [Fact] @@ -211,7 +207,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.AddText(12, "x"); // Act - var result = GetSingleUpdatedComponent(); + var (result, referenceFrames) = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -225,9 +221,8 @@ namespace Microsoft.AspNetCore.Blazor.Test AssertEdit(entry, RenderTreeEditType.PrependFrame, 2); Assert.Equal(1, entry.ReferenceFrameIndex); }); - Assert.Collection(result.ReferenceFrames, - frame => AssertFrame.Text(frame, "x", 10), - frame => AssertFrame.Text(frame, "x", 11)); + AssertFrame.Text(referenceFrames[0], "x", 10); + AssertFrame.Text(referenceFrames[1], "x", 11); } [Fact] @@ -242,7 +237,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.AddText(2, "x"); // Note that the '0' and '1' items are not present on this iteration // Act - var result = GetSingleUpdatedComponent(); + var (result, referenceFrames) = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -258,7 +253,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.AddText(1, "text"); // Act - var result = GetSingleUpdatedComponent(); + var (result, referenceFrames) = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -276,7 +271,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.AddText(182, "new text 2"); // Act - var result = GetSingleUpdatedComponent(); + var (result, referenceFrames) = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -306,7 +301,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.CloseElement(); // Act - var result = GetSingleUpdatedComponent(); + var (result, referenceFrames) = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -327,34 +322,22 @@ namespace Microsoft.AspNetCore.Blazor.Test GetRenderedBatch(new RenderTreeBuilder(renderer), oldTree); // Assign initial IDs newTree.OpenComponent(123); newTree.CloseComponent(); + var batchBuilder = new RenderBatchBuilder(); // Act - var renderBatch = GetRenderedBatch(); + var diff = RenderTreeDiffBuilder.ComputeDiff(renderer, batchBuilder, 0, oldTree.GetFrames(), newTree.GetFrames()); - // Assert - Assert.Collection(renderBatch.DisposedComponentIDs, - disposedComponentId => Assert.Equal(0, disposedComponentId)); + // Assert: We're going to dispose the old component, and render the new one + Assert.Equal(new[] { 0 }, batchBuilder.ComponentDisposalQueue); + Assert.Equal(new[] { 1 }, batchBuilder.ComponentRenderQueue); - // Assert: First updated component is the root with one child being - // prepended, and its earlier incarnation being removed - Assert.Equal(2, renderBatch.UpdatedComponents.Count); - var updatedComponent1 = renderBatch.UpdatedComponents.Array[0]; - Assert.Collection(updatedComponent1.Edits, + // Assert: Got correct info in diff + Assert.Collection(diff.Edits, entry => AssertEdit(entry, RenderTreeEditType.RemoveFrame, 0), entry => { AssertEdit(entry, RenderTreeEditType.PrependFrame, 0); - Assert.Equal(0, entry.ReferenceFrameIndex); - Assert.IsType(updatedComponent1.ReferenceFrames.Array[0].Component); - }); - - // Assert: Second updated component is the new FakeComponent2 - var updatedComponent2 = renderBatch.UpdatedComponents.Array[1]; - Assert.Collection(updatedComponent2.Edits, - entry => - { - AssertEdit(entry, RenderTreeEditType.PrependFrame, 0); - Assert.Equal(0, entry.ReferenceFrameIndex); + Assert.IsType(batchBuilder.ReferenceFramesBuffer.Buffer[entry.ReferenceFrameIndex].Component); }); } @@ -371,7 +354,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.CloseElement(); // Act - var result = GetSingleUpdatedComponent(); + var (result, referenceFrames) = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -380,8 +363,7 @@ namespace Microsoft.AspNetCore.Blazor.Test AssertEdit(entry, RenderTreeEditType.SetAttribute, 0); Assert.Equal(0, entry.ReferenceFrameIndex); }); - Assert.Collection(result.ReferenceFrames, - frame => AssertFrame.Attribute(frame, "added", "added value")); + AssertFrame.Attribute(referenceFrames[0], "added", "added value"); } [Fact] @@ -397,7 +379,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.CloseElement(); // Act - var result = GetSingleUpdatedComponent(); + var (result, referenceFrames) = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -422,7 +404,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.CloseElement(); // Act - var result = GetSingleUpdatedComponent(); + var (result, referenceFrames) = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -431,8 +413,7 @@ namespace Microsoft.AspNetCore.Blazor.Test AssertEdit(entry, RenderTreeEditType.SetAttribute, 0); Assert.Equal(0, entry.ReferenceFrameIndex); }); - Assert.Collection(result.ReferenceFrames, - frame => AssertFrame.Attribute(frame, "will change", "did change value")); + AssertFrame.Attribute(referenceFrames[0], "will change", "did change value"); } [Fact] @@ -452,7 +433,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.CloseElement(); // Act - var result = GetSingleUpdatedComponent(); + var (result, referenceFrames) = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -461,8 +442,7 @@ namespace Microsoft.AspNetCore.Blazor.Test AssertEdit(entry, RenderTreeEditType.SetAttribute, 0); Assert.Equal(0, entry.ReferenceFrameIndex); }); - Assert.Collection(result.ReferenceFrames, - frame => AssertFrame.Attribute(frame, "will change", addedHandler)); + AssertFrame.Attribute(referenceFrames[0], "will change", addedHandler); } [Fact] @@ -477,7 +457,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.CloseElement(); // Act - var result = GetSingleUpdatedComponent(); + var (result, referenceFrames) = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -491,8 +471,7 @@ namespace Microsoft.AspNetCore.Blazor.Test AssertEdit(entry, RenderTreeEditType.SetAttribute, 0); Assert.Equal(0, entry.ReferenceFrameIndex); }); - Assert.Collection(result.ReferenceFrames, - frame => AssertFrame.Attribute(frame, "newname", "same value")); + AssertFrame.Attribute(referenceFrames[0], "newname", "same value"); } [Fact] @@ -518,7 +497,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.CloseElement(); // Act - var result = GetSingleUpdatedComponent(); + var (result, referenceFrames) = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -533,8 +512,7 @@ namespace Microsoft.AspNetCore.Blazor.Test entry => AssertEdit(entry, RenderTreeEditType.StepOut, 0), entry => AssertEdit(entry, RenderTreeEditType.StepOut, 0), entry => AssertEdit(entry, RenderTreeEditType.StepOut, 0)); - Assert.Collection(result.ReferenceFrames, - frame => AssertFrame.Text(frame, "grandchild new text", 13)); + AssertFrame.Text(referenceFrames[0], "grandchild new text", 13); } [Fact] @@ -560,7 +538,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.CloseElement(); // Act - var result = GetSingleUpdatedComponent(); + var (result, referenceFrames) = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -571,8 +549,7 @@ namespace Microsoft.AspNetCore.Blazor.Test Assert.Equal(0, entry.ReferenceFrameIndex); }, entry => AssertEdit(entry, RenderTreeEditType.StepOut, 0)); - Assert.Collection(result.ReferenceFrames, - frame => AssertFrame.Text(frame, "Text that has changed", 11)); + AssertFrame.Text(referenceFrames[0], "Text that has changed", 11); } [Fact] @@ -589,7 +566,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.AddText(13, "text4"); // Act - var result = GetSingleUpdatedComponent(); + var (result, referenceFrames) = GetSingleUpdatedComponent(); // Assert Assert.Collection(result.Edits, @@ -598,8 +575,7 @@ namespace Microsoft.AspNetCore.Blazor.Test AssertEdit(entry, RenderTreeEditType.UpdateText, 1); Assert.Equal(0, entry.ReferenceFrameIndex); }); - Assert.Collection(result.ReferenceFrames, - frame => AssertFrame.Text(frame, "text2modified", 11)); + AssertFrame.Text(referenceFrames[0], "text2modified", 11); } [Fact] @@ -621,11 +597,8 @@ namespace Microsoft.AspNetCore.Blazor.Test var renderBatch = GetRenderedBatch(); // Assert - Assert.Equal(3, renderBatch.UpdatedComponents.Count); - - // First component is the root one - var firstComponentDiff = renderBatch.UpdatedComponents.Array[0]; - Assert.Collection(firstComponentDiff.Edits, + var diff = renderBatch.UpdatedComponents.Single(); + Assert.Collection(diff.Edits, entry => AssertEdit(entry, RenderTreeEditType.StepIn, 1), entry => { @@ -638,23 +611,8 @@ namespace Microsoft.AspNetCore.Blazor.Test Assert.Equal(1, entry.ReferenceFrameIndex); }, entry => AssertEdit(entry, RenderTreeEditType.StepOut, 0)); - Assert.Collection(firstComponentDiff.ReferenceFrames, - frame => AssertFrame.ComponentWithInstance(frame, 0, 12), - frame => AssertFrame.ComponentWithInstance(frame, 1, 13)); - - // Second in batch is the first child component - var secondComponentDiff = renderBatch.UpdatedComponents.Array[1]; - Assert.Equal(0, secondComponentDiff.ComponentId); - Assert.Empty(secondComponentDiff.Edits); // Because FakeComponent produces no frames - Assert.Empty(secondComponentDiff.ReferenceFrames); // Because FakeComponent produces no frames - - // Third in batch is the second child component - var thirdComponentDiff = renderBatch.UpdatedComponents.Array[2]; - Assert.Equal(1, thirdComponentDiff.ComponentId); - Assert.Collection(thirdComponentDiff.Edits, - entry => AssertEdit(entry, RenderTreeEditType.PrependFrame, 0)); - Assert.Collection(thirdComponentDiff.ReferenceFrames, - frame => AssertFrame.Text(frame, $"Hello from {nameof(FakeComponent2)}")); + AssertFrame.ComponentWithInstance(renderBatch.ReferenceFrames.Array[0], 0, 12); + AssertFrame.ComponentWithInstance(renderBatch.ReferenceFrames.Array[1], 1, 13); } [Fact] @@ -673,8 +631,7 @@ namespace Microsoft.AspNetCore.Blazor.Test var componentInstance = newTree.GetFrames().First().Component as FakeComponent; // Assert - Assert.Equal(2, renderBatch.UpdatedComponents.Count); - + Assert.Equal(1, renderBatch.UpdatedComponents.Count); var rootComponentDiff = renderBatch.UpdatedComponents.Array[0]; AssertEdit(rootComponentDiff.Edits.Single(), RenderTreeEditType.PrependFrame, 0); Assert.NotNull(componentInstance); @@ -695,7 +652,7 @@ namespace Microsoft.AspNetCore.Blazor.Test // Act/Assert var ex = Assert.Throws(() => { - diff.ApplyNewRenderTreeVersion(new RenderBatchBuilder(), 0, oldTree.GetFrames(), newTree.GetFrames()); + RenderTreeDiffBuilder.ComputeDiff(renderer, new RenderBatchBuilder(), 0, oldTree.GetFrames(), newTree.GetFrames()); }); Assert.Equal($"Component of type '{typeof(FakeComponent).FullName}' does not have a property matching the name 'SomeUnknownProperty'.", ex.Message); } @@ -712,7 +669,7 @@ namespace Microsoft.AspNetCore.Blazor.Test // Act/Assert var ex = Assert.Throws(() => { - diff.ApplyNewRenderTreeVersion(new RenderBatchBuilder(), 0, oldTree.GetFrames(), newTree.GetFrames()); + RenderTreeDiffBuilder.ComputeDiff(renderer, new RenderBatchBuilder(), 0, oldTree.GetFrames(), newTree.GetFrames()); }); Assert.StartsWith($"Unable to set property '{nameof(FakeComponent.ReadonlyProperty)}' on " + $"component of type '{typeof(FakeComponent).FullName}'.", ex.Message); @@ -735,14 +692,14 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.CloseComponent(); // newTree.OpenComponent(13); // 3: newTree.CloseComponent(); // - newTree.CloseElement(); // - diff.ApplyNewRenderTreeVersion(new RenderBatchBuilder(), 0, new RenderTreeBuilder(renderer).GetFrames(), oldTree.GetFrames()); + RenderTreeDiffBuilder.ComputeDiff(renderer, new RenderBatchBuilder(), 0, new RenderTreeBuilder(renderer).GetFrames(), oldTree.GetFrames()); var originalFakeComponentInstance = oldTree.GetFrames().Array[2].Component; var originalFakeComponent2Instance = oldTree.GetFrames().Array[3].Component; // Act - var result = GetSingleUpdatedComponent(); + var (result, referenceFrames) = GetSingleUpdatedComponent(); var newFrame1 = newTree.GetFrames().Array[2]; var newFrame2 = newTree.GetFrames().Array[3]; @@ -768,7 +725,7 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.AddAttribute(14, nameof(FakeComponent.ObjectProperty), objectWillNotChange); newTree.CloseComponent(); - diff.ApplyNewRenderTreeVersion(new RenderBatchBuilder(), 0, new RenderTreeBuilder(renderer).GetFrames(), oldTree.GetFrames()); + RenderTreeDiffBuilder.ComputeDiff(renderer, new RenderBatchBuilder(), 0, new RenderTreeBuilder(renderer).GetFrames(), oldTree.GetFrames()); var originalComponentInstance = (FakeComponent)oldTree.GetFrames().Array[0].Component; originalComponentInstance.ObjectProperty = null; // So we can see it doesn't get reassigned @@ -777,67 +734,14 @@ namespace Microsoft.AspNetCore.Blazor.Test var newComponentInstance = (FakeComponent)oldTree.GetFrames().Array[0].Component; // Assert - Assert.Equal(2, renderBatch.UpdatedComponents.Count); + Assert.Equal(1, renderBatch.UpdatedComponents.Count); // Because the diff builder only queues child component renders; it doesn't actually perfom them itself Assert.Same(originalComponentInstance, newComponentInstance); Assert.Equal("String did change", newComponentInstance.StringProperty); Assert.Null(newComponentInstance.ObjectProperty); // To observe that the property wasn't even written, we nulled it out on the original } [Fact] - public void NotifiesIHandlePropertiesChangedBeforeFirstRender() - { - // Arrange - newTree.OpenComponent(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(0); - oldTree.AddAttribute(1, nameof(HandlePropertiesChangedComponent.IntProperty), 123); - oldTree.CloseComponent(); - newTree1.OpenComponent(0); - newTree1.AddAttribute(1, nameof(HandlePropertiesChangedComponent.IntProperty), 123); - newTree1.CloseComponent(); - newTree2.OpenComponent(0); - newTree2.AddAttribute(1, nameof(HandlePropertiesChangedComponent.IntProperty), 456); - newTree2.CloseComponent(); - - // Act/Assert 0: Initial render - var batch0 = GetRenderedBatch(new RenderTreeBuilder(renderer), oldTree); - var diffForChildComponent0 = batch0.UpdatedComponents.Array[1]; - var childComponentFrame = batch0.UpdatedComponents.Array[0].ReferenceFrames.Array[0]; - var childComponentInstance = (HandlePropertiesChangedComponent)childComponentFrame.Component; - Assert.Equal(1, childComponentInstance.NotificationsCount); - Assert.Collection(diffForChildComponent0.ReferenceFrames, - frame => AssertFrame.Text(frame, "Notifications: 1", 0)); - - // Act/Assert 1: If properties didn't change, we don't notify - GetRenderedBatch(oldTree, newTree1); - Assert.Equal(1, childComponentInstance.NotificationsCount); - - // Act/Assert 2: If properties did change, we do notify - var batch2 = GetRenderedBatch(newTree1, newTree2); - var diffForChildComponent2 = batch2.UpdatedComponents.Array[1]; - Assert.Equal(2, childComponentInstance.NotificationsCount); - Assert.Collection(diffForChildComponent2.ReferenceFrames, - frame => AssertFrame.Text(frame, "Notifications: 2", 0)); - } - - [Fact] - public void CallsDisposeOnlyOnRemovedChildComponents() + public void QueuesRemovedChildComponentsForDisposal() { // Arrange oldTree.OpenComponent(10); // @@ -849,29 +753,23 @@ namespace Microsoft.AspNetCore.Blazor.Test newTree.OpenComponent(30); // newTree.CloseComponent(); // - diff.ApplyNewRenderTreeVersion(new RenderBatchBuilder(), 0, new RenderTreeBuilder(renderer).GetFrames(), oldTree.GetFrames()); - var disposableComponent1 = (DisposableComponent)oldTree.GetFrames().Array[0].Component; - var nonDisposableComponent = (NonDisposableComponent)oldTree.GetFrames().Array[1].Component; - var disposableComponent2 = (DisposableComponent)oldTree.GetFrames().Array[2].Component; + var batchBuilder = new RenderBatchBuilder(); + RenderTreeDiffBuilder.ComputeDiff(renderer, batchBuilder, 0, new RenderTreeBuilder(renderer).GetFrames(), oldTree.GetFrames()); - // Act - var renderedBatch = GetRenderedBatch(); - - // Assert: We track NonDisposableComponent was disposed even though it's not IDisposable - Assert.Equal(renderedBatch.DisposedComponentIDs, new[] { 0, 1 }); - - // Assert: We did call Dispose on the disposed DisposableComponent - Assert.Equal(1, disposableComponent1.DisposalCount); - - // Assert: We didn't dispose the retained component - Assert.Equal(0, disposableComponent2.DisposalCount); + // Act/Assert + // Note that we track NonDisposableComponent was disposed even though it's not IDisposable, + // because it's up to the upstream renderer to decide what "disposing" a component means + Assert.Empty(batchBuilder.ComponentDisposalQueue); + RenderTreeDiffBuilder.ComputeDiff(renderer, batchBuilder, 0, oldTree.GetFrames(), newTree.GetFrames()); + Assert.Equal(new[] { 0, 1 }, batchBuilder.ComponentDisposalQueue); } - private RenderTreeDiff GetSingleUpdatedComponent() + private (RenderTreeDiff, RenderTreeFrame[]) GetSingleUpdatedComponent() { - var diffsInBatch = GetRenderedBatch().UpdatedComponents; + var batch = GetRenderedBatch(); + var diffsInBatch = batch.UpdatedComponents; Assert.Equal(1, diffsInBatch.Count); - return diffsInBatch.Array[0]; + return (diffsInBatch.Array[0], batch.ReferenceFrames.ToArray()); } private RenderBatch GetRenderedBatch() @@ -880,7 +778,8 @@ namespace Microsoft.AspNetCore.Blazor.Test private RenderBatch GetRenderedBatch(RenderTreeBuilder from, RenderTreeBuilder to) { var batchBuilder = new RenderBatchBuilder(); - diff.ApplyNewRenderTreeVersion(batchBuilder, 0, from.GetFrames(), to.GetFrames()); + var diff = RenderTreeDiffBuilder.ComputeDiff(renderer, batchBuilder, 0, from.GetFrames(), to.GetFrames()); + batchBuilder.UpdatedComponentDiffs.Append(diff); return batchBuilder.ToBatch(); } @@ -912,23 +811,6 @@ namespace Microsoft.AspNetCore.Blazor.Test } } - private class HandlePropertiesChangedComponent : IComponent, IHandlePropertiesChanged - { - public int NotificationsCount { get; private set; } - - public int IntProperty { get; set; } - - public void BuildRenderTree(RenderTreeBuilder builder) - { - builder.AddText(0, $"Notifications: {NotificationsCount}"); - } - - public void OnPropertiesChanged() - { - NotificationsCount++; - } - } - private class DisposableComponent : IComponent, IDisposable { public int DisposalCount { get; private set; } diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs index 10e32eb954..8c1f719dcd 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs @@ -31,16 +31,16 @@ namespace Microsoft.AspNetCore.Blazor.Test renderer.RenderNewBatch(componentId); // Assert - var diff = renderer.Batches.Single().DiffsByComponentId[componentId].Single(); + var batch = renderer.Batches.Single(); + var diff = batch.DiffsByComponentId[componentId].Single(); Assert.Collection(diff.Edits, edit => { Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); Assert.Equal(0, edit.ReferenceFrameIndex); }); - Assert.Collection(diff.ReferenceFrames, - frame => AssertFrame.Element(frame, "my element", 2), - frame => AssertFrame.Text(frame, "some text")); + AssertFrame.Element(batch.ReferenceFrames[0], "my element", 2); + AssertFrame.Text(batch.ReferenceFrames[1], "some text"); } [Fact] @@ -60,11 +60,14 @@ namespace Microsoft.AspNetCore.Blazor.Test var componentId = renderer.AssignComponentId(component); renderer.RenderNewBatch(componentId); var batch = renderer.Batches.Single(); - var componentFrame = batch.DiffsByComponentId[componentId].Single().ReferenceFrames + var componentFrame = batch.ReferenceFrames .Single(frame => frame.FrameType == RenderTreeFrameType.Component); var nestedComponentId = componentFrame.ComponentId; var nestedComponentDiff = batch.DiffsByComponentId[nestedComponentId].Single(); + // We rendered both components + Assert.Equal(2, batch.DiffsByComponentId.Count); + // The nested component exists Assert.IsType(componentFrame.Component); @@ -73,10 +76,10 @@ namespace Microsoft.AspNetCore.Blazor.Test edit => { Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); - Assert.Equal(0, edit.ReferenceFrameIndex); + AssertFrame.Text( + batch.ReferenceFrames[edit.ReferenceFrameIndex], + "Nested component output"); }); - Assert.Collection(nestedComponentDiff.ReferenceFrames, - frame => AssertFrame.Text(frame, "Nested component output")); } [Fact] @@ -89,28 +92,28 @@ namespace Microsoft.AspNetCore.Blazor.Test // Act/Assert: first render renderer.RenderNewBatch(componentId); - var firstDiff = renderer.Batches.Single().DiffsByComponentId[componentId].Single(); + var batch = renderer.Batches.Single(); + var firstDiff = batch.DiffsByComponentId[componentId].Single(); Assert.Collection(firstDiff.Edits, edit => { Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); Assert.Equal(0, edit.ReferenceFrameIndex); + AssertFrame.Text(batch.ReferenceFrames[0], "Initial message"); }); - Assert.Collection(firstDiff.ReferenceFrames, - frame => AssertFrame.Text(frame, "Initial message")); // Act/Assert: second render component.Message = "Modified message"; renderer.RenderNewBatch(componentId); - var secondDiff = renderer.Batches.Skip(1).Single().DiffsByComponentId[componentId].Single(); - Assert.Collection(firstDiff.Edits, + var secondBatch = renderer.Batches.Skip(1).Single(); + var secondDiff = secondBatch.DiffsByComponentId[componentId].Single(); + Assert.Collection(secondDiff.Edits, edit => { Assert.Equal(RenderTreeEditType.UpdateText, edit.Type); Assert.Equal(0, edit.ReferenceFrameIndex); + AssertFrame.Text(secondBatch.ReferenceFrames[0], "Modified message"); }); - Assert.Collection(firstDiff.ReferenceFrames, - frame => AssertFrame.Text(frame, "Modified message")); } [Fact] @@ -125,8 +128,7 @@ namespace Microsoft.AspNetCore.Blazor.Test }); var parentComponentId = renderer.AssignComponentId(parentComponent); renderer.RenderNewBatch(parentComponentId); - var nestedComponentFrame = renderer.Batches.Single().DiffsByComponentId[parentComponentId] - .Single() + var nestedComponentFrame = renderer.Batches.Single() .ReferenceFrames .Single(frame => frame.FrameType == RenderTreeFrameType.Component); var nestedComponent = (MessageComponent)nestedComponentFrame.Component; @@ -135,28 +137,28 @@ namespace Microsoft.AspNetCore.Blazor.Test // Assert: inital render nestedComponent.Message = "Render 1"; renderer.RenderNewBatch(nestedComponentId); - var firstDiff = renderer.Batches[1].DiffsByComponentId[nestedComponentId].Single(); + var batch = renderer.Batches[1]; + var firstDiff = batch.DiffsByComponentId[nestedComponentId].Single(); Assert.Collection(firstDiff.Edits, edit => { Assert.Equal(RenderTreeEditType.UpdateText, edit.Type); Assert.Equal(0, edit.ReferenceFrameIndex); + AssertFrame.Text(batch.ReferenceFrames[0], "Render 1"); }); - Assert.Collection(firstDiff.ReferenceFrames, - frame => AssertFrame.Text(frame, "Render 1")); // Act/Assert: re-render nestedComponent.Message = "Render 2"; renderer.RenderNewBatch(nestedComponentId); - var secondDiff = renderer.Batches[2].DiffsByComponentId[nestedComponentId].Single(); - Assert.Collection(firstDiff.Edits, + var secondBatch = renderer.Batches[2]; + var secondDiff = secondBatch.DiffsByComponentId[nestedComponentId].Single(); + Assert.Collection(secondDiff.Edits, edit => { Assert.Equal(RenderTreeEditType.UpdateText, edit.Type); Assert.Equal(0, edit.ReferenceFrameIndex); + AssertFrame.Text(secondBatch.ReferenceFrames[0], "Render 2"); }); - Assert.Collection(firstDiff.ReferenceFrames, - frame => AssertFrame.Text(frame, "Render 2")); } [Fact] @@ -173,7 +175,7 @@ namespace Microsoft.AspNetCore.Blazor.Test var componentId = renderer.AssignComponentId(component); renderer.RenderNewBatch(componentId); - var eventHandlerId = renderer.Batches.Single().DiffsByComponentId[componentId].Single() + var eventHandlerId = renderer.Batches.Single() .ReferenceFrames .First(frame => frame.AttributeValue != null) .AttributeEventHandlerId; @@ -203,8 +205,7 @@ namespace Microsoft.AspNetCore.Blazor.Test renderer.RenderNewBatch(parentComponentId); // Arrange: Render nested component - var nestedComponentFrame = renderer.Batches.Single().DiffsByComponentId[parentComponentId] - .Single() + var nestedComponentFrame = renderer.Batches.Single() .ReferenceFrames .Single(frame => frame.FrameType == RenderTreeFrameType.Component); var nestedComponent = (EventComponent)nestedComponentFrame.Component; @@ -213,7 +214,7 @@ namespace Microsoft.AspNetCore.Blazor.Test renderer.RenderNewBatch(nestedComponentId); // Find nested component's event handler ID - var eventHandlerId = renderer.Batches[1].DiffsByComponentId[nestedComponentId].Single() + var eventHandlerId = renderer.Batches[1] .ReferenceFrames .First(frame => frame.AttributeValue != null) .AttributeEventHandlerId; @@ -363,8 +364,6 @@ namespace Microsoft.AspNetCore.Blazor.Test renderer.RenderNewBatch(rootComponentId); var nestedComponentFrame = renderer.Batches.Single() - .DiffsByComponentId[rootComponentId] - .Single() .ReferenceFrames .Single(frame => frame.FrameType == RenderTreeFrameType.Component); var nestedComponentInstance = (MessageComponent)nestedComponentFrame.Component; @@ -382,8 +381,7 @@ namespace Microsoft.AspNetCore.Blazor.Test Assert.Equal(RenderTreeEditType.UpdateText, edit.Type); Assert.Equal(0, edit.ReferenceFrameIndex); }); - Assert.Collection(diff.ReferenceFrames, - frame => AssertFrame.Text(frame, "Modified message")); + AssertFrame.Text(batch.ReferenceFrames[0], "Modified message"); Assert.False(batch.DiffsByComponentId.ContainsKey(nestedComponentFrame.ComponentId)); } @@ -406,8 +404,7 @@ namespace Microsoft.AspNetCore.Blazor.Test var rootComponentId = renderer.AssignComponentId(component); renderer.RenderNewBatch(rootComponentId); - var originalComponentFrame = renderer.Batches.Single().DiffsByComponentId[rootComponentId] - .Single() + var originalComponentFrame = renderer.Batches.Single() .ReferenceFrames .Single(frame => frame.FrameType == RenderTreeFrameType.Component); var childComponentInstance = (FakeComponent)originalComponentFrame.Component; @@ -443,8 +440,7 @@ namespace Microsoft.AspNetCore.Blazor.Test var rootComponentId = renderer.AssignComponentId(component); renderer.RenderNewBatch(rootComponentId); - var childComponentId = renderer.Batches.Single().DiffsByComponentId[rootComponentId] - .Single() + var childComponentId = renderer.Batches.Single() .ReferenceFrames .Single(frame => frame.FrameType == RenderTreeFrameType.Component) .ComponentId; @@ -461,8 +457,67 @@ namespace Microsoft.AspNetCore.Blazor.Test Assert.Equal(RenderTreeEditType.UpdateText, edit.Type); Assert.Equal(0, edit.ReferenceFrameIndex); }); - Assert.Collection(diff.ReferenceFrames, - frame => AssertFrame.Text(frame, "second")); + AssertFrame.Text(renderer.Batches[1].ReferenceFrames[0], "second"); + } + + [Fact] + public void NotifiesIHandlePropertiesChangedBeforeFirstRender() + { + // Arrange + var renderer = new TestRenderer(); + var component = new HandlePropertiesChangedComponent(); + var rootComponentId = renderer.AssignComponentId(component); + + // Act + renderer.RenderNewBatch(rootComponentId); + + // Assert + AssertFrame.Text(renderer.Batches.Single().ReferenceFrames[0], "Notifications: 1", 0); + } + + [Fact] + public void NotifiesIHandlePropertiesChangedWhenChanged() + { + // Arrange + var renderer = new TestRenderer(); + var component = new ConditionalParentComponent + { + IncludeChild = true, + ChildParameters = new Dictionary + { + { nameof(HandlePropertiesChangedComponent.IntProperty), 123 } + } + }; + var rootComponentId = renderer.AssignComponentId(component); + + // Act/Assert 0: Initial render + renderer.RenderNewBatch(rootComponentId); + var batch1 = renderer.Batches.Single(); + var childComponentFrame = batch1 + .ReferenceFrames.Where(frame => frame.FrameType == RenderTreeFrameType.Component).Single(); + var childComponentId = childComponentFrame.ComponentId; + var diffForChildComponent0 = batch1.DiffsByComponentId[childComponentId].Single(); + var childComponentInstance = (HandlePropertiesChangedComponent)childComponentFrame.Component; + Assert.Equal(1, childComponentInstance.NotificationsCount); + Assert.Collection(diffForChildComponent0.Edits, + edit => + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + AssertFrame.Text(batch1.ReferenceFrames[edit.ReferenceFrameIndex], "Notifications: 1", 0); + }); + + // Act/Assert 1: If properties didn't change, we don't notify + renderer.RenderNewBatch(rootComponentId); + Assert.Equal(1, childComponentInstance.NotificationsCount); + + // Act/Assert 2: If properties did change, we do notify + component.ChildParameters[nameof(HandlePropertiesChangedComponent.IntProperty)] = 456; + renderer.RenderNewBatch(rootComponentId); + Assert.Equal(2, childComponentInstance.NotificationsCount); + var batch3 = renderer.Batches.Skip(2).Single(); + var diffForChildComponent2 = batch3.DiffsByComponentId[childComponentId].Single(); + Assert.Equal(2, childComponentInstance.NotificationsCount); + AssertFrame.Text(batch3.ReferenceFrames[0], "Notifications: 2", 0); } [Fact] @@ -473,7 +528,6 @@ namespace Microsoft.AspNetCore.Blazor.Test var firstRender = true; var component = new TestComponent(builder => { - builder.OpenElement(7, "some element"); if (firstRender) { // Nested descendants @@ -483,27 +537,28 @@ namespace Microsoft.AspNetCore.Blazor.Test } builder.OpenComponent(200); builder.CloseComponent(); - builder.CloseElement(); }); var rootComponentId = renderer.AssignComponentId(component); // Act/Assert 1: First render, capturing child component IDs renderer.RenderNewBatch(rootComponentId); - var childComponentIds = renderer.Batches.Single().DiffsByComponentId[rootComponentId] - .Single() - .ReferenceFrames + var batch = renderer.Batches.Single(); + var rootComponentDiff = batch.DiffsByComponentId[rootComponentId].Single(); + var childComponentIds = rootComponentDiff + .Edits + .Select(edit => batch.ReferenceFrames[edit.ReferenceFrameIndex]) .Where(frame => frame.FrameType == RenderTreeFrameType.Component) .Select(frame => frame.ComponentId) .ToList(); - Assert.Equal(new[] { 1, 3 }, childComponentIds); + Assert.Equal(new[] { 1, 2 }, childComponentIds); // Act: Second render firstRender = false; renderer.RenderNewBatch(rootComponentId); // Assert: Applicable children are included in disposal list - Assert.Equal(new[] { 2, 1 }, renderer.Batches[1].DisposedComponentIDs); + Assert.Equal(new[] { 1, 3 }, renderer.Batches[1].DisposedComponentIDs); } [Fact] @@ -516,7 +571,7 @@ namespace Microsoft.AspNetCore.Blazor.Test var component = new EventComponent { Handler = origEventHandler }; var componentId = renderer.AssignComponentId(component); renderer.RenderNewBatch(componentId); - var origEventHandlerId = renderer.Batches.Single().DiffsByComponentId[componentId].Single() + var origEventHandlerId = renderer.Batches.Single() .ReferenceFrames .Where(f => f.FrameType == RenderTreeFrameType.Attribute) .Single(f => f.AttributeEventHandlerId != 0) @@ -553,7 +608,7 @@ namespace Microsoft.AspNetCore.Blazor.Test var component = new EventComponent { Handler = origEventHandler }; var componentId = renderer.AssignComponentId(component); renderer.RenderNewBatch(componentId); - var origEventHandlerId = renderer.Batches.Single().DiffsByComponentId[componentId].Single() + var origEventHandlerId = renderer.Batches.Single() .ReferenceFrames .Where(f => f.FrameType == RenderTreeFrameType.Attribute) .Single(f => f.AttributeEventHandlerId != 0) @@ -593,13 +648,17 @@ namespace Microsoft.AspNetCore.Blazor.Test }; var rootComponentId = renderer.AssignComponentId(component); renderer.RenderNewBatch(rootComponentId); - var childComponentId = renderer.Batches.Single().DiffsByComponentId[rootComponentId].Single() - .ReferenceFrames + var batch = renderer.Batches.Single(); + var rootComponentDiff = batch.DiffsByComponentId[rootComponentId].Single(); + var rootComponentFrame = batch.ReferenceFrames[0]; + var childComponentFrame = rootComponentDiff.Edits + .Select(e => batch.ReferenceFrames[e.ReferenceFrameIndex]) .Where(f => f.FrameType == RenderTreeFrameType.Component) - .Single() - .ComponentId; - var eventHandlerId = renderer.Batches.Single().DiffsByComponentId[childComponentId].Single() - .ReferenceFrames + .Single(); + var childComponentId = childComponentFrame.ComponentId; + var childComponentDiff = batch.DiffsByComponentId[childComponentFrame.ComponentId].Single(); + var eventHandlerId = batch.ReferenceFrames + .Skip(childComponentDiff.Edits[0].ReferenceFrameIndex) // Search from where the child component frames start .Where(f => f.FrameType == RenderTreeFrameType.Attribute) .Single(f => f.AttributeEventHandlerId != 0) .AttributeEventHandlerId; @@ -631,7 +690,7 @@ namespace Microsoft.AspNetCore.Blazor.Test var component = new EventComponent { Handler = origEventHandler }; var componentId = renderer.AssignComponentId(component); renderer.RenderNewBatch(componentId); - var origEventHandlerId = renderer.Batches.Single().DiffsByComponentId[componentId].Single() + var origEventHandlerId = renderer.Batches.Single() .ReferenceFrames .Where(f => f.FrameType == RenderTreeFrameType.Attribute) .Single(f => f.AttributeEventHandlerId != 0) @@ -692,6 +751,8 @@ namespace Microsoft.AspNetCore.Blazor.Test capturedBatch.AddDiff(renderTreeDiff); } + // Clone other data, as underlying storage will get reused by later batches + capturedBatch.ReferenceFrames = renderBatch.ReferenceFrames.ToArray(); capturedBatch.DisposedComponentIDs = renderBatch.DisposedComponentIDs.ToList(); } } @@ -702,6 +763,7 @@ namespace Microsoft.AspNetCore.Blazor.Test = new Dictionary>(); public IList DisposedComponentIDs { get; set; } + public RenderTreeFrame[] ReferenceFrames { get; set; } internal void AddDiff(RenderTreeDiff diff) { @@ -710,7 +772,11 @@ namespace Microsoft.AspNetCore.Blazor.Test { DiffsByComponentId.Add(componentId, new List()); } - 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(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 targetFactory) { // We have to construct the WeakReference in a separate scope