File: System\Windows\Forms\Controls\TreeView\TreeNodeCollection.cs
Web Access
Project: src\src\System.Windows.Forms\src\System.Windows.Forms.csproj (System.Windows.Forms)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections;
using System.ComponentModel;
using System.Drawing.Design;
using System.Windows.Forms.Primitives;
 
namespace System.Windows.Forms;
 
[Editor($"System.Windows.Forms.Design.TreeNodeCollectionEditor, {AssemblyRef.SystemDesign}", typeof(UITypeEditor))]
public class TreeNodeCollection : IList
{
    private readonly TreeNode _owner;
 
    ///  A caching mechanism for key accessor
    ///  We use an index here rather than control so that we don't have lifetime
    ///  issues by holding on to extra references.
    private int _lastAccessedIndex = -1;
 
    internal TreeNodeCollection(TreeNode owner)
    {
        _owner = owner;
    }
 
    // This index is used to optimize performance of AddRange
    // items are added from last to first after this index
    // (to work around TV_INSertItem comctl32 perf issue with consecutive adds in the end of the list)
    internal int FixedIndex { get; set; } = -1;
 
    public virtual TreeNode this[int index]
    {
        get
        {
            ArgumentOutOfRangeException.ThrowIfNegative(index);
            ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, _owner._childCount);
 
            return _owner._children[index];
        }
        set
        {
            ArgumentOutOfRangeException.ThrowIfNegative(index);
            ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, _owner._childCount);
            ArgumentNullException.ThrowIfNull(value);
 
            TreeView tv = _owner._treeView!;
            TreeNode actual = _owner._children[index];
 
            if (value._treeView is not null && value._treeView.Handle != tv.Handle)
            {
                throw new ArgumentException(string.Format(SR.TreeNodeBoundToAnotherTreeView), nameof(value));
            }
 
            if (tv._nodesByHandle.ContainsKey(value.Handle) && value._index != index)
            {
                throw new ArgumentException(string.Format(SR.OnlyOneControl, value.Text), nameof(value));
            }
 
            if (tv._nodesByHandle.ContainsKey(value.Handle)
                && value.Handle == actual.Handle
                && value._index == index)
            {
                return;
            }
 
