// 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
{
///
/// Provides mechanisms for rendering hierarchies of instances,
/// dispatching events to them, and notifying when the user interface is being updated.
///
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 _componentStateById
= new WeakValueDictionary();
private readonly ConditionalWeakTable _componentStateByComponent
= new ConditionalWeakTable();
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;
///
/// Associates the with the , assigning
/// an identifier that is unique within the scope of the .
///
/// The .
/// The assigned identifier for the .
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;
}
}
///
/// Updates the visible UI.
///
/// The changes to the UI since the previous call.
internal protected abstract void UpdateDisplay(RenderBatch renderBatch);
///
/// Updates the rendered state of the specified .
///
/// The identifier of the to render.
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);
}
///
/// Notifies the specified component that an event has occurred.
///
/// The unique identifier for the component within the scope of this .
/// The index into the component's current render tree that specifies which event handler to invoke.
/// Arguments to be passed to the event handler.
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}.");
}
}