// 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 { /// /// 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; /// /// 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); } /// /// 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); } /// /// 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 AddText(int sequence, string textContent) => Append(RenderTreeFrame.Text(sequence, textContent ?? string.Empty)); /// /// 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 AddText(int sequence, object textContent) => AddText(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 { // Currently, child components can't have further grandchildren of their own, so it would // technically be possible to skip their CloseElement calls and not track them in _openElementIndices. // However at some point we might want to have the grandchildren frames available at runtime // (rather than being parsed as attributes at compile time) so that we could have APIs for // components to query the complete hierarchy of transcluded frames instead of forcing the // transcluded subtree to be in a particular shape such as representing key/value pairs. // So it's more flexible if we track open/close frames for components explicitly. _openElementIndices.Push(_entries.Count); Append(RenderTreeFrame.ChildComponent(sequence)); } 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; } } } }