            Remove(actual);
            Insert(index, value);
        }
    }
 
    object? IList.this[int index]
    {
        get => this[index];
        set
        {
            if (value is TreeNode treeNode)
            {
                this[index] = treeNode;
            }
            else
            {
                throw new ArgumentException(SR.TreeNodeCollectionBadTreeNode, nameof(value));
            }
        }
    }
 
    /// <summary>
    ///  Retrieves the child control with the specified key.
    /// </summary>
    public virtual TreeNode? this[string? key]
    {
        get
        {
            // We do not support null and empty string as valid keys.
            if (string.IsNullOrEmpty(key))
            {
                return null;
            }
 
            // Search for the key in our collection
            int index = IndexOfKey(key);
            if (IsValidIndex(index))
            {
                return this[index];
            }
            else
            {
                return null;
            }
        }
    }
 
    // Make this property available to Intellisense. (Removed the EditorBrowsable attribute.)
    [Browsable(false)]
    public int Count => _owner._childCount;
 
    object ICollection.SyncRoot => this;
 
    bool ICollection.IsSynchronized => false;
 
    bool IList.IsFixedSize => false;
 
    public bool IsReadOnly => false;
 
    /// <summary>
    ///  Creates a new child node under this node. Child node is positioned after siblings.
    /// </summary>
    public virtual TreeNode Add(string? text)
    {
        TreeNode tn = new(text);
        Add(tn);
        return tn;
    }
 
    // <-- NEW ADD OVERLOADS IN WHIDBEY
 
    /// <summary>
    ///  Creates a new child node under this node. Child node is positioned after siblings.
    /// </summary>
    public virtual TreeNode Add(string? key, string? text)
    {
        TreeNode tn = new(text)
        {
            Name = key
        };
        Add(tn);
        return tn;
    }
 
    /// <summary>
    ///  Creates a new child node under this node. Child node is positioned after siblings.
    /// </summary>
    public virtual TreeNode Add(string? key, string? text, int imageIndex)
    {
        TreeNode tn = new(text)
        {
            Name = key,
            ImageIndex = imageIndex
        };
        Add(tn);
        return tn;
    }
 
    /// <summary>
    ///  Creates a new child node under this node. Child node is positioned after siblings.
    /// </summary>
    public virtual TreeNode Add(string? key, string? text, string? imageKey)
    {
        TreeNode tn = new(text)
        {
            Name = key,
            ImageKey = imageKey
        };
        Add(tn);
        return tn;
    }
 
    /// <summary>
    ///  Creates a new child node under this node. Child node is positioned after siblings.
    /// </summary>
    public virtual TreeNode Add(string? key, string? text, int imageIndex, int selectedImageIndex)
    {
        TreeNode tn = new(text, imageIndex, selectedImageIndex)
        {
            Name = key
        };
        Add(tn);
        return tn;
    }
 
    /// <summary>
    ///  Creates a new child node under this node. Child node is positioned after siblings.
    /// </summary>
    public virtual TreeNode Add(string? key, string? text, string? imageKey, string? selectedImageKey)
    {
        TreeNode tn = new(text)
        {
            Name = key,
            ImageKey = imageKey,
            SelectedImageKey = selectedImageKey
        };
        Add(tn);
        return tn;
    }
 
    // END - NEW ADD OVERLOADS IN WHIDBEY -->
 
    public virtual void AddRange(params TreeNode[] nodes)
    {
        ArgumentNullException.ThrowIfNull(nodes);
 
        if (nodes.Length == 0)
        {
            return;
        }
 
        TreeView? tv = _owner.TreeView;
        if (tv is not null && nodes.Length > TreeNode.MAX_TREENODES_OPS)
        {
            tv.BeginUpdate();
        }
 
        if (!LocalAppContextSwitches.TreeNodeCollectionAddRangeRespectsSortOrder || tv is null || !tv.Sorted)
        {
            _owner.Nodes.FixedIndex = _owner._childCount;
        }
 
        _owner.EnsureCapacity(nodes.Length);
        for (int i = nodes.Length - 1; i >= 0; i--)
        {
            AddInternal(nodes[i], delta: i);
        }
 
        _owner.Nodes.FixedIndex = -1;
        if (tv is not null && nodes.Length > TreeNode.MAX_TREENODES_OPS)
        {
            tv.EndUpdate();
        }
    }
 
    public TreeNode[] Find(string key, bool searchAllChildren)
    {
        key.ThrowIfNullOrEmptyWithMessage(SR.FindKeyMayNotBeEmptyOrNull);
 
        List<TreeNode> foundNodes = FindInternal(key, searchAllChildren, this, []);
 
        return [.. foundNodes];
    }
 
    private static List<TreeNode> FindInternal(
        string key,
        bool searchAllChildren,
        TreeNodeCollection treeNodeCollectionToLookIn,
        List<TreeNode> foundTreeNodes)
    {
        // Perform breadth first search - as it's likely people will want tree nodes belonging
        // to the same parent close to each other.
        for (int i = 0; i < treeNodeCollectionToLookIn.Count; i++)
        {
            if (treeNodeCollectionToLookIn[i] is null)
            {
                continue;
            }
 
            if (WindowsFormsUtils.SafeCompareStrings(treeNodeCollectionToLookIn[i].Name, key, ignoreCase: true))
            {
                foundTreeNodes.Add(treeNodeCollectionToLookIn[i]);
            }
        }
 
        // Optional recursive search for controls in child collections.
        if (searchAllChildren)
        {
            for (int i = 0; i < treeNodeCollectionToLookIn.Count; i++)
            {
                if (treeNodeCollectionToLookIn[i] is null)
                {
                    continue;
                }
 
                if ((treeNodeCollectionToLookIn[i].Nodes is not null) && treeNodeCollectionToLookIn[i].Nodes.Count > 0)
                {
                    // If it has a valid child collection, append those results to our collection.
                    foundTreeNodes = FindInternal(key, searchAllChildren, treeNodeCollectionToLookIn[i].Nodes, foundTreeNodes);
                }
            }
        }
 
        return foundTreeNodes;
    }
 
    /// <summary>
    ///  Adds a new child node to this node. Child node is positioned after siblings.
    /// </summary>
    public virtual int Add(TreeNode node) => AddInternal(node, delta: 0);
 
    private int AddInternal(TreeNode node, int delta)
    {
        ArgumentNullException.ThrowIfNull(node);
 
        if (node.HTREEITEMInternal != IntPtr.Zero)
        {
            throw new ArgumentException(string.Format(SR.OnlyOneControl, node.Text), nameof(node));
        }
 
        // Check for ParentingCycle
        _owner.CheckParentingCycle(node);
 
        // If the TreeView is sorted, index is ignored
        TreeView? tv = _owner.TreeView;
 
        if (tv is not null)
        {
            foreach (TreeNode treeNode in node.GetSelfAndChildNodes())
            {
                KeyboardToolTipStateMachine.Instance.Hook(treeNode, tv.KeyboardToolTip);
            }
        }
 
        if (tv is not null && tv.Sorted)
        {
            return _owner.AddSorted(tv, node);
        }
 
        node._parent = _owner;
        int fixedIndex = _owner.Nodes.FixedIndex;
        if (fixedIndex != -1)
        {
            node._index = fixedIndex + delta;
        }
        else
        {
            // if fixedIndex != -1 capacity was ensured by AddRange
            Debug.Assert(delta == 0, "delta should be 0");
            _owner.EnsureCapacity(1);
            node._index = _owner._childCount;
        }
 
        _owner._children[node._index] = node;
        _owner._childCount++;
        node.Realize(false);
 
        if (tv is not null && node == tv._selectedNode)
        {
            tv.SelectedNode = node; // communicate this to the handle
        }
 
        if (tv is not null && tv.TreeViewNodeSorter is not null)
        {
            tv.Sort();
        }
 
        return node._index;
    }
 
    int IList.Add(object? node)
    {
        ArgumentNullException.ThrowIfNull(node);
 
        if (node is TreeNode treeNode)
        {
            return Add(treeNode);
        }
        else
        {
            return Add(node.ToString())._index;
        }
    }
 
    public bool Contains(TreeNode node) => IndexOf(node) != -1;
 
    /// <summary>
    ///  Returns true if the collection contains an item with the specified key, false otherwise.
    /// </summary>
    public virtual bool ContainsKey(string? key) => IsValidIndex(IndexOfKey(key));
 
    bool IList.Contains(object? node) => node is TreeNode treeNode && Contains(treeNode);
 
    public int IndexOf(TreeNode node)
    {
        for (int index = 0; index < Count; ++index)
        {
            if (this[index] == node)
            {
                return index;
            }
        }
 
        return -1;
    }
 
    int IList.IndexOf(object? node) =>
        node is TreeNode treeNode
            ? IndexOf(treeNode)
            : -1;
 
    /// <summary>
    ///  The zero-based index of the first occurrence of value within the entire CollectionBase, if found; otherwise, -1.
    /// </summary>
    public virtual int IndexOfKey(string? key)
    {
        // Step 0 - Arg validation
        if (string.IsNullOrEmpty(key))
        {
            return -1; // we don't support empty or null keys.
        }
 
        // step 1 - check the last cached item
        if (IsValidIndex(_lastAccessedIndex))
        {
            if (WindowsFormsUtils.SafeCompareStrings(this[_lastAccessedIndex].Name, key, ignoreCase: true))
            {
                return _lastAccessedIndex;
            }
        }
 
        // step 2 - search for the item
        for (int i = 0; i < Count; i++)
        {
            if (WindowsFormsUtils.SafeCompareStrings(this[i].Name, key, ignoreCase: true))
            {
                _lastAccessedIndex = i;
                return i;
            }
        }
 
        // step 3 - we didn't find it. Invalidate the last accessed index and return -1.
        _lastAccessedIndex = -1;
        return -1;
    }
 
    /// <summary>
    ///  Inserts a new child node on this node. Child node is positioned as specified by index.
    /// </summary>
    public virtual void Insert(int index, TreeNode node)
    {
        if (node.HTREEITEMInternal != IntPtr.Zero)
        {
            throw new ArgumentException(string.Format(SR.OnlyOneControl, node.Text), nameof(node));
        }
 
        // Check for ParentingCycle
        _owner.CheckParentingCycle(node);
 
        // If the TreeView is sorted, index is ignored
        TreeView? tv = _owner.TreeView;
 
        if (tv is not null)
        {
            foreach (TreeNode treeNode in node.GetSelfAndChildNodes())
            {
                KeyboardToolTipStateMachine.Instance.Hook(treeNode, tv.KeyboardToolTip);
            }
        }
 
        if (tv is not null && tv.Sorted)
        {
            _owner.AddSorted(tv, node);
            return;
        }
 
        if (index < 0)
        {
            index = 0;
        }
 
        if (index > _owner._childCount)
        {
            index = _owner._childCount;
        }
 
        _owner.InsertNodeAt(index, node);
    }
 
    void IList.Insert(int index, object? node)
    {
        if (node is TreeNode treeNode)
        {
            Insert(index, treeNode);
        }
        else
        {
            throw new ArgumentException(SR.TreeNodeCollectionBadTreeNode, nameof(node));
        }
    }
 
    // <-- NEW INSERT OVERLOADS IN WHIDBEY
 
    /// <summary>
    ///  Inserts a new child node on this node. Child node is positioned as specified by index.
    /// </summary>
    public virtual TreeNode Insert(int index, string? text)
    {
        TreeNode tn = new(text);
        Insert(index, tn);
        return tn;
    }
 
    /// <summary>
    ///  Inserts a new child node on this node. Child node is positioned as specified by index.
    /// </summary>
    public virtual TreeNode Insert(int index, string? key, string? text)
    {
        TreeNode tn = new(text)
        {
            Name = key
        };
        Insert(index, tn);
        return tn;
    }
 
    /// <summary>
    ///  Inserts a new child node on this node. Child node is positioned as specified by index.
    /// </summary>
    public virtual TreeNode Insert(int index, string? key, string? text, int imageIndex)
    {
        TreeNode tn = new(text)
        {
            Name = key,
            ImageIndex = imageIndex
        };
        Insert(index, tn);
        return tn;
    }
 
    /// <summary>
    ///  Inserts a new child node on this node. Child node is positioned as specified by index.
    /// </summary>
    public virtual TreeNode Insert(int index, string? key, string? text, string? imageKey)
    {
        TreeNode tn = new(text)
        {
            Name = key,
            ImageKey = imageKey
        };
        Insert(index, tn);
        return tn;
    }
 
    /// <summary>
    ///  Inserts a new child node on this node. Child node is positioned as specified by index.
    /// </summary>
    public virtual TreeNode Insert(int index, string? key, string? text, int imageIndex, int selectedImageIndex)
    {
        TreeNode tn = new(text, imageIndex, selectedImageIndex)
        {
            Name = key
        };
        Insert(index, tn);
        return tn;
    }
 
    /// <summary>
    ///  Inserts a new child node on this node. Child node is positioned as specified by index.
    /// </summary>
    public virtual TreeNode Insert(int index, string? key, string? text, string? imageKey, string? selectedImageKey)
    {
        TreeNode tn = new(text)
        {
            Name = key,
            ImageKey = imageKey,
            SelectedImageKey = selectedImageKey
        };
        Insert(index, tn);
        return tn;
    }
 
    // END - NEW INSERT OVERLOADS IN WHIDBEY -->
 
    /// <summary>
    ///  Determines if the index is valid for the collection.
    /// </summary>
    private bool IsValidIndex(int index) => (index >= 0) && (index < Count);
 
    /// <summary>
    ///  Remove all nodes from the tree view.
    /// </summary>
    public virtual void Clear()
    {
        _owner.Clear();
    }
 
    public void CopyTo(Array dest, int index)
    {
        if (_owner._childCount > 0)
        {
            Array.Copy(_owner._children, 0, dest, index, _owner._childCount);
        }
    }
 
    public void Remove(TreeNode node)
    {
        node.Remove();
    }
 
    void IList.Remove(object? node)
    {
        if (node is TreeNode treeNode)
        {
            Remove(treeNode);
        }
    }
 
    public virtual void RemoveAt(int index)
    {
        this[index].Remove();
    }
 
    /// <summary>
    ///  Removes the child control with the specified key.
    /// </summary>
    public virtual void RemoveByKey(string? key)
    {
        int index = IndexOfKey(key);
        if (IsValidIndex(index))
        {
            RemoveAt(index);
        }
    }
 
    public IEnumerator GetEnumerator()
    {
        if (_owner._children is not null)
        {
            return new ArraySubsetEnumerator(_owner._children, _owner._childCount);
        }
        else
        {
            return Array.Empty<TreeNode>().GetEnumerator();
        }
    }
}