Further renderer refactoring
This commit is contained in:
parent
d8ae9fd478
commit
e37e22aa27
|
|
@ -14,7 +14,7 @@ namespace HostedInAspNet.Client
|
||||||
{
|
{
|
||||||
// Temporarily render this test component until there's a proper mechanism
|
// Temporarily render this test component until there's a proper mechanism
|
||||||
// for testing this.
|
// for testing this.
|
||||||
new BrowserRenderer().AddComponent("app", new MyComponent());
|
new BrowserRenderer().AddComponent<MyComponent>("app");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@ namespace StandaloneApp
|
||||||
{
|
{
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
new BrowserRenderer()
|
new BrowserRenderer().AddComponent<Home>("app");
|
||||||
.AddComponent("app", new Home());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,6 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering
|
||||||
{
|
{
|
||||||
private readonly int _browserRendererId;
|
private readonly int _browserRendererId;
|
||||||
|
|
||||||
// Ensures the explicitly-added components aren't GCed, because the browser
|
|
||||||
// will still send events referencing them by ID. We only need to store the
|
|
||||||
// top-level components, because the associated ComponentState will reference
|
|
||||||
// all the reachable descendant components of each.
|
|
||||||
private IList<IComponent> _rootComponents = new List<IComponent>();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructs an instance of <see cref="BrowserRenderer"/>.
|
/// Constructs an instance of <see cref="BrowserRenderer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -38,22 +32,33 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering
|
||||||
internal void RenderNewBatchInternal(int componentId)
|
internal void RenderNewBatchInternal(int componentId)
|
||||||
=> RenderNewBatch(componentId);
|
=> RenderNewBatch(componentId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attaches a new root component to the renderer,
|
||||||
|
/// causing it to be displayed in the specified DOM element.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TComponent">The type of the component.</typeparam>
|
||||||
|
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
|
||||||
|
public void AddComponent<TComponent>(string domElementSelector)
|
||||||
|
where TComponent: IComponent
|
||||||
|
{
|
||||||
|
AddComponent(typeof(TComponent), domElementSelector);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Associates the <see cref="IComponent"/> with the <see cref="BrowserRenderer"/>,
|
/// Associates the <see cref="IComponent"/> with the <see cref="BrowserRenderer"/>,
|
||||||
/// causing it to be displayed in the specified DOM element.
|
/// causing it to be displayed in the specified DOM element.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="componentType">The type of the component.</param>
|
||||||
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
|
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
|
||||||
/// <param name="component">The <see cref="IComponent"/>.</param>
|
public void AddComponent(Type componentType, string domElementSelector)
|
||||||
public void AddComponent(string domElementSelector, IComponent component)
|
|
||||||
{
|
{
|
||||||
|
var component = InstantiateComponent(componentType);
|
||||||
var componentId = AssignComponentId(component);
|
var componentId = AssignComponentId(component);
|
||||||
RegisteredFunction.InvokeUnmarshalled<int, string, int, object>(
|
RegisteredFunction.InvokeUnmarshalled<int, string, int, object>(
|
||||||
"attachComponentToElement",
|
"attachComponentToElement",
|
||||||
_browserRendererId,
|
_browserRendererId,
|
||||||
domElementSelector,
|
domElementSelector,
|
||||||
componentId);
|
componentId);
|
||||||
_rootComponents.Add(component);
|
|
||||||
|
|
||||||
RenderNewBatch(componentId);
|
RenderNewBatch(componentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,36 +10,6 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
||||||
{
|
{
|
||||||
internal static class RenderTreeDiffBuilder
|
internal static class RenderTreeDiffBuilder
|
||||||
{
|
{
|
||||||
private struct DiffContext
|
|
||||||
{
|
|
||||||
// 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<RenderTreeEdit> Edits;
|
|
||||||
public readonly ArrayBuilder<RenderTreeFrame> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static RenderTreeDiff ComputeDiff(
|
public static RenderTreeDiff ComputeDiff(
|
||||||
Renderer renderer,
|
Renderer renderer,
|
||||||
RenderBatchBuilder batchBuilder,
|
RenderBatchBuilder batchBuilder,
|
||||||
|
|
@ -419,7 +389,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
||||||
{
|
{
|
||||||
if (oldFrame.AttributeEventHandlerId > 0)
|
if (oldFrame.AttributeEventHandlerId > 0)
|
||||||
{
|
{
|
||||||
diffContext.BatchBuilder.AddDisposedEventHandlerId(oldFrame.AttributeEventHandlerId);
|
diffContext.BatchBuilder.DisposedEventHandlerIds.Append(oldFrame.AttributeEventHandlerId);
|
||||||
}
|
}
|
||||||
InitializeNewAttributeFrame(ref diffContext, ref newFrame);
|
InitializeNewAttributeFrame(ref diffContext, ref newFrame);
|
||||||
var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame);
|
var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame);
|
||||||
|
|
@ -490,7 +460,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
||||||
diffContext.Edits.Append(RenderTreeEdit.RemoveAttribute(diffContext.SiblingIndex, oldFrame.AttributeName));
|
diffContext.Edits.Append(RenderTreeEdit.RemoveAttribute(diffContext.SiblingIndex, oldFrame.AttributeName));
|
||||||
if (oldFrame.AttributeEventHandlerId > 0)
|
if (oldFrame.AttributeEventHandlerId > 0)
|
||||||
{
|
{
|
||||||
diffContext.BatchBuilder.AddDisposedEventHandlerId(oldFrame.AttributeEventHandlerId);
|
diffContext.BatchBuilder.DisposedEventHandlerIds.Append(oldFrame.AttributeEventHandlerId);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -568,7 +538,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
||||||
throw new InvalidOperationException($"Child component already exists during {nameof(InitializeNewComponentFrame)}");
|
throw new InvalidOperationException($"Child component already exists during {nameof(InitializeNewComponentFrame)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
diffContext.Renderer.InstantiateChildComponent(ref frame);
|
diffContext.Renderer.InstantiateChildComponentOnFrame(ref frame);
|
||||||
var childComponentInstance = frame.Component;
|
var childComponentInstance = frame.Component;
|
||||||
|
|
||||||
// All descendants of a component are its properties
|
// All descendants of a component are its properties
|
||||||
|
|
@ -604,9 +574,42 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
||||||
}
|
}
|
||||||
else if (frame.FrameType == RenderTreeFrameType.Attribute && frame.AttributeEventHandlerId > 0)
|
else if (frame.FrameType == RenderTreeFrameType.Attribute && frame.AttributeEventHandlerId > 0)
|
||||||
{
|
{
|
||||||
batchBuilder.AddDisposedEventHandlerId(frame.AttributeEventHandlerId);
|
batchBuilder.DisposedEventHandlerIds.Append(frame.AttributeEventHandlerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
private struct DiffContext
|
||||||
|
{
|
||||||
|
public readonly Renderer Renderer;
|
||||||
|
public readonly RenderBatchBuilder BatchBuilder;
|
||||||
|
public readonly RenderTreeFrame[] OldTree;
|
||||||
|
public readonly RenderTreeFrame[] NewTree;
|
||||||
|
public readonly ArrayBuilder<RenderTreeEdit> Edits;
|
||||||
|
public readonly ArrayBuilder<RenderTreeFrame> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,11 +35,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
|
||||||
_renderTreeBuilderPrevious = new RenderTreeBuilder(renderer);
|
_renderTreeBuilderPrevious = new RenderTreeBuilder(renderer);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public void RenderIntoBatch(RenderBatchBuilder batchBuilder)
|
||||||
/// Regenerates the <see cref="RenderTree"/> and adds the changes to the
|
|
||||||
/// <paramref name="batchBuilder"/>.
|
|
||||||
/// </summary>
|
|
||||||
public void Render(Renderer renderer, RenderBatchBuilder batchBuilder)
|
|
||||||
{
|
{
|
||||||
if (_component is IHandlePropertiesChanged notifyableComponent)
|
if (_component is IHandlePropertiesChanged notifyableComponent)
|
||||||
{
|
{
|
||||||
|
|
@ -59,19 +55,9 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
|
||||||
_renderTreeBuilderPrevious.GetFrames(),
|
_renderTreeBuilderPrevious.GetFrames(),
|
||||||
_renderTreeBuilderCurrent.GetFrames());
|
_renderTreeBuilderCurrent.GetFrames());
|
||||||
batchBuilder.UpdatedComponentDiffs.Append(diff);
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public void DisposeInBatch(RenderBatchBuilder batchBuilder)
|
||||||
/// Notifies the component that it is being disposed.
|
|
||||||
/// </summary>
|
|
||||||
public void NotifyDisposed(RenderBatchBuilder batchBuilder)
|
|
||||||
{
|
{
|
||||||
// TODO: Handle components throwing during dispose. Shouldn't break the whole render batch.
|
// TODO: Handle components throwing during dispose. Shouldn't break the whole render batch.
|
||||||
if (_component is IDisposable disposable)
|
if (_component is IDisposable disposable)
|
||||||
|
|
|
||||||
|
|
@ -6,45 +6,41 @@ using Microsoft.AspNetCore.Blazor.RenderTree;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Blazor.Rendering
|
namespace Microsoft.AspNetCore.Blazor.Rendering
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Collects the data produced by the rendering system during the course
|
||||||
|
/// of rendering a single batch. This tracks both the final output data
|
||||||
|
/// and the intermediate states (such as the queue of components still to
|
||||||
|
/// be rendered).
|
||||||
|
/// </summary>
|
||||||
internal class RenderBatchBuilder
|
internal class RenderBatchBuilder
|
||||||
{
|
{
|
||||||
|
// Primary result data
|
||||||
|
public ArrayBuilder<RenderTreeDiff> UpdatedComponentDiffs { get; } = new ArrayBuilder<RenderTreeDiff>();
|
||||||
|
public ArrayBuilder<int> DisposedComponentIds { get; } = new ArrayBuilder<int>();
|
||||||
|
public ArrayBuilder<int> DisposedEventHandlerIds { get; } = new ArrayBuilder<int>();
|
||||||
|
|
||||||
|
// Buffers referenced by UpdatedComponentDiffs
|
||||||
public ArrayBuilder<RenderTreeEdit> EditsBuffer { get; } = new ArrayBuilder<RenderTreeEdit>();
|
public ArrayBuilder<RenderTreeEdit> EditsBuffer { get; } = new ArrayBuilder<RenderTreeEdit>();
|
||||||
public ArrayBuilder<RenderTreeFrame> ReferenceFramesBuffer { get; } = new ArrayBuilder<RenderTreeFrame>();
|
public ArrayBuilder<RenderTreeFrame> ReferenceFramesBuffer { get; } = new ArrayBuilder<RenderTreeFrame>();
|
||||||
|
|
||||||
|
// State of render pipeline
|
||||||
public Queue<int> ComponentRenderQueue { get; } = new Queue<int>();
|
public Queue<int> ComponentRenderQueue { get; } = new Queue<int>();
|
||||||
|
|
||||||
public Queue<int> ComponentDisposalQueue { get; } = new Queue<int>();
|
public Queue<int> ComponentDisposalQueue { get; } = new Queue<int>();
|
||||||
|
|
||||||
public ArrayBuilder<RenderTreeDiff> UpdatedComponentDiffs { get; set; }
|
|
||||||
= new ArrayBuilder<RenderTreeDiff>();
|
|
||||||
|
|
||||||
private readonly ArrayBuilder<int> _disposedComponentIds = new ArrayBuilder<int>();
|
|
||||||
|
|
||||||
private readonly ArrayBuilder<int> _disposedEventHandlerIds = new ArrayBuilder<int>();
|
|
||||||
|
|
||||||
public ArrayRange<int> GetDisposedEventHandlerIds()
|
|
||||||
=> _disposedEventHandlerIds.ToRange();
|
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
EditsBuffer.Clear();
|
EditsBuffer.Clear();
|
||||||
ReferenceFramesBuffer.Clear();
|
ReferenceFramesBuffer.Clear();
|
||||||
ComponentRenderQueue.Clear();
|
ComponentRenderQueue.Clear();
|
||||||
UpdatedComponentDiffs.Clear();
|
UpdatedComponentDiffs.Clear();
|
||||||
_disposedComponentIds.Clear();
|
DisposedComponentIds.Clear();
|
||||||
_disposedEventHandlerIds.Clear();
|
DisposedEventHandlerIds.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public RenderBatch ToBatch()
|
public RenderBatch ToBatch()
|
||||||
=> new RenderBatch(
|
=> new RenderBatch(
|
||||||
UpdatedComponentDiffs.ToRange(),
|
UpdatedComponentDiffs.ToRange(),
|
||||||
ReferenceFramesBuffer.ToRange(),
|
ReferenceFramesBuffer.ToRange(),
|
||||||
_disposedComponentIds.ToRange());
|
DisposedComponentIds.ToRange());
|
||||||
|
|
||||||
public void AddDisposedComponentId(int componentId)
|
|
||||||
=> _disposedComponentIds.Append(componentId);
|
|
||||||
|
|
||||||
public void AddDisposedEventHandlerId(int attributeEventHandlerId)
|
|
||||||
=> _disposedEventHandlerIds.Append(attributeEventHandlerId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Blazor.Components;
|
||||||
using Microsoft.AspNetCore.Blazor.RenderTree;
|
using Microsoft.AspNetCore.Blazor.RenderTree;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Blazor.Rendering
|
namespace Microsoft.AspNetCore.Blazor.Rendering
|
||||||
|
|
@ -16,15 +15,9 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class Renderer
|
public abstract class Renderer
|
||||||
{
|
{
|
||||||
// Methods for tracking associations between component IDs, instances, and states,
|
|
||||||
// without pinning any of them in memory here. The explictly GC rooted items are the
|
|
||||||
// components explicitly added to the renderer (i.e., top-level components). In turn
|
|
||||||
// these reference descendant components and associated ComponentState instances.
|
|
||||||
private readonly WeakValueDictionary<int, ComponentState> _componentStateById
|
|
||||||
= new WeakValueDictionary<int, ComponentState>();
|
|
||||||
private readonly ConditionalWeakTable<IComponent, ComponentState> _componentStateByComponent
|
|
||||||
= new ConditionalWeakTable<IComponent, ComponentState>();
|
|
||||||
private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it
|
private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it
|
||||||
|
private readonly Dictionary<int, ComponentState> _componentStateById
|
||||||
|
= new Dictionary<int, ComponentState>();
|
||||||
|
|
||||||
// Because rendering is currently synchronous and single-threaded, we can keep re-using the
|
// Because rendering is currently synchronous and single-threaded, we can keep re-using the
|
||||||
// same RenderBatchBuilder instance to avoid reallocating
|
// same RenderBatchBuilder instance to avoid reallocating
|
||||||
|
|
@ -35,35 +28,46 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
|
||||||
private readonly Dictionary<int, UIEventHandler> _eventHandlersById
|
private readonly Dictionary<int, UIEventHandler> _eventHandlersById
|
||||||
= new Dictionary<int, UIEventHandler>();
|
= new Dictionary<int, UIEventHandler>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a new component of the specified type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="componentType">The type of the component to instantiate.</param>
|
||||||
|
/// <returns>The component instance.</returns>
|
||||||
|
protected IComponent InstantiateComponent(Type componentType)
|
||||||
|
{
|
||||||
|
if (!typeof(IComponent).IsAssignableFrom(componentType))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Must implement {nameof(IComponent)}", nameof(componentType));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (IComponent)Activator.CreateInstance(componentType);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Associates the <see cref="IComponent"/> with the <see cref="Renderer"/>, assigning
|
/// Associates the <see cref="IComponent"/> with the <see cref="Renderer"/>, assigning
|
||||||
/// an identifier that is unique within the scope of the <see cref="Renderer"/>.
|
/// an identifier that is unique within the scope of the <see cref="Renderer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="component">The <see cref="IComponent"/>.</param>
|
/// <param name="component">The component.</param>
|
||||||
/// <returns>The assigned identifier for the <see cref="IComponent"/>.</returns>
|
/// <returns>The component's assigned identifier.</returns>
|
||||||
protected int AssignComponentId(IComponent component)
|
protected int AssignComponentId(IComponent component)
|
||||||
{
|
{
|
||||||
lock (_componentStateById)
|
var componentId = _nextComponentId++;
|
||||||
{
|
var componentState = new ComponentState(this, componentId, component);
|
||||||
var componentId = _nextComponentId++;
|
_componentStateById.Add(componentId, componentState);
|
||||||
var componentState = new ComponentState(this, componentId, component);
|
return componentId;
|
||||||
_componentStateById.Add(componentId, componentState);
|
|
||||||
_componentStateByComponent.Add(component, componentState); // Ensure the componentState lives for at least as long as the component
|
|
||||||
return componentId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the visible UI.
|
/// Updates the visible UI.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="renderBatch">The changes to the UI since the previous call.</param>
|
/// <param name="renderBatch">The changes to the UI since the previous call.</param>
|
||||||
internal protected abstract void UpdateDisplay(RenderBatch renderBatch);
|
protected abstract void UpdateDisplay(RenderBatch renderBatch);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the rendered state of the specified <see cref="IComponent"/>.
|
/// Updates the rendered state of the specified <see cref="IComponent"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="componentId">The identifier of the <see cref="IComponent"/> to render.</param>
|
/// <param name="componentId">The identifier of the <see cref="IComponent"/> to render.</param>
|
||||||
protected internal void RenderNewBatch(int componentId)
|
protected void RenderNewBatch(int componentId)
|
||||||
{
|
{
|
||||||
// It's very important that components' rendering logic has no side-effects, and in particular
|
// It's very important that components' rendering logic has no side-effects, and in particular
|
||||||
// components must *not* trigger Render from inside their render logic, otherwise you could
|
// components must *not* trigger Render from inside their render logic, otherwise you could
|
||||||
|
|
@ -84,11 +88,11 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
|
||||||
"Render logic must not have side-effects such as manually triggering other rendering.");
|
"Render logic must not have side-effects such as manually triggering other rendering.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_sharedRenderBatchBuilder.ComponentRenderQueue.Enqueue(componentId);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
RenderInExistingBatch(_sharedRenderBatchBuilder, componentId);
|
// Process render queue until empty
|
||||||
|
|
||||||
// Process
|
|
||||||
while (_sharedRenderBatchBuilder.ComponentRenderQueue.Count > 0)
|
while (_sharedRenderBatchBuilder.ComponentRenderQueue.Count > 0)
|
||||||
{
|
{
|
||||||
var nextComponentIdToRender = _sharedRenderBatchBuilder.ComponentRenderQueue.Dequeue();
|
var nextComponentIdToRender = _sharedRenderBatchBuilder.ComponentRenderQueue.Dequeue();
|
||||||
|
|
@ -96,24 +100,15 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateDisplay(_sharedRenderBatchBuilder.ToBatch());
|
UpdateDisplay(_sharedRenderBatchBuilder.ToBatch());
|
||||||
RemoveEventHandlerIds(_sharedRenderBatchBuilder.GetDisposedEventHandlerIds());
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
RemoveEventHandlerIds(_sharedRenderBatchBuilder.DisposedEventHandlerIds.ToRange());
|
||||||
_sharedRenderBatchBuilder.Clear();
|
_sharedRenderBatchBuilder.Clear();
|
||||||
Interlocked.Exchange(ref _renderBatchLock, 0);
|
Interlocked.Exchange(ref _renderBatchLock, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void RenderInExistingBatch(RenderBatchBuilder batchBuilder, int componentId)
|
|
||||||
=> GetRequiredComponentState(componentId).Render(this, batchBuilder);
|
|
||||||
|
|
||||||
internal void DisposeInExistingBatch(RenderBatchBuilder batchBuilder, int componentId)
|
|
||||||
{
|
|
||||||
GetRequiredComponentState(componentId).NotifyDisposed(batchBuilder);
|
|
||||||
batchBuilder.AddDisposedComponentId(componentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Notifies the specified component that an event has occurred.
|
/// Notifies the specified component that an event has occurred.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -136,7 +131,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void InstantiateChildComponent(ref RenderTreeFrame frame)
|
internal void InstantiateChildComponentOnFrame(ref RenderTreeFrame frame)
|
||||||
{
|
{
|
||||||
if (frame.FrameType != RenderTreeFrameType.Component)
|
if (frame.FrameType != RenderTreeFrameType.Component)
|
||||||
{
|
{
|
||||||
|
|
@ -148,7 +143,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
|
||||||
throw new ArgumentException($"The frame already has a non-null component instance", nameof(frame));
|
throw new ArgumentException($"The frame already has a non-null component instance", nameof(frame));
|
||||||
}
|
}
|
||||||
|
|
||||||
var newComponent = (IComponent)Activator.CreateInstance(frame.ComponentType);
|
var newComponent = InstantiateComponent(frame.ComponentType);
|
||||||
var newComponentId = AssignComponentId(newComponent);
|
var newComponentId = AssignComponentId(newComponent);
|
||||||
frame = frame.WithComponentInstance(newComponentId, newComponent);
|
frame = frame.WithComponentInstance(newComponentId, newComponent);
|
||||||
}
|
}
|
||||||
|
|
@ -160,6 +155,25 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
|
||||||
frame = frame.WithAttributeEventHandlerId(id);
|
frame = frame.WithAttributeEventHandlerId(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ComponentState GetRequiredComponentState(int componentId)
|
||||||
|
=> _componentStateById.TryGetValue(componentId, out var componentState)
|
||||||
|
? componentState
|
||||||
|
: throw new ArgumentException($"The renderer does not have a component with ID {componentId}.");
|
||||||
|
|
||||||
|
private void RenderInExistingBatch(RenderBatchBuilder batchBuilder, int componentId)
|
||||||
|
{
|
||||||
|
GetRequiredComponentState(componentId).RenderIntoBatch(batchBuilder);
|
||||||
|
|
||||||
|
// Process disposal queue now in case it causes further component renders to be enqueued
|
||||||
|
while (batchBuilder.ComponentDisposalQueue.Count > 0)
|
||||||
|
{
|
||||||
|
var disposeComponentId = batchBuilder.ComponentDisposalQueue.Dequeue();
|
||||||
|
GetRequiredComponentState(disposeComponentId).DisposeInBatch(batchBuilder);
|
||||||
|
_componentStateById.Remove(disposeComponentId);
|
||||||
|
batchBuilder.DisposedComponentIds.Append(disposeComponentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void RemoveEventHandlerIds(ArrayRange<int> eventHandlerIds)
|
private void RemoveEventHandlerIds(ArrayRange<int> eventHandlerIds)
|
||||||
{
|
{
|
||||||
var array = eventHandlerIds.Array;
|
var array = eventHandlerIds.Array;
|
||||||
|
|
@ -169,10 +183,5 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
|
||||||
_eventHandlersById.Remove(array[i]);
|
_eventHandlersById.Remove(array[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ComponentState GetRequiredComponentState(int componentId)
|
|
||||||
=> _componentStateById.TryGetValue(componentId, out var componentState)
|
|
||||||
? componentState
|
|
||||||
: throw new ArgumentException($"The renderer does not have a component with ID {componentId}.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -297,7 +297,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
||||||
|
|
||||||
private class TestRenderer : Renderer
|
private class TestRenderer : Renderer
|
||||||
{
|
{
|
||||||
protected internal override void UpdateDisplay(RenderBatch renderBatch)
|
protected override void UpdateDisplay(RenderBatch renderBatch)
|
||||||
=> throw new NotImplementedException();
|
=> throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -785,7 +785,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
||||||
|
|
||||||
private class FakeRenderer : Renderer
|
private class FakeRenderer : Renderer
|
||||||
{
|
{
|
||||||
internal protected override void UpdateDisplay(RenderBatch renderBatch)
|
protected override void UpdateDisplay(RenderBatch renderBatch)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -275,78 +275,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
||||||
Assert.True(renderer2.Batches.Single().DiffsByComponentId.ContainsKey(renderer2ComponentId));
|
Assert.True(renderer2.Batches.Single().DiffsByComponentId.ContainsKey(renderer2ComponentId));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ComponentsAreNotPinnedInMemory()
|
|
||||||
{
|
|
||||||
// It's important that the Renderer base class does not itself pin in memory
|
|
||||||
// any of the component instances that were attached to it (or by extension,
|
|
||||||
// their descendants). This is because as the set of active components changes
|
|
||||||
// over time, we need the GC to be able to release unused ones, and there isn't
|
|
||||||
// any other mechanism for explicitly destroying components when they stop
|
|
||||||
// being referenced.
|
|
||||||
var renderer = new NoOpRenderer();
|
|
||||||
|
|
||||||
AssertCanBeCollected(() =>
|
|
||||||
{
|
|
||||||
var component = new TestComponent(null);
|
|
||||||
renderer.AssignComponentId(component);
|
|
||||||
return component;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CannotRenderComponentsIfGCed()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var renderer = new NoOpRenderer();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var componentId = new Func<int>(() =>
|
|
||||||
{
|
|
||||||
var component = new TestComponent(builder =>
|
|
||||||
throw new NotImplementedException("Should not be invoked"));
|
|
||||||
|
|
||||||
return renderer.AssignComponentId(component);
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Since there are no unit test references to 'component' here, the GC
|
|
||||||
// should be able to collect it
|
|
||||||
GC.Collect();
|
|
||||||
GC.WaitForPendingFinalizers();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.ThrowsAny<Exception>(() =>
|
|
||||||
{
|
|
||||||
renderer.RenderNewBatch(componentId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CanRenderComponentsIfNotGCed()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var renderer = new NoOpRenderer();
|
|
||||||
var didRender = false;
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var component = new TestComponent(builder =>
|
|
||||||
{
|
|
||||||
didRender = true;
|
|
||||||
});
|
|
||||||
var componentId = renderer.AssignComponentId(component);
|
|
||||||
|
|
||||||
// Unlike the preceding test, we still have a reference to the component
|
|
||||||
// instance on the stack here, so the following should not cause it to
|
|
||||||
// be collected. Then when we call RenderComponent, there should be no error.
|
|
||||||
GC.Collect();
|
|
||||||
GC.WaitForPendingFinalizers();
|
|
||||||
|
|
||||||
renderer.RenderNewBatch(componentId);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(didRender);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PreservesChildComponentInstancesWithNoAttributes()
|
public void PreservesChildComponentInstancesWithNoAttributes()
|
||||||
{
|
{
|
||||||
|
|
@ -721,7 +649,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
||||||
public new void RenderNewBatch(int componentId)
|
public new void RenderNewBatch(int componentId)
|
||||||
=> base.RenderNewBatch(componentId);
|
=> base.RenderNewBatch(componentId);
|
||||||
|
|
||||||
protected internal override void UpdateDisplay(RenderBatch renderBatch)
|
protected override void UpdateDisplay(RenderBatch renderBatch)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -740,7 +668,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
||||||
public new void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs args)
|
public new void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs args)
|
||||||
=> base.DispatchEvent(componentId, eventHandlerId, args);
|
=> base.DispatchEvent(componentId, eventHandlerId, args);
|
||||||
|
|
||||||
protected internal override void UpdateDisplay(RenderBatch renderBatch)
|
protected override void UpdateDisplay(RenderBatch renderBatch)
|
||||||
{
|
{
|
||||||
var capturedBatch = new CapturedBatch();
|
var capturedBatch = new CapturedBatch();
|
||||||
Batches.Add(capturedBatch);
|
Batches.Add(capturedBatch);
|
||||||
|
|
@ -879,17 +807,6 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AssertCanBeCollected(Func<object> targetFactory)
|
|
||||||
{
|
|
||||||
// We have to construct the WeakReference in a separate scope
|
|
||||||
// otherwise its target won't be collected on this GC cycle
|
|
||||||
var weakRef = new Func<WeakReference>(
|
|
||||||
() => new WeakReference(targetFactory()))();
|
|
||||||
GC.Collect();
|
|
||||||
GC.WaitForPendingFinalizers();
|
|
||||||
Assert.Null(weakRef.Target);
|
|
||||||
}
|
|
||||||
|
|
||||||
(int, T) FirstWithIndex<T>(IEnumerable<T> items, Predicate<T> predicate)
|
(int, T) FirstWithIndex<T>(IEnumerable<T> items, Predicate<T> predicate)
|
||||||
{
|
{
|
||||||
var index = 0;
|
var index = 0;
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,7 @@ namespace BasicTestApp
|
||||||
public static void MountTestComponent(string componentTypeName)
|
public static void MountTestComponent(string componentTypeName)
|
||||||
{
|
{
|
||||||
var componentType = Type.GetType(componentTypeName);
|
var componentType = Type.GetType(componentTypeName);
|
||||||
var componentInstance = (IComponent)Activator.CreateInstance(componentType);
|
new BrowserRenderer().AddComponent(componentType, "app");
|
||||||
new BrowserRenderer().AddComponent("app", componentInstance);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue