// 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.Rendering;
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Blazor.RenderTree
{
// IMPORTANT
//
// Many of these names are used in code generation. Keep these in sync with the code generation code
// See: src/Microsoft.AspNetCore.Blazor.Razor.Extensions/RenderTreeBuilder.cs
///
/// Provides methods for building a collection of entries.
///
public class RenderTreeBuilder
{
private readonly Renderer _renderer;
private readonly ArrayBuilder _entries = new ArrayBuilder(10);
private readonly Stack _openElementIndices = new Stack();
private RenderTreeFrameType? _lastNonAttributeFrameType;
///
/// The reserved parameter name used for supplying child content.
///
public const string ChildContent = nameof(ChildContent);
///
/// Constructs an instance of .
///
/// The associated .
public RenderTreeBuilder(Renderer renderer)
{
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
}
///
/// Appends a frame representing an element, i.e., a container for other frames.
/// In order for the state to be valid, you must
/// also call immediately after appending the
/// new element's child frames.
///
/// An integer that represents the position of the instruction in the source code.
/// A value representing the type of the element.
public void OpenElement(int sequence, string elementName)
{
_openElementIndices.Push(_entries.Count);
Append(RenderTreeFrame.Element(sequence, elementName));
}
///
/// Marks a previously appended element frame as closed. Calls to this method
/// must be balanced with calls to .
///
public void CloseElement()
{
var indexOfEntryBeingClosed = _openElementIndices.Pop();
ref var entry = ref _entries.Buffer[indexOfEntryBeingClosed];
entry = entry.WithElementSubtreeLength(_entries.Count - indexOfEntryBeingClosed);
}
///
/// Appends a frame representing text content.
///
/// An integer that represents the position of the instruction in the source code.
/// Content for the new text frame.
public void AddContent(int sequence, string textContent)
=> Append(RenderTreeFrame.Text(sequence, textContent ?? string.Empty));
///
/// Appends frames representing an arbitrary fragment of content.
///
/// An integer that represents the position of the instruction in the source code.
/// Content to append.
public void AddContent(int sequence, RenderFragment fragment)
{
if (fragment != null)
{
// We surround the fragment with a region delimiter to indicate that the
// sequence numbers inside the fragment are unrelated to the sequence numbers
// outside it. If we didn't do this, the diffing logic might produce inefficient
// diffs depending on how the sequence numbers compared.
OpenRegion(sequence);
fragment(this);
CloseRegion();
}
}
///
/// Appends a frame representing text content.
///
/// An integer that represents the position of the instruction in the source code.
/// Content for the new text frame.
public void AddContent(int sequence, object textContent)
=> AddContent(sequence, textContent?.ToString());
///
/// Appends a frame representing a string-valued attribute.
/// The attribute is associated with the most recently added element.
///
/// An integer that represents the position of the instruction in the source code.
/// The name of the attribute.
/// The value of the attribute.
public void AddAttribute(int sequence, string name, string value)
{
AssertCanAddAttribute();
Append(RenderTreeFrame.Attribute(sequence, name, value));
}
///
/// Appends a frame representing an -valued attribute.
/// The attribute is associated with the most recently added element.
///
/// An integer that represents the position of the instruction in the source code.
/// The name of the attribute.
/// The value of the attribute.
public void AddAttribute(int sequence, string name, UIEventHandler value)
{
AssertCanAddAttribute();
Append(RenderTreeFrame.Attribute(sequence, name, value));
}
///
/// Appends a frame representing a string-valued attribute.
/// The attribute is associated with the most recently added element.
///
/// An integer that represents the position of the instruction in the source code.
/// The name of the attribute.
/// The value of the attribute.
public void AddAttribute(int sequence, string name, object value)
{
if (_lastNonAttributeFrameType == RenderTreeFrameType.Element)
{
// Element attribute values can only be strings or UIEventHandler
Append(RenderTreeFrame.Attribute(sequence, name, value.ToString()));
}
else if (_lastNonAttributeFrameType == RenderTreeFrameType.Component)
{
Append(RenderTreeFrame.Attribute(sequence, name, value));
}
else
{
// This is going to throw. Calling it just to get a consistent exception message.
AssertCanAddAttribute();
}
}
///
/// Appends a frame representing an attribute.
/// The attribute is associated with the most recently added element.
///
/// An integer that represents the position of the instruction in the source code.
/// The name of the attribute.
/// The value of the attribute.
public void AddAttribute(int sequence, RenderTreeFrame frame)
{
if (frame.FrameType != RenderTreeFrameType.Attribute)
{
throw new ArgumentException($"The {nameof(frame.FrameType)} must be {RenderTreeFrameType.Attribute}.");
}
AssertCanAddAttribute();
Append(frame.WithAttributeSequence(sequence));
}
///
/// Appends a frame representing a child component.
///
/// The type of the child component.
/// An integer that represents the position of the instruction in the source code.
public void OpenComponent(int sequence) where TComponent : IComponent
=> OpenComponentUnchecked(sequence, typeof(TComponent));
///
/// Appends a frame representing a child component.
///
/// An integer that represents the position of the instruction in the source code.
/// The type of the child component.
public void OpenComponent(int sequence, Type componentType)
{
if (!typeof(IComponent).IsAssignableFrom(componentType))
{
throw new ArgumentException($"The component type must implement {typeof(IComponent).FullName}.");
}
OpenComponentUnchecked(sequence, componentType);
}
private void OpenComponentUnchecked(int sequence, Type componentType)
{
_openElementIndices.Push(_entries.Count);
Append(RenderTreeFrame.ChildComponent(sequence, componentType));
}
///
/// Marks a previously appended component frame as closed. Calls to this method
/// must be balanced with calls to .
///
public void CloseComponent()
{
var indexOfEntryBeingClosed = _openElementIndices.Pop();
ref var entry = ref _entries.Buffer[indexOfEntryBeingClosed];
entry = entry.WithComponentSubtreeLength(_entries.Count - indexOfEntryBeingClosed);
}
// Internal for tests
// Not public because there's no current use case for user code defining regions arbitrarily.
// Currently the sole use case for regions is when appending a RenderFragment.
internal void OpenRegion(int sequence)
{
_openElementIndices.Push(_entries.Count);
Append(RenderTreeFrame.Region(sequence));
}
// See above for why this is not public
internal void CloseRegion()
{
var indexOfEntryBeingClosed = _openElementIndices.Pop();
ref var entry = ref _entries.Buffer[indexOfEntryBeingClosed];
entry = entry.WithRegionSubtreeLength(_entries.Count - indexOfEntryBeingClosed);
}
private void AssertCanAddAttribute()
{
if (_lastNonAttributeFrameType != RenderTreeFrameType.Element
&& _lastNonAttributeFrameType != RenderTreeFrameType.Component)
{
throw new InvalidOperationException($"Attributes may only be added immediately after frames of type {RenderTreeFrameType.Element} or {RenderTreeFrameType.Component}");
}
}
///
/// Clears the builder.
///
public void Clear()
{
_entries.Clear();
_openElementIndices.Clear();
_lastNonAttributeFrameType = null;
}
///
/// Returns the values that have been appended.
///
/// An array range of values.
public ArrayRange GetFrames() =>
_entries.ToRange();
private void Append(in RenderTreeFrame frame)
{
_entries.Append(frame);
var frameType = frame.FrameType;
if (frameType != RenderTreeFrameType.Attribute)
{
_lastNonAttributeFrameType = frame.FrameType;
}
}
}
}