Add Init/RenderHandle concepts so components can rerender themselves arbitrarily (e.g., after internal state change)

This commit is contained in:
Steve Sanderson 2018-02-13 11:49:33 +00:00
parent 7bb4bbbe5c
commit 695ddc0fd6
9 changed files with 250 additions and 14 deletions

View File

@ -20,6 +20,10 @@ namespace HostedInAspNet.Client
internal class MyComponent : IComponent
{
public void Init(RenderHandle renderHandle)
{
}
public void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "h1");

View File

@ -13,6 +13,21 @@ namespace Microsoft.AspNetCore.Blazor.Components
/// </summary>
public abstract class BlazorComponent : IComponent, IHandlePropertiesChanged
{
private RenderHandle _renderHandle;
void IComponent.Init(RenderHandle renderHandle)
{
// This implicitly means a BlazorComponent can only be associated with a single
// renderer. That's the only use case we have right now. If there was ever a need,
// a component could hold a collection of render handles.
if (_renderHandle.IsInitalised)
{
throw new InvalidOperationException($"The render handle is already set. Cannot initialize a {nameof(BlazorComponent)} more than once.");
}
_renderHandle = renderHandle;
}
/// <inheritdoc />
public virtual void BuildRenderTree(RenderTreeBuilder builder)
{

View File

@ -10,6 +10,12 @@ namespace Microsoft.AspNetCore.Blazor.Components
/// </summary>
public interface IComponent
{
/// <summary>
/// Initializes the component.
/// </summary>
/// <param name="renderHandle">A <see cref="RenderHandle"/> that allows the component to be rendered.</param>
void Init(RenderHandle renderHandle);
/// <summary>
/// Builds a <see cref="RenderTree"/> representing the current state of the component.
/// </summary>

View File

@ -0,0 +1,43 @@
// 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.Rendering;
using System;
namespace Microsoft.AspNetCore.Blazor.Components
{
/// <summary>
/// Allows a component to notify the renderer that it should be rendered.
/// </summary>
public readonly struct RenderHandle
{
private readonly Renderer _renderer;
private readonly int _componentId;
internal RenderHandle(Renderer renderer, int componentId)
{
_renderer = renderer ?? throw new System.ArgumentNullException(nameof(renderer));
_componentId = componentId;
}
/// <summary>
/// Gets a value that indicates whether the <see cref="RenderHandle"/> has been
/// initialised and is ready to use.
/// </summary>
public bool IsInitalised
=> _renderer != null;
/// <summary>
/// Notifies the renderer that the component should be rendered.
/// </summary>
public void Render()
{
if (_renderer == null)
{
throw new InvalidOperationException("The render handle is not yet assigned.");
}
_renderer.ComponentRequestedRender(_componentId);
}
}
}

View File

@ -54,6 +54,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
var componentId = _nextComponentId++;
var componentState = new ComponentState(this, componentId, component);
_componentStateById.Add(componentId, componentState);
component.Init(new RenderHandle(this, componentId));
return componentId;
}
@ -155,6 +156,20 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
frame = frame.WithAttributeEventHandlerId(id);
}
internal void ComponentRequestedRender(int componentId)
{
// TODO: Clean up the locking around rendering. The Renderer doesn't really need
// to be thread-safe, and the following code isn't actually thread-safe anyway.
if (_renderBatchLock == 0)
{
RenderNewBatch(componentId);
}
else
{
_sharedRenderBatchBuilder.ComponentRenderQueue.Enqueue(componentId);
}
}
private ComponentState GetRequiredComponentState(int componentId)
=> _componentStateById.TryGetValue(componentId, out var componentState)
? componentState

View File

@ -478,6 +478,10 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
public class TestComponent : IComponent
{
public void Init(RenderHandle renderHandle)
{
}
public void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddText(0, $"Hello from {nameof(TestComponent)}");

View File

@ -291,6 +291,8 @@ namespace Microsoft.AspNetCore.Blazor.Test
private class TestComponent : IComponent
{
public void Init(RenderHandle renderHandle) { }
public void BuildRenderTree(RenderTreeBuilder builder)
=> throw new NotImplementedException();
}

View File

@ -798,13 +798,14 @@ namespace Microsoft.AspNetCore.Blazor.Test
public string ReadonlyProperty { get; private set; }
private string PrivateProperty { get; set; }
public void BuildRenderTree(RenderTreeBuilder builder)
{
}
public void Init(RenderHandle renderHandle) { }
public void BuildRenderTree(RenderTreeBuilder builder) { }
}
private class FakeComponent2 : IComponent
{
public void Init(RenderHandle renderHandle) { }
public void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddText(100, $"Hello from {nameof(FakeComponent2)}");
@ -815,11 +816,16 @@ namespace Microsoft.AspNetCore.Blazor.Test
{
public int DisposalCount { get; private set; }
public void Dispose() => DisposalCount++;
public void Init(RenderHandle renderHandle) { }
public void BuildRenderTree(RenderTreeBuilder builder) { }
}
private class NonDisposableComponent : IComponent
{
public void Init(RenderHandle renderHandle) { }
public void BuildRenderTree(RenderTreeBuilder builder) { }
}

View File

@ -641,6 +641,110 @@ namespace Microsoft.AspNetCore.Blazor.Test
Assert.Equal(1, eventCount);
}
[Fact]
public void ComponentCannotTriggerRenderBeforeRenderHandleAssigned()
{
// Arrange
var component = new TestComponent(builder => { });
// Act/Assert
var ex = Assert.Throws<InvalidOperationException>(() =>
{
component.TriggerRender();
});
Assert.Equal("The render handle is not yet assigned.", ex.Message);
}
[Fact]
public void ComponentCanTriggerRenderWhenNoBatchIsInProgress()
{
// Arrange
var renderer = new TestRenderer();
var renderCount = 0;
var component = new TestComponent(builder =>
{
builder.AddText(0, $"Render count: {++renderCount}");
});
var componentId = renderer.AssignComponentId(component);
// Act/Assert: Can trigger initial render
Assert.Equal(0, renderCount);
component.TriggerRender();
Assert.Equal(1, renderCount);
var batch1 = renderer.Batches.Single();
var edit1 = batch1.DiffsByComponentId[componentId].Single().Edits.Single();
Assert.Equal(RenderTreeEditType.PrependFrame, edit1.Type);
AssertFrame.Text(batch1.ReferenceFrames[edit1.ReferenceFrameIndex],
"Render count: 1", 0);
// Act/Assert: Can trigger subsequent render
component.TriggerRender();
Assert.Equal(2, renderCount);
var batch2 = renderer.Batches.Skip(1).Single();
var edit2 = batch2.DiffsByComponentId[componentId].Single().Edits.Single();
Assert.Equal(RenderTreeEditType.UpdateText, edit2.Type);
AssertFrame.Text(batch2.ReferenceFrames[edit2.ReferenceFrameIndex],
"Render count: 2", 0);
}
[Fact]
public void ComponentCanTriggerRenderWhenExistingBatchIsInProgress()
{
// Arrange
var renderer = new TestRenderer();
TestComponent parent = null;
var parentRenderCount = 0;
parent = new TestComponent(builder =>
{
builder.OpenComponent<ReRendersParentComponent>(0);
builder.AddAttribute(1, nameof(ReRendersParentComponent.Parent), parent);
builder.CloseComponent();
builder.AddText(2, $"Parent render count: {++parentRenderCount}");
});
var parentComponentId = renderer.AssignComponentId(parent);
// Act
renderer.RenderNewBatch(parentComponentId);
// Assert
var batch = renderer.Batches.Single();
Assert.Equal(3, batch.DiffsInOrder.Count);
// First is the parent component's initial render
var diff1 = batch.DiffsInOrder[0];
Assert.Equal(parentComponentId, diff1.ComponentId);
Assert.Collection(diff1.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Component<ReRendersParentComponent>(
batch.ReferenceFrames[edit.ReferenceFrameIndex]);
},
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Text(
batch.ReferenceFrames[edit.ReferenceFrameIndex],
"Parent render count: 1");
});
// Second is the child component's single render
var diff2 = batch.DiffsInOrder[1];
Assert.NotEqual(parentComponentId, diff2.ComponentId);
var diff2edit = diff2.Edits.Single();
Assert.Equal(RenderTreeEditType.PrependFrame, diff2edit.Type);
AssertFrame.Text(batch.ReferenceFrames[diff2edit.ReferenceFrameIndex],
"Child is here");
// Third is the parent's triggered render
var diff3 = batch.DiffsInOrder[2];
Assert.Equal(parentComponentId, diff3.ComponentId);
var diff3edit = diff3.Edits.Single();
Assert.Equal(RenderTreeEditType.UpdateText, diff3edit.Type);
AssertFrame.Text(batch.ReferenceFrames[diff3edit.ReferenceFrameIndex],
"Parent render count: 2");
}
private class NoOpRenderer : Renderer
{
public new int AssignComponentId(IComponent component)
@ -690,6 +794,9 @@ namespace Microsoft.AspNetCore.Blazor.Test
public IDictionary<int, List<RenderTreeDiff>> DiffsByComponentId { get; }
= new Dictionary<int, List<RenderTreeDiff>>();
public IList<RenderTreeDiff> DiffsInOrder { get; }
= new List<RenderTreeDiff>();
public IList<int> DisposedComponentIDs { get; set; }
public RenderTreeFrame[] ReferenceFrames { get; set; }
@ -702,14 +809,17 @@ namespace Microsoft.AspNetCore.Blazor.Test
}
// Clone the diff, because its underlying storage will get reused in subsequent batches
DiffsByComponentId[componentId].Add(new RenderTreeDiff(
var diffClone = new RenderTreeDiff(
diff.ComponentId,
new ArraySegment<RenderTreeEdit>(diff.Edits.ToArray())));
new ArraySegment<RenderTreeEdit>(diff.Edits.ToArray()));
DiffsByComponentId[componentId].Add(diffClone);
DiffsInOrder.Add(diffClone);
}
}
private class TestComponent : IComponent
{
private RenderHandle _renderHandle;
private Action<RenderTreeBuilder> _renderAction;
public TestComponent(Action<RenderTreeBuilder> renderAction)
@ -717,14 +827,26 @@ namespace Microsoft.AspNetCore.Blazor.Test
_renderAction = renderAction;
}
public void Init(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}
public void BuildRenderTree(RenderTreeBuilder builder)
=> _renderAction(builder);
public void TriggerRender()
=> _renderHandle.Render();
}
private class MessageComponent : IComponent
{
public string Message { get; set; }
public void Init(RenderHandle renderHandle)
{
}
public void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddText(0, Message);
@ -737,6 +859,10 @@ namespace Microsoft.AspNetCore.Blazor.Test
public string StringProperty { get; set; }
public object ObjectProperty { get; set; }
public void Init(RenderHandle renderHandle)
{
}
public void BuildRenderTree(RenderTreeBuilder builder)
{
}
@ -747,6 +873,10 @@ namespace Microsoft.AspNetCore.Blazor.Test
public UIEventHandler Handler { get; set; }
public bool SkipElement { get; set; }
public void Init(RenderHandle renderHandle)
{
}
public void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "grandparent");
@ -770,6 +900,10 @@ namespace Microsoft.AspNetCore.Blazor.Test
public bool IncludeChild { get; set; }
public IDictionary<string, object> ChildParameters { get; set; }
public void Init(RenderHandle renderHandle)
{
}
public void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddText(0, "Parent here");
@ -796,6 +930,10 @@ namespace Microsoft.AspNetCore.Blazor.Test
public int IntProperty { get; set; }
public void Init(RenderHandle renderHandle)
{
}
public void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddText(0, $"Notifications: {NotificationsCount}");
@ -806,21 +944,24 @@ namespace Microsoft.AspNetCore.Blazor.Test
NotificationsCount++;
}
}
(int, T) FirstWithIndex<T>(IEnumerable<T> items, Predicate<T> predicate)
private class ReRendersParentComponent : IComponent
{
var index = 0;
foreach (var item in items)
public TestComponent Parent { get; set; }
private bool _isFirstTime = true;
public void Init(RenderHandle renderHandle) { }
public void BuildRenderTree(RenderTreeBuilder builder)
{
if (predicate(item))
if (_isFirstTime) // Don't want an infinite loop
{
return (index, item);
_isFirstTime = false;
Parent.TriggerRender();
}
index++;
builder.AddText(0, "Child is here");
}
throw new InvalidOperationException("No matching element was found.");
}
}
}