aspnetcore/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs

138 lines
7.2 KiB
C#

// 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;
using Microsoft.AspNetCore.Blazor.RenderTree;
using System;
using System.Runtime.CompilerServices;
using System.Threading;
namespace Microsoft.AspNetCore.Blazor.Rendering
{
/// <summary>
/// Provides mechanisms for rendering hierarchies of <see cref="IComponent"/> instances,
/// dispatching events to them, and notifying when the user interface is being updated.
/// </summary>
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
// Because rendering is currently synchronous and single-threaded, we can keep re-using the
// same RenderBatchBuilder instance to avoid reallocating
private readonly RenderBatchBuilder _sharedRenderBatchBuilder = new RenderBatchBuilder();
private int _renderBatchLock = 0;
/// <summary>
/// Associates the <see cref="IComponent"/> with the <see cref="Renderer"/>, assigning
/// an identifier that is unique within the scope of the <see cref="Renderer"/>.
/// </summary>
/// <param name="component">The <see cref="IComponent"/>.</param>
/// <returns>The assigned identifier for the <see cref="IComponent"/>.</returns>
protected int AssignComponentId(IComponent component)
{
lock (_componentStateById)
{
var componentId = _nextComponentId++;
var componentState = new ComponentState(this, componentId, component);
_componentStateById.Add(componentId, componentState);
_componentStateByComponent.Add(component, componentState); // Ensure the componentState lives for at least as long as the component
return componentId;
}
}
/// <summary>
/// Updates the visible UI.
/// </summary>
/// <param name="renderBatch">The changes to the UI since the previous call.</param>
internal protected abstract void UpdateDisplay(RenderBatch renderBatch);
/// <summary>
/// Updates the rendered state of the specified <see cref="IComponent"/>.
/// </summary>
/// <param name="componentId">The identifier of the <see cref="IComponent"/> to render.</param>
protected internal void RenderNewBatch(int componentId)
{
// 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
// easily get hard-to-debug infinite loops.
// Since rendering is currently synchronous and single-threaded, we can enforce the above by
// checking here that no other rendering process is already underway. This also means we only
// need a single _renderBatchBuilder instance that can be reused throughout the lifetime of
// the Renderer instance, which also means we're not allocating on a typical render cycle.
// In the future, if rendering becomes async, we'll need a more sophisticated system of
// capturing successive diffs from each component and probably serializing them for the
// interop calls instead of using shared memory.
// Note that Monitor.TryEnter is not yet supported in Mono WASM, so using the following instead
var renderAlreadyRunning = Interlocked.CompareExchange(ref _renderBatchLock, 1, 0) == 1;
if (renderAlreadyRunning)
{
throw new InvalidOperationException("Cannot render while a render is already in progress. " +
"Render logic must not have side-effects such as manually triggering other rendering.");
}
try
{
RenderInExistingBatch(_sharedRenderBatchBuilder, componentId);
UpdateDisplay(_sharedRenderBatchBuilder.ToBatch());
}
finally
{
_sharedRenderBatchBuilder.Clear();
Interlocked.Exchange(ref _renderBatchLock, 0);
}
}
internal void RenderInExistingBatch(RenderBatchBuilder batchBuilder, int componentId)
{
GetRequiredComponentState(componentId).Render(batchBuilder);
}
internal void DisposeInExistingBatch(RenderBatchBuilder batchBuilder, int componentId)
{
GetRequiredComponentState(componentId).NotifyDisposed();
batchBuilder.AddDisposedComponent(componentId);
}
/// <summary>
/// Notifies the specified component that an event has occurred.
/// </summary>
/// <param name="componentId">The unique identifier for the component within the scope of this <see cref="Renderer"/>.</param>
/// <param name="renderTreeIndex">The index into the component's current render tree that specifies which event handler to invoke.</param>
/// <param name="eventArgs">Arguments to be passed to the event handler.</param>
protected void DispatchEvent(int componentId, int renderTreeIndex, UIEventArgs eventArgs)
=> GetRequiredComponentState(componentId).DispatchEvent(renderTreeIndex, eventArgs);
internal void InstantiateChildComponent(RenderTreeNode[] nodes, int componentNodeIndex)
{
ref var node = ref nodes[componentNodeIndex];
if (node.NodeType != RenderTreeNodeType.Component)
{
throw new ArgumentException($"The node's {nameof(RenderTreeNode.NodeType)} property must equal {RenderTreeNodeType.Component}", nameof(node));
}
if (node.Component != null)
{
throw new ArgumentException($"The node already has a non-null component instance", nameof(node));
}
var newComponent = (IComponent)Activator.CreateInstance(node.ComponentType);
var newComponentId = AssignComponentId(newComponent);
node.SetChildComponentInstance(newComponentId, newComponent);
}
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}.");
}
}