// 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; } } } }