diff --git a/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs b/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs index dfb00d171d..f1aede68c9 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs +++ b/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs @@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering public void AddComponent(Type componentType, string domElementSelector) { var component = InstantiateComponent(componentType); - var componentId = AssignComponentId(component); + var componentId = AssignRootComponentId(component); // The only reason we're calling this synchronously is so that, if it throws, // we get the exception back *before* attempting the first UpdateDisplay diff --git a/src/Microsoft.AspNetCore.Blazor.Server/Circuits/RemoteRenderer.cs b/src/Microsoft.AspNetCore.Blazor.Server/Circuits/RemoteRenderer.cs index 42ca4ee092..256a51fa4d 100644 --- a/src/Microsoft.AspNetCore.Blazor.Server/Circuits/RemoteRenderer.cs +++ b/src/Microsoft.AspNetCore.Blazor.Server/Circuits/RemoteRenderer.cs @@ -64,7 +64,7 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering public void AddComponent(Type componentType, string domElementSelector) { var component = InstantiateComponent(componentType); - var componentId = AssignComponentId(component); + var componentId = AssignRootComponentId(component); var attachComponentTask = _jsRuntime.InvokeAsync( "Blazor._internal.attachRootComponentToElement", diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs index 917ec59b41..4e6e51d60d 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree var editsBuffer = batchBuilder.EditsBuffer; var editsBufferStartLength = editsBuffer.Count; - var diffContext = new DiffContext(renderer, batchBuilder, oldTree.Array, newTree.Array); + var diffContext = new DiffContext(renderer, batchBuilder, componentId, oldTree.Array, newTree.Array); AppendDiffEntriesForRange(ref diffContext, 0, oldTree.Count, 0, newTree.Count); var editsSegment = editsBuffer.ToSegment(editsBufferStartLength, editsBuffer.Count); @@ -638,7 +638,8 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree throw new InvalidOperationException($"Child component already exists during {nameof(InitializeNewComponentFrame)}"); } - diffContext.Renderer.InstantiateChildComponentOnFrame(ref frame); + var parentComponentId = diffContext.ComponentId; + diffContext.Renderer.InstantiateChildComponentOnFrame(ref frame, parentComponentId); var childComponentInstance = frame.Component; // Set initial parameters @@ -718,16 +719,19 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree public readonly ArrayBuilder Edits; public readonly ArrayBuilder ReferenceFrames; public readonly Dictionary AttributeDiffSet; + public readonly int ComponentId; public int SiblingIndex; public DiffContext( Renderer renderer, RenderBatchBuilder batchBuilder, + int componentId, RenderTreeFrame[] oldTree, RenderTreeFrame[] newTree) { Renderer = renderer; BatchBuilder = batchBuilder; + ComponentId = componentId; OldTree = oldTree; NewTree = newTree; Edits = batchBuilder.EditsBuffer; diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs index e981c9eba0..b6e5537cd6 100644 --- a/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs @@ -1,4 +1,4 @@ -// 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. using System; @@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering internal class ComponentState { 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 ComponentState _parentComponentState; private readonly IComponent _component; private readonly Renderer _renderer; private RenderTreeBuilder _renderTreeBuilderCurrent; @@ -27,9 +28,11 @@ namespace Microsoft.AspNetCore.Blazor.Rendering /// The with which the new instance should be associated. /// The externally visible identifier for the . The identifier must be unique in the context of the . /// The whose state is being tracked. - public ComponentState(Renderer renderer, int componentId, IComponent component) + /// The for the parent component, or null if this is a root component. + public ComponentState(Renderer renderer, int componentId, IComponent component, ComponentState parentComponentState) { _componentId = componentId; + _parentComponentState = parentComponentState; _component = component ?? throw new ArgumentNullException(nameof(component)); _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); _renderTreeBuilderCurrent = new RenderTreeBuilder(renderer); @@ -89,5 +92,10 @@ namespace Microsoft.AspNetCore.Blazor.Rendering public void NotifyRenderCompleted() => (_component as IHandleAfterRender)?.OnAfterRender(); + + // TODO: Remove this once we can remove TemporaryGetParentComponentIdForTest + // from Renderer.cs and corresponding unit test. + public int? TemporaryParentComponentIdForTests + => _parentComponentState?._componentId; } } diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs index 379d921aa8..7567f6afbb 100644 --- a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs @@ -48,10 +48,14 @@ namespace Microsoft.AspNetCore.Blazor.Rendering /// /// The component. /// The component's assigned identifier. - protected int AssignComponentId(IComponent component) + protected int AssignRootComponentId(IComponent component) + => AssignComponentId(component, -1); + + private int AssignComponentId(IComponent component, int parentComponentId) { var componentId = _nextComponentId++; - var componentState = new ComponentState(this, componentId, component); + var parentComponentState = GetOptionalComponentState(parentComponentId); + var componentState = new ComponentState(this, componentId, component, parentComponentState); _componentStateById.Add(componentId, componentState); component.Init(new RenderHandle(this, componentId)); return componentId; @@ -92,7 +96,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering } } - internal void InstantiateChildComponentOnFrame(ref RenderTreeFrame frame) + internal void InstantiateChildComponentOnFrame(ref RenderTreeFrame frame, int parentComponentId) { if (frame.FrameType != RenderTreeFrameType.Component) { @@ -105,7 +109,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering } var newComponent = InstantiateComponent(frame.ComponentType); - var newComponentId = AssignComponentId(newComponent); + var newComponentId = AssignComponentId(newComponent, parentComponentId); frame = frame.WithComponentInstance(newComponentId, newComponent); } @@ -140,6 +144,15 @@ namespace Microsoft.AspNetCore.Blazor.Rendering } } + /// + /// This only needs to exist until there's some other unit-testable functionality + /// that makes use of walking the ancestor hierarchy. + /// + /// The component ID. + /// The parent component's ID, or null if the component was at the root. + internal int? TemporaryGetParentComponentIdForTest(int componentId) + => GetRequiredComponentState(componentId).TemporaryParentComponentIdForTests; + private ComponentState GetRequiredComponentState(int componentId) => _componentStateById.TryGetValue(componentId, out var componentState) ? componentState diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorIntegrationTestBase.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorIntegrationTestBase.cs index fe9a6818fd..101590afe7 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorIntegrationTestBase.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorIntegrationTestBase.cs @@ -1,4 +1,4 @@ -// 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. using System; @@ -368,7 +368,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test public RenderTreeFrame[] LatestBatchReferenceFrames { get; private set; } public void AttachComponent(IComponent component) - => AssignComponentId(component); + => AssignRootComponentId(component); protected override void UpdateDisplay(in RenderBatch renderBatch) { diff --git a/test/Microsoft.AspNetCore.Blazor.Test/LayoutTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/LayoutTest.cs index b3c6c5e024..06402edf6c 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/LayoutTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/LayoutTest.cs @@ -1,4 +1,4 @@ -// 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. using Microsoft.AspNetCore.Blazor.Components; @@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Blazor.Test { _renderer = new TestRenderer(); _layoutDisplayComponent = new LayoutDisplay(); - _layoutDisplayComponentId = _renderer.AssignComponentId(_layoutDisplayComponent); + _layoutDisplayComponentId = _renderer.AssignRootComponentId(_layoutDisplayComponent); } [Fact] diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs index 26f8d39f50..5d33b11f9b 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs @@ -1,4 +1,4 @@ -// 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. using System; @@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Blazor.Test }); // Act - var componentId = renderer.AssignComponentId(component); + var componentId = renderer.AssignRootComponentId(component); component.TriggerRender(); // Assert @@ -58,7 +58,7 @@ namespace Microsoft.AspNetCore.Blazor.Test }); // Act/Assert - var componentId = renderer.AssignComponentId(component); + var componentId = renderer.AssignRootComponentId(component); component.TriggerRender(); var batch = renderer.Batches.Single(); var componentFrame = batch.ReferenceFrames @@ -83,13 +83,46 @@ namespace Microsoft.AspNetCore.Blazor.Test }); } + [Fact] + public void CanWalkTheAncestorHierarchy() + { + // TODO: Instead of testing this directly, once there's some other functionaly + // that relies on the ancestor hierarchy (e.g., deep params), test that instead + // and remove the otherwise unnecessary TemporaryGetParentComponentIdForTest API. + + // Arrange + var renderer = new TestRenderer(); + var rootComponent = new TestComponent(builder => + { + builder.AddContent(0, "Hello"); + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(AncestryComponent.NumDescendants), 2); + builder.CloseComponent(); + }); + + // Act + var componentId = renderer.AssignRootComponentId(rootComponent); + rootComponent.TriggerRender(); + + // Assert + var batch = renderer.Batches.Single(); + var componentIds = batch.ReferenceFrames + .Where(frame => frame.FrameType == RenderTreeFrameType.Component) + .Select(f => f.ComponentId); + Assert.Equal(new[] { 1, 2, 3 }, componentIds); + Assert.Equal(2, renderer.TemporaryGetParentComponentIdForTest(3)); + Assert.Equal(1, renderer.TemporaryGetParentComponentIdForTest(2)); + Assert.Equal(0, renderer.TemporaryGetParentComponentIdForTest(1)); + Assert.Null(renderer.TemporaryGetParentComponentIdForTest(0)); + } + [Fact] public void CanReRenderTopLevelComponents() { // Arrange var renderer = new TestRenderer(); var component = new MessageComponent { Message = "Initial message" }; - var componentId = renderer.AssignComponentId(component); + var componentId = renderer.AssignRootComponentId(component); // Act/Assert: first render component.TriggerRender(); @@ -127,7 +160,7 @@ namespace Microsoft.AspNetCore.Blazor.Test builder.OpenComponent(0); builder.CloseComponent(); }); - var parentComponentId = renderer.AssignComponentId(parentComponent); + var parentComponentId = renderer.AssignRootComponentId(parentComponent); parentComponent.TriggerRender(); var nestedComponentFrame = renderer.Batches.Single() .ReferenceFrames @@ -173,7 +206,7 @@ namespace Microsoft.AspNetCore.Blazor.Test { OnTest = args => { receivedArgs = args; } }; - var componentId = renderer.AssignComponentId(component); + var componentId = renderer.AssignRootComponentId(component); component.TriggerRender(); var eventHandlerId = renderer.Batches.Single() @@ -201,7 +234,7 @@ namespace Microsoft.AspNetCore.Blazor.Test { OnClick = args => { receivedArgs = args; } }; - var componentId = renderer.AssignComponentId(component); + var componentId = renderer.AssignRootComponentId(component); component.TriggerRender(); var eventHandlerId = renderer.Batches.Single() @@ -229,7 +262,7 @@ namespace Microsoft.AspNetCore.Blazor.Test { OnClickAction = () => { receivedArgs = new object(); } }; - var componentId = renderer.AssignComponentId(component); + var componentId = renderer.AssignRootComponentId(component); component.TriggerRender(); var eventHandlerId = renderer.Batches.Single() @@ -258,7 +291,7 @@ namespace Microsoft.AspNetCore.Blazor.Test builder.OpenComponent(0); builder.CloseComponent(); }); - var parentComponentId = renderer.AssignComponentId(parentComponent); + var parentComponentId = renderer.AssignRootComponentId(parentComponent); parentComponent.TriggerRender(); // Arrange: Render nested component @@ -298,7 +331,7 @@ namespace Microsoft.AspNetCore.Blazor.Test builder.CloseElement(); }); - var componentId = renderer.AssignComponentId(component); + var componentId = renderer.AssignRootComponentId(component); component.TriggerRender(); var eventHandlerId = renderer.Batches.Single() @@ -336,9 +369,9 @@ namespace Microsoft.AspNetCore.Blazor.Test var renderer1 = new TestRenderer(); var renderer2 = new TestRenderer(); var component = new MultiRendererComponent(); - var renderer1ComponentId = renderer1.AssignComponentId(component); - renderer2.AssignComponentId(new TestComponent(null)); // Just so they don't get the same IDs - var renderer2ComponentId = renderer2.AssignComponentId(component); + var renderer1ComponentId = renderer1.AssignRootComponentId(component); + renderer2.AssignRootComponentId(new TestComponent(null)); // Just so they don't get the same IDs + var renderer2ComponentId = renderer2.AssignRootComponentId(component); // Act/Assert component.TriggerRender(); @@ -376,7 +409,7 @@ namespace Microsoft.AspNetCore.Blazor.Test builder.CloseComponent(); }); - var rootComponentId = renderer.AssignComponentId(component); + var rootComponentId = renderer.AssignRootComponentId(component); component.TriggerRender(); var nestedComponentFrame = renderer.Batches.Single() @@ -417,7 +450,7 @@ namespace Microsoft.AspNetCore.Blazor.Test builder.CloseComponent(); }); - var rootComponentId = renderer.AssignComponentId(component); + var rootComponentId = renderer.AssignRootComponentId(component); component.TriggerRender(); var originalComponentFrame = renderer.Batches.Single() @@ -453,7 +486,7 @@ namespace Microsoft.AspNetCore.Blazor.Test builder.CloseComponent(); }); - var rootComponentId = renderer.AssignComponentId(component); + var rootComponentId = renderer.AssignRootComponentId(component); component.TriggerRender(); var childComponentId = renderer.Batches.Single() @@ -495,7 +528,7 @@ namespace Microsoft.AspNetCore.Blazor.Test builder.CloseComponent(); }); - var rootComponentId = renderer.AssignComponentId(component); + var rootComponentId = renderer.AssignRootComponentId(component); // Act/Assert 1: First render, capturing child component IDs component.TriggerRender(); @@ -534,7 +567,7 @@ namespace Microsoft.AspNetCore.Blazor.Test var eventCount = 0; Action origEventHandler = args => { eventCount++; }; var component = new EventComponent { OnTest = origEventHandler }; - var componentId = renderer.AssignComponentId(component); + var componentId = renderer.AssignRootComponentId(component); component.TriggerRender(); var origEventHandlerId = renderer.Batches.Single() .ReferenceFrames @@ -571,7 +604,7 @@ namespace Microsoft.AspNetCore.Blazor.Test var eventCount = 0; Action origEventHandler = args => { eventCount++; }; var component = new EventComponent { OnTest = origEventHandler }; - var componentId = renderer.AssignComponentId(component); + var componentId = renderer.AssignRootComponentId(component); component.TriggerRender(); var origEventHandlerId = renderer.Batches.Single() .ReferenceFrames @@ -611,7 +644,7 @@ namespace Microsoft.AspNetCore.Blazor.Test { nameof(EventComponent.OnTest), origEventHandler } } }; - var rootComponentId = renderer.AssignComponentId(component); + var rootComponentId = renderer.AssignRootComponentId(component); component.TriggerRender(); var batch = renderer.Batches.Single(); var rootComponentDiff = batch.DiffsByComponentId[rootComponentId].Single(); @@ -653,7 +686,7 @@ namespace Microsoft.AspNetCore.Blazor.Test var eventCount = 0; Action origEventHandler = args => { eventCount++; }; var component = new EventComponent { OnTest = origEventHandler }; - var componentId = renderer.AssignComponentId(component); + var componentId = renderer.AssignRootComponentId(component); component.TriggerRender(); var origEventHandlerId = renderer.Batches.Single() .ReferenceFrames @@ -699,7 +732,7 @@ namespace Microsoft.AspNetCore.Blazor.Test }); builder.CloseComponent(); }); - var rootComponentId = renderer.AssignComponentId(rootComponent); + var rootComponentId = renderer.AssignRootComponentId(rootComponent); rootComponent.TriggerRender(); var origBatchReferenceFrames = renderer.Batches.Single().ReferenceFrames; var childComponentFrame = origBatchReferenceFrames @@ -781,7 +814,7 @@ namespace Microsoft.AspNetCore.Blazor.Test { builder.AddContent(0, $"Render count: {++renderCount}"); }); - var componentId = renderer.AssignComponentId(component); + var componentId = renderer.AssignRootComponentId(component); // Act/Assert: Can trigger initial render Assert.Equal(0, renderCount); @@ -817,7 +850,7 @@ namespace Microsoft.AspNetCore.Blazor.Test builder.CloseComponent(); builder.AddContent(2, $"Parent render count: {++parentRenderCount}"); }); - var parentComponentId = renderer.AssignComponentId(parent); + var parentComponentId = renderer.AssignRootComponentId(parent); // Act parent.TriggerRender(); @@ -891,7 +924,7 @@ namespace Microsoft.AspNetCore.Blazor.Test } }); - var componentId = renderer.AssignComponentId(component); + var componentId = renderer.AssignRootComponentId(component); component.TriggerRender(); var childComponentId = renderer.Batches.Single() .ReferenceFrames @@ -928,7 +961,7 @@ namespace Microsoft.AspNetCore.Blazor.Test // Arrange: Rendered with textbox enabled var renderer = new TestRenderer(); var component = new BindPlusConditionalAttributeComponent(); - var componentId = renderer.AssignComponentId(component); + var componentId = renderer.AssignRootComponentId(component); component.TriggerRender(); var checkboxChangeEventHandlerId = renderer.Batches.Single() .ReferenceFrames @@ -971,7 +1004,7 @@ namespace Microsoft.AspNetCore.Blazor.Test builder.CloseElement(); }); var renderer = new TestRenderer(); - renderer.AssignComponentId(component); + renderer.AssignRootComponentId(component); // Act: Update the attribute value on the parent component.TriggerRender(); @@ -1001,7 +1034,7 @@ namespace Microsoft.AspNetCore.Blazor.Test { OnUpdateDisplay = _ => onAfterRenderCallCountLog.Add(component.OnAfterRenderCallCount) }; - renderer.AssignComponentId(component); + renderer.AssignRootComponentId(component); // Act component.TriggerRender(); @@ -1042,7 +1075,7 @@ namespace Microsoft.AspNetCore.Blazor.Test } }); var renderer = new TestRenderer(); - var parentComponentId = renderer.AssignComponentId(parentComponent); + var parentComponentId = renderer.AssignRootComponentId(parentComponent); // Act: First render parentComponent.TriggerRender(); @@ -1077,8 +1110,8 @@ namespace Microsoft.AspNetCore.Blazor.Test { } - public new int AssignComponentId(IComponent component) - => base.AssignComponentId(component); + public new int AssignRootComponentId(IComponent component) + => base.AssignRootComponentId(component); protected override void UpdateDisplay(in RenderBatch renderBatch) { @@ -1330,5 +1363,25 @@ namespace Microsoft.AspNetCore.Blazor.Test { } } + + private class AncestryComponent : AutoRenderComponent + { + [Parameter] public int NumDescendants { get; private set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + // Recursively renders more of the same until NumDescendants == 0 + if (NumDescendants > 0) + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(NumDescendants), NumDescendants - 1); + builder.CloseComponent(); + } + else + { + builder.AddContent(1, "I'm the final descendant"); + } + } + } } } diff --git a/test/shared/TestRenderer.cs b/test/shared/TestRenderer.cs index 269e7001a6..d2b058f772 100644 --- a/test/shared/TestRenderer.cs +++ b/test/shared/TestRenderer.cs @@ -1,4 +1,4 @@ -// 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. using System; @@ -24,8 +24,8 @@ namespace Microsoft.AspNetCore.Blazor.Test.Helpers public List Batches { get; } = new List(); - public new int AssignComponentId(IComponent component) - => base.AssignComponentId(component); + public new int AssignRootComponentId(IComponent component) + => base.AssignRootComponentId(component); public new void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs args) => base.DispatchEvent(componentId, eventHandlerId, args);