|
// 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.Windows.Forms.Layout;
namespace System.Windows.Forms;
public partial class Control
{
/// <summary>
/// Collection of controls...
/// </summary>
[ListBindable(false)]
public partial class ControlCollection : ArrangedElementCollection, IList, ICloneable
{
/// 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.
/// Note this is not Thread Safe - but WinForms has to be run in a STA anyways.
private int _lastAccessedIndex = -1;
public ControlCollection(Control owner)
{
Owner = owner.OrThrowIfNull();
}
/// <summary>
/// Returns true if the collection contains an item with the specified key, false otherwise.
/// </summary>
public virtual bool ContainsKey(string? key)
{
return IsValidIndex(IndexOfKey(key));
}
/// <summary>
/// Adds a child control to this control. The control becomes the last control in
/// the child control list. If the control is already a child of another control it
/// is first removed from that control.
/// </summary>
public virtual void Add(Control? value)
{
if (value is null)
{
return;
}
if (value.GetTopLevel())
{
throw new ArgumentException(SR.TopLevelControlAdd);
}
// Verify that the control being added is on the same thread as
// us...or our parent chain.
if (Owner.CreateThreadId != value.CreateThreadId)
{
throw new ArgumentException(SR.AddDifferentThreads);
}
CheckParentingCycle(Owner, value);
if (value._parent == Owner)
{
value.SendToBack();
return;
}
// Remove the new control from its old parent (if any)
value._parent?.Controls.Remove(value);
// Add the control
InnerList.Add(value);
if (value._tabIndex == -1)
{
// Find the next highest tab index
int nextTabIndex = 0;
for (int c = 0; c < (Count - 1); c++)
{
int t = this[c].TabIndex;
if (nextTabIndex <= t)
{
nextTabIndex = t + 1;
}
}
value._tabIndex = nextTabIndex;
}
// If we don't suspend layout, AssignParent will indirectly trigger a layout event
// before we're ready (AssignParent will fire a PropertyChangedEvent("Visible"), which calls PerformLayout)
using (SuspendLayoutScope scope = new(Owner, performLayout: false))
{
Control? oldParent = value._parent;
try
{
// AssignParent calls into user code - this could throw, which
// would make us short-circuit the rest of the reparenting logic.
// you could end up with a control half reparented.
value.AssignParent(Owner);
}
finally
{
if (oldParent != value._parent && (Owner._state & States.Created) != 0)
{
value.SetParentHandle(Owner.InternalHandle);
if (value.Visible)
{
value.CreateControl();
}
}
}
}
value.InitLayout();
// Not putting in the finally block, as it would eat the original
// exception thrown from AssignParent if the following throws an exception.
LayoutTransaction.DoLayout(Owner, value, PropertyNames.Parent);
Owner.OnControlAdded(new ControlEventArgs(value));
}
int IList.Add(object? control)
{
if (control is Control c)
{
Add(c);
return IndexOf(c);
}
else
{
throw new ArgumentException(SR.ControlBadControl, nameof(control));
}
}
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public virtual void AddRange(params Control[] controls)
{
ArgumentNullException.ThrowIfNull(controls);
if (controls.Length == 0)
{
return;
}
using SuspendLayoutScope scope = new(Owner, performLayout: true);
for (int i = 0; i < controls.Length; ++i)
{
Add(controls[i]);
}
}
object ICloneable.Clone()
{
// Use CreateControlInstance so we get the same type of ControlCollection, but whack the
// owner so adding controls to this new collection does not affect the control we cloned from.
ControlCollection ccOther = Owner.CreateControlsInstance();
// We add using InnerList to prevent unnecessary parent cycle checks, etc.
ccOther.InnerList.AddRange(InnerList);
return ccOther;
}
public bool Contains(Control? control) => ((IList)InnerList).Contains(control);
/// <summary>
/// Searches for Controls by their Name property, builds up an array
/// of all the controls that match.
/// </summary>
public Control[] Find(string key, bool searchAllChildren)
{
key.ThrowIfNullOrEmptyWithMessage(SR.FindKeyMayNotBeEmptyOrNull);
List<Control> foundControls = [];
FindInternal(key, searchAllChildren, this, foundControls);
return [.. foundControls];
}
/// <summary>
/// Searches for Controls by their Name property, builds up a list
/// of all the controls that match.
/// </summary>
private static void FindInternal(string key, bool searchAllChildren, ControlCollection controlsToLookIn, List<Control> foundControls)
{
try
{
// Perform breadth first search - as it's likely people will want controls belonging
// to the same parent close to each other.
for (int i = 0; i < controlsToLookIn.Count; i++)
{
if (controlsToLookIn[i] is null)
{
continue;
}
if (WindowsFormsUtils.SafeCompareStrings(controlsToLookIn[i].Name, key, ignoreCase: true))
{
foundControls.Add(controlsToLookIn[i]);
}
}
// Optional recursive search for controls in child collections.
if (searchAllChildren)
{
for (int i = 0; i < controlsToLookIn.Count; i++)
{
if (controlsToLookIn[i] is null)
{
continue;
}
if (controlsToLookIn[i].Controls.Count > 0)
{
// If it has a valid child collection, append those results to our collection.
FindInternal(key, true, controlsToLookIn[i].Controls, foundControls);
}
}
}
}
catch (Exception e) when (!e.IsCriticalException())
{
}
}
public override IEnumerator GetEnumerator()
{
return new ControlCollectionEnumerator(this);
}
public int IndexOf(Control? control) => ((IList)InnerList).IndexOf(control);
/// <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>
/// Determines if the index is valid for the collection.
/// </summary>
private bool IsValidIndex(int index)
{
return ((index >= 0) && (index < Count));
}
/// <summary>
/// Who owns this control collection.
/// </summary>
public Control Owner { get; }
/// <summary>
/// Removes control from this control. Inheriting controls should call
/// base.remove to ensure that the control is removed.
/// </summary>
public virtual void Remove(Control? value)
{
// Sanity check parameter
if (value is null)
{
return; // Don't do anything
}
if (value.ParentInternal == Owner)
{
value.SetParentHandle(default);
// Remove the control from the internal control array
InnerList.Remove(value);
value.AssignParent(null);
LayoutTransaction.DoLayout(Owner, value, PropertyNames.Parent);
Owner.OnControlRemoved(new ControlEventArgs(value));
// ContainerControl needs to see it needs to find a new ActiveControl.
if (Owner.GetContainerControl() is ContainerControl cc)
{
cc.AfterControlRemoved(value, Owner);
}
}
}
void IList.Remove(object? control)
{
if (control is Control c)
{
Remove(c);
}
}
public void RemoveAt(int index)
{
Remove(this[index]);
}
/// <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);
}
}
/// <summary>
/// Retrieves the child control with the specified index.
/// </summary>
public new virtual Control this[int index]
{
get
{
// do some bounds checking here...
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Count);
Control control = (Control)InnerList[index]!;
Debug.Assert(control is not null, "Why are we returning null controls from a valid index?");
return control;
}
}
/// <summary>
/// Retrieves the child control with the specified key.
/// </summary>
public virtual Control? 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;
}
}
}
public virtual void Clear()
{
using SuspendLayoutScope scope = new(Owner);
// Clear all preferred size caches in the tree - inherited fonts could go away, etc.
CommonProperties.xClearAllPreferredSizeCaches(Owner);
while (Count != 0)
{
RemoveAt(Count - 1);
}
}
/// <summary>
/// Retrieves the index of the specified child control in this array. An ArgumentException
/// is thrown if child is not parented to this Control.
/// </summary>
public int GetChildIndex(Control child) => GetChildIndex(child, true);
/// <summary>
/// Retrieves the index of the specified child control in this array. An ArgumentException
/// is thrown if child is not parented to this Control.
/// </summary>
public virtual int GetChildIndex(Control child, bool throwException)
{
int index = IndexOf(child);
if (index == -1 && throwException)
{
throw new ArgumentException(SR.ControlNotChild);
}
return index;
}
/// <summary>
/// This is internal virtual method so that "Readonly Collections" can override this and throw as they
/// should not allow changing the child control indices.
/// </summary>
internal virtual void SetChildIndexInternal(Control child, int newIndex)
{
// Sanity check parameters
ArgumentNullException.ThrowIfNull(child);
int currentIndex = GetChildIndex(child);
if (currentIndex == newIndex)
{
return;
}
if (newIndex >= Count || newIndex == -1)
{
newIndex = Count - 1;
}
MoveElement(child, currentIndex, newIndex);
child.UpdateZOrder();
LayoutTransaction.DoLayout(Owner, child, PropertyNames.ChildIndex);
}
/// <summary>
/// Sets the index of the specified child control in this array. An ArgumentException
/// is thrown if child is not parented to this Control.
/// </summary>
public virtual void SetChildIndex(Control child, int newIndex) =>
SetChildIndexInternal(child, newIndex);
}
}
|