File: Sections\SectionOutlet.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 Microsoft.AspNetCore.Components.Rendering;
 
namespace Microsoft.AspNetCore.Components.Sections;
 
/// <summary>
/// Renders content provided by <see cref="SectionContent"/> components with matching <see cref="SectionId"/>s.
/// </summary>
public sealed class SectionOutlet : IComponent, IDisposable
{
    private static readonly RenderFragment _emptyRenderFragment = _ => { };
    private object? _subscribedIdentifier;
    private RenderHandle _renderHandle;
    private SectionRegistry _registry = default!;
    private SectionContent? _currentContentProvider;
 
    /// <summary>
    /// Gets or sets the <see cref="string"/> ID that determines which <see cref="SectionContent"/> instances will provide
    /// content to this instance.
    /// </summary>
    [Parameter] public string? SectionName { get; set; }
 
    /// <summary>
    /// Gets or sets the <see cref="object"/> ID that determines which <see cref="SectionContent"/> instances will provide
    /// content to this instance.
    /// </summary>
    [Parameter] public object? SectionId { get; set; }
 
    internal IComponent? CurrentLogicalParent => _currentContentProvider;
 
    void IComponent.Attach(RenderHandle renderHandle)
    {
        _renderHandle = renderHandle;
        _registry = _renderHandle.Dispatcher.SectionRegistry;
    }
 
    Task IComponent.SetParametersAsync(ParameterView parameters)
    {
        parameters.SetParameterProperties(this);
 
        object? identifier;
 
        if (SectionName is not null && SectionId is not null)
        {
            throw new InvalidOperationException($"{nameof(SectionOutlet)} requires that '{nameof(SectionName)}' and '{nameof(SectionId)}' cannot both have non-null values.");
        }
        else if (SectionName is not null)
        {
            identifier = SectionName;
        }
        else if (SectionId is not null)
        {
            identifier = SectionId;
        }
        else
        {
            throw new InvalidOperationException($"{nameof(SectionOutlet)} requires a non-null value either for '{nameof(SectionName)}' or '{nameof(SectionId)}'.");
        }
 
        if (!object.Equals(identifier, _subscribedIdentifier))
        {
            if (_subscribedIdentifier is not null)
            {
                _registry.Unsubscribe(_subscribedIdentifier);
            }
 
            _registry.Subscribe(identifier, this);
            _subscribedIdentifier = identifier;
        }
 
        RenderContent();
 
        return Task.CompletedTask;
    }
 
    internal void ContentUpdated(SectionContent? provider)
    {
        _currentContentProvider = provider;
        RenderContent();
    }
 
    private void RenderContent()
    {
        // Here, we guard against rendering after the renderer has been disposed.
        // This can occur after prerendering or when the page is refreshed.
        // In these cases, a no-op is preferred.
        if (_renderHandle.IsRendererDisposed)
        {
            return;
        }
 
        _renderHandle.Render(BuildRenderTree);
    }
 
    private void BuildRenderTree(RenderTreeBuilder builder)
    {
        var fragment = _currentContentProvider?.ChildContent ?? _emptyRenderFragment;
 
        builder.OpenComponent<SectionOutletContentRenderer>(0);
        builder.SetKey(fragment);
        builder.AddComponentParameter(1, SectionOutletContentRenderer.ContentParameterName, fragment);
        builder.CloseComponent();
    }
 
    /// <inheritdoc/>
    public void Dispose()
    {
        if (_subscribedIdentifier is not null)
        {
            _registry.Unsubscribe(_subscribedIdentifier);
        }
    }
 
    // This component simply renders the RenderFragment it is given
    // The reason for rendering SectionOutlet output via this component is so that
    // [1] We can use @key to guarantee that we only preserve descendant component
    //     instances when they come from the same SectionContent, not unrelated ones
    // [2] We know that whenever the SectionContent is changed to another one, there
    //     will be a new ComponentState established to represent this intermediate
    //     component, and it will already have the correct LogicalParentComponentState
    //     so anything computed from this (e.g., whether or not streaming rendering is
    //     enabled) will be freshly re-evaluated, without that information having to
    //     change in place on an existing ComponentState.
    internal sealed class SectionOutletContentRenderer : IComponent
    {
        public const string ContentParameterName = "content";
 
        private RenderHandle _renderHandle;
 
        public void Attach(RenderHandle renderHandle)
        {
            _renderHandle = renderHandle;
        }
 
        public Task SetParametersAsync(ParameterView parameters)
        {
            var fragment = parameters.GetValueOrDefault<RenderFragment>(ContentParameterName)!;
            _renderHandle.Render(fragment);
            return Task.CompletedTask;
        }
    }
}