Add Init/RenderHandle concepts so components can rerender themselves arbitrarily (e.g., after internal state change)
This commit is contained in:
parent
7bb4bbbe5c
commit
695ddc0fd6
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)}");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) { }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue