// 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}."); } }