In ComponentState, track parent ComponentState. Be explicit that Renderer only lets you attach root components.

This commit is contained in:
Steve Sanderson 2018-10-05 14:20:22 +01:00
parent c05b98f9cb
commit fa2b61773a
9 changed files with 126 additions and 48 deletions

View File

@ -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

View File

@ -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<object>(
"Blazor._internal.attachRootComponentToElement",

View File

@ -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<RenderTreeEdit> Edits;
public readonly ArrayBuilder<RenderTreeFrame> ReferenceFrames;
public readonly Dictionary<string, int> 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;

View File

@ -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
/// <param name="renderer">The <see cref="Renderer"/> with which the new instance should be associated.</param>
/// <param name="componentId">The externally visible identifier for the <see cref="IComponent"/>. The identifier must be unique in the context of the <see cref="Renderer"/>.</param>
/// <param name="component">The <see cref="IComponent"/> whose state is being tracked.</param>
public ComponentState(Renderer renderer, int componentId, IComponent component)
/// <param name="parentComponentState">The <see cref="ComponentState"/> for the parent component, or null if this is a root component.</param>
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;
}
}

View File

@ -48,10 +48,14 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
/// </summary>
/// <param name="component">The component.</param>
/// <returns>The component's assigned identifier.</returns>
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
}
}
/// <summary>
/// This only needs to exist until there's some other unit-testable functionality
/// that makes use of walking the ancestor hierarchy.
/// </summary>
/// <param name="componentId">The component ID.</param>
/// <returns>The parent component's ID, or null if the component was at the root.</returns>
internal int? TemporaryGetParentComponentIdForTest(int componentId)
=> GetRequiredComponentState(componentId).TemporaryParentComponentIdForTests;
private ComponentState GetRequiredComponentState(int componentId)
=> _componentStateById.TryGetValue(componentId, out var componentState)
? componentState

View File

@ -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)
{

View File

@ -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]

View File

@ -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<AncestryComponent>(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<MessageComponent>(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<EventComponent>(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<UIEventArgs> 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<UIEventArgs> 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<UIEventArgs> 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<AncestryComponent>(0);
builder.AddAttribute(1, nameof(NumDescendants), NumDescendants - 1);
builder.CloseComponent();
}
else
{
builder.AddContent(1, "I'm the final descendant");
}
}
}
}
}

View File

@ -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<CapturedBatch> Batches { get; }
= new List<CapturedBatch>();
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);