File: Rendering\ComponentState.cs
Web Access
Project: src\src\Components\Components\src\Microsoft.AspNetCore.Components.csproj (Microsoft.AspNetCore.Components)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Sections;
 
namespace Microsoft.AspNetCore.Components.Rendering;
 
/// <summary>
/// Tracks the rendering state associated with an <see cref="IComponent"/> instance
/// within the context of a <see cref="Renderer"/>. This is an internal implementation
/// detail of <see cref="Renderer"/>.
/// </summary>
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
public class ComponentState : IAsyncDisposable
{
    private readonly Renderer _renderer;
    private readonly bool _hasAnyCascadingParameterSubscriptions;
    private IReadOnlyList<CascadingParameterState> _cascadingParameters;
    private bool _hasCascadingParameters;
    private bool _hasSingleDeliveryCascadingParameters;
    private RenderTreeBuilder _nextRenderTree;
    private ArrayBuilder<RenderTreeFrame>? _latestDirectParametersSnapshot; // Lazily instantiated
    private bool _componentWasDisposed;
 
    /// <summary>
    /// Constructs an instance of <see cref="ComponentState"/>.
    /// </summary>
    /// <param name="renderer">The <see cref="Renderer"/> with which the new instance should be associated.</param>
    /// <param name="componentId">The externally visible identifier for the <see cref="IComponent"/>. The identifier must be unique in the context of the <see cref="Renderer"/>.</param>
    /// <param name="component">The <see cref="IComponent"/> whose state is being tracked.</param>
    /// <param name="parentComponentState">The <see cref="ComponentState"/> for the parent component, or null if this is a root component.</param>
    public ComponentState(Renderer renderer, int componentId, IComponent component, ComponentState? parentComponentState)
    {
        ComponentId = componentId;
        ParentComponentState = parentComponentState;
        Component = component ?? throw new ArgumentNullException(nameof(component));
        LogicalParentComponentState = component is SectionOutlet.SectionOutletContentRenderer
            ? (GetSectionOutletLogicalParent(renderer, (SectionOutlet)parentComponentState!.Component) ?? parentComponentState)
            : parentComponentState;
        _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
        _cascadingParameters = CascadingParameterState.FindCascadingParameters(this, out _hasSingleDeliveryCascadingParameters);
        CurrentRenderTree = new RenderTreeBuilder();
        _nextRenderTree = new RenderTreeBuilder();
 
        if (_cascadingParameters.Count != 0)
        {
            _hasCascadingParameters = true;
            _hasAnyCascadingParameterSubscriptions = AddCascadingParameterSubscriptions();
        }
    }
 
    private static ComponentState? GetSectionOutletLogicalParent(Renderer renderer, SectionOutlet sectionOutlet)
    {
        // This will return null if the SectionOutlet is not currently rendering any content
        if (sectionOutlet.CurrentLogicalParent is { } logicalParent
            && renderer.GetComponentState(logicalParent) is { } logicalParentComponentState)
        {
            return logicalParentComponentState;
        }
 
        return null;
    }
 
    /// <summary>
    /// Gets the component ID.
    /// </summary>
    public int ComponentId { get; }
 
    /// <summary>
    /// Gets the component instance.
    /// </summary>
    public IComponent Component { get; }
 
    /// <summary>
    /// Gets the <see cref="ComponentState"/> of the parent component, or null if this is a root component.
    /// </summary>
    public ComponentState? ParentComponentState { get; }
 
    /// <summary>
    /// Gets the <see cref="ComponentState"/> of the logical parent component, or null if this is a root component.
    /// </summary>
    public ComponentState? LogicalParentComponentState { get; }
 
    internal RenderTreeBuilder CurrentRenderTree { get; set; }
 
    internal Renderer Renderer => _renderer;
 
    internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment, out Exception? renderFragmentException)
    {
        renderFragmentException = null;
 
        // A component might be in the render queue already before getting disposed by an
        // earlier entry in the render queue. In that case, rendering is a no-op.
        if (_componentWasDisposed)
        {
            return;
        }
 
        _nextRenderTree.Clear();
 
        try
        {
            renderFragment(_nextRenderTree);
        }
        catch (Exception ex)
        {
            // If an exception occurs in the render fragment delegate, we won't process the diff in any way, so child components,
            // event handlers, etc., will all be left untouched as if this component didn't re-render at all. The Renderer will
            // then forcibly clear the descendant subtree by rendering an empty fragment for this component.
            renderFragmentException = ex;
            return;
        }
 
        // We don't want to make errors from this be recoverable, because there's no legitimate reason for them to happen
        _nextRenderTree.AssertTreeIsValid(Component);
 
        // Swap the old and new tree builders
        (CurrentRenderTree, _nextRenderTree) = (_nextRenderTree, CurrentRenderTree);
 
        var diff = RenderTreeDiffBuilder.ComputeDiff(
            _renderer,
            batchBuilder,
            ComponentId,
            _nextRenderTree.GetFrames(),
            CurrentRenderTree.GetFrames());
        batchBuilder.UpdatedComponentDiffs.Append(diff);
        batchBuilder.InvalidateParameterViews();
    }
 
    // Callers expect this method to always return a faulted task.
    internal Task NotifyRenderCompletedAsync()
    {
        if (Component is IHandleAfterRender handlerAfterRender)
        {
            try
            {
                return handlerAfterRender.OnAfterRenderAsync();
            }
            catch (OperationCanceledException cex)
            {
                return Task.FromCanceled(cex.CancellationToken);
            }
            catch (Exception ex)
            {
                return Task.FromException(ex);
            }
        }
 
        return Task.CompletedTask;
    }
 
    internal void SetDirectParameters(ParameterView parameters)
    {
        // Note: We should be careful to ensure that the framework never calls
        // IComponent.SetParametersAsync directly elsewhere. We should only call it
        // via ComponentState.SetDirectParameters (or NotifyCascadingValueChanged below).
        // If we bypass this, the component won't receive the cascading parameters nor
        // will it update its snapshot of direct parameters.
 
        if (_hasAnyCascadingParameterSubscriptions)
        {
            // We may need to replay these direct parameters later (in NotifyCascadingValueChanged),
            // but we can't guarantee that the original underlying data won't have mutated in the
            // meantime, since it's just an index into the parent's RenderTreeFrames buffer.
            if (_latestDirectParametersSnapshot == null)
            {
                _latestDirectParametersSnapshot = new ArrayBuilder<RenderTreeFrame>();
            }
 
            parameters.CaptureSnapshot(_latestDirectParametersSnapshot);
        }
 
        if (_hasCascadingParameters)
        {
            parameters = parameters.WithCascadingParameters(_cascadingParameters);
            if (_hasSingleDeliveryCascadingParameters)
            {
                StopSupplyingSingleDeliveryCascadingParameters();
            }
        }
 
        SupplyCombinedParameters(parameters);
    }
 
    private void StopSupplyingSingleDeliveryCascadingParameters()
    {
        // We're optimizing for the case where there are no single-delivery parameters, or if there were, we already
        // removed them. In those cases _cascadingParameters is already up-to-date and gets used as-is without any filtering.
        // In the unusual case were there are single-delivery parameters and we haven't yet removed them, it's OK to
        // go through the extra work and allocation of creating a new list.
        List<CascadingParameterState>? remainingCascadingParameters = null;
        foreach (var param in _cascadingParameters)
        {
            if (!param.ParameterInfo.Attribute.SingleDelivery)
            {
                remainingCascadingParameters ??= new(_cascadingParameters.Count /* upper bound on capacity needed */);
                remainingCascadingParameters.Add(param);
            }
        }
 
        // Now update all the tracking state to match the filtered set
        _hasCascadingParameters = remainingCascadingParameters is not null;
        _cascadingParameters = (IReadOnlyList<CascadingParameterState>?)remainingCascadingParameters ?? Array.Empty<CascadingParameterState>();
        _hasSingleDeliveryCascadingParameters = false;
    }
 
    internal void NotifyCascadingValueChanged(in ParameterViewLifetime lifetime)
    {
        // If the component was already disposed, we must not try to supply new parameters. Among other reasons,
        // _latestDirectParametersSnapshot will already have been disposed and that puts it into an invalid state
        // so we can't even read from it. Note that disposal doesn't instantly trigger unsubscription from cascading
        // values - that only happens when the ComponentState is processed later by the disposal queue.
        if (_componentWasDisposed)
        {
            return;
        }
 
        var directParams = _latestDirectParametersSnapshot != null
            ? new ParameterView(lifetime, _latestDirectParametersSnapshot.Buffer, 0)
            : ParameterView.Empty;
        var allParams = directParams.WithCascadingParameters(_cascadingParameters!);
        SupplyCombinedParameters(allParams);
    }
 
    // This should not be called from anywhere except SetDirectParameters or NotifyCascadingValueChanged.
    // Those two methods know how to correctly combine both cascading and non-cascading parameters to supply
    // a consistent set to the recipient.
    private void SupplyCombinedParameters(ParameterView directAndCascadingParameters)
    {
        // Normalise sync and async exceptions into a Task
        Task setParametersAsyncTask;
        try
        {
            setParametersAsyncTask = Component.SetParametersAsync(directAndCascadingParameters);
        }
        catch (Exception ex)
        {
            setParametersAsyncTask = Task.FromException(ex);
        }
 
        _renderer.AddToPendingTasksWithErrorHandling(setParametersAsyncTask, owningComponentState: this);
    }
 
    private bool AddCascadingParameterSubscriptions()
    {
        var hasSubscription = false;
        var numCascadingParameters = _cascadingParameters!.Count;
 
        for (var i = 0; i < numCascadingParameters; i++)
        {
            var valueSupplier = _cascadingParameters[i].ValueSupplier;
            if (!valueSupplier.IsFixed)
            {
                valueSupplier.Subscribe(this, _cascadingParameters[i].ParameterInfo);
                hasSubscription = true;
            }
        }
 
        return hasSubscription;
    }
 
    private void RemoveCascadingParameterSubscriptions()
    {
        var numCascadingParameters = _cascadingParameters!.Count;
        for (var i = 0; i < numCascadingParameters; i++)
        {
            var supplier = _cascadingParameters[i].ValueSupplier;
            if (!supplier.IsFixed)
            {
                supplier.Unsubscribe(this, _cascadingParameters[i].ParameterInfo);
            }
        }
    }
 
    /// <summary>
    /// Disposes this instance and its associated component.
    /// </summary>
    public virtual ValueTask DisposeAsync()
    {
        _componentWasDisposed = true;
        DisposeBuffers();
 
        // Components shouldn't need to implement IAsyncDisposable and IDisposable simultaneously,
        // but in case they do, we prefer the async overload since we understand the sync overload
        // is implemented for more "constrained" scenarios.
        // Component authors are responsible for their IAsyncDisposable implementations not taking
        // forever.
        if (Component is IAsyncDisposable asyncDisposable)
        {
            return asyncDisposable.DisposeAsync();
        }
        else
        {
            (Component as IDisposable)?.Dispose();
            return ValueTask.CompletedTask;
        }
    }
 
    private void DisposeBuffers()
    {
        ((IDisposable)_nextRenderTree).Dispose();
        ((IDisposable)CurrentRenderTree).Dispose();
        _latestDirectParametersSnapshot?.Dispose();
    }
 
    internal ValueTask DisposeInBatchAsync(RenderBatchBuilder batchBuilder)
    {
        // We don't expect these things to throw.
        RenderTreeDiffBuilder.DisposeFrames(batchBuilder, ComponentId, CurrentRenderTree.GetFrames());
 
        if (_hasAnyCascadingParameterSubscriptions)
        {
            RemoveCascadingParameterSubscriptions();
        }
 
        return DisposeAsync();
    }
 
    private string GetDebuggerDisplay()
    {
        return $"ComponentId = {ComponentId}, Type = {Component.GetType().Name}, Disposed = {_componentWasDisposed}";
    }
}