File: System\Windows\Forms\Controls\PropertyGrid\PropertyGridInternal\GridEntry.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.ComponentModel.Design;
using System.Drawing;
using System.Drawing.Design;
using System.Drawing.Drawing2D;
using System.Windows.Forms.Design;
using System.Windows.Forms.VisualStyles;
 
namespace System.Windows.Forms.PropertyGridInternal;
 
/// <summary>
///  Base entry for properties to be displayed in the <see cref="PropertyGridView"/>.
/// </summary>
internal abstract partial class GridEntry : GridItem, ITypeDescriptorContext
{
    protected static Point InvalidPoint { get; } = new(int.MinValue, int.MinValue);
 
    private static readonly BooleanSwitch s_pbrsAssertPropsSwitch
        = new("PbrsAssertProps", "PropertyBrowser : Assert on broken properties");
 
    internal static AttributeTypeSorter AttributeTypeSorter { get; } = new();
 
    protected static IComparer DisplayNameComparer { get; } = new DisplayNameSortComparer();
 
    private static char s_passwordReplaceChar;
 
    // Maximum number of characters we'll show in the property grid.
    // Too many characters leads to bad performance.
    private const int MaximumLengthOfPropertyString = 1000;
 
    private EventEntry? _eventList;
    private CacheItems? _cacheItems;
 
    protected TypeConverter? _typeConverter;
 
    protected UITypeEditor? Editor { get; set; }
 
    private GridEntry? _parent;
    private GridEntryCollection? _children;
    private int _propertyDepth;
    private bool _hasFocus;
    private Rectangle _outlineRect = Rectangle.Empty;
 
    private Flags _flags;
    protected PropertySort _propertySort;
 
    private Point _labelTipPoint = InvalidPoint;
    private Point _valueTipPoint = InvalidPoint;
 
    private static readonly object s_valueClickEvent = new();
    private static readonly object s_labelClickEvent = new();
    private static readonly object s_outlineClickEvent = new();
    private static readonly object s_valueDoubleClickEvent = new();
    private static readonly object s_labelDoubleClickEvent = new();
    private static readonly object s_outlineDoubleClickEvent = new();
    private static readonly object s_recreateChildrenEvent = new();
 
    private GridEntryAccessibleObject? _accessibleObject;
 
    private bool _lastPaintWithExplorerStyle;
 
    private readonly Lock _lock = new();
 
    private static Color InvertColor(Color color)
        => Color.FromArgb(color.A, (byte)~color.R, (byte)~color.G, (byte)~color.B);
 
    protected GridEntry(PropertyGrid ownerGrid, GridEntry? parent)
    {
        _parent = parent;
        OwnerGrid = ownerGrid;
 
        Debug.Assert(OwnerGrid is not null, "GridEntry w/o PropertyGrid owner, text rendering will fail.");
 
        if (parent is not null)
        {
            _propertyDepth = parent.PropertyDepth + 1;
            _propertySort = parent._propertySort;
 
            if (parent.ForceReadOnly)
            {
                SetForceReadOnlyFlag();
            }
        }
        else
        {
            _propertyDepth = -1;
        }
    }
 
    /// <summary>
    ///  Outline Icon padding.
    /// </summary>
    protected int OutlineIconPadding
    {
        get
        {
            const int LogicalOutlineIconPadding = 5;
            return OwnerGridView?.LogicalToDeviceUnits(LogicalOutlineIconPadding) ?? LogicalOutlineIconPadding;
        }
    }
 
    private bool ColorInversionNeededInHighContrast
        => SystemInformation.HighContrast && !OwnerGrid.HasCustomLineColor;
 
    /// <summary>
    ///  Gets the <see cref="AccessibleObject"/> for this instance.
    /// </summary>
    public AccessibleObject AccessibilityObject => _accessibleObject ??= GetAccessibilityObject();
 
    /// <summary>
    ///  Creates a new <see cref="AccessibleObject"/> for this instance.
    /// </summary>
    protected virtual GridEntryAccessibleObject GetAccessibilityObject() => new(this);
 
    /// <summary>
    ///  Specify that this grid entry should be allowed to be merged for multi-select.
    /// </summary>
    public virtual bool AllowMerge => true;
 
    protected virtual AttributeCollection Attributes => TypeDescriptor.GetAttributes(PropertyType!);
 
    /// <summary>
    ///  Gets the value of the background brush to use. Override this member to cause the entry to paint it's
    ///  background in a different color. The base implementation returns null.
    /// </summary>
    protected virtual Color BackgroundColor => OwnerGridView?.BackColor ?? default;
 
    protected virtual Color LabelTextColor
    {
        get
        {
            if (OwnerGridView is null)
            {
                return default;
            }
 
            return ShouldRenderReadOnly
                ? OwnerGridView.GrayTextColor
                : OwnerGridView.TextColor;
        }
    }
 
    /// <summary>
    ///  The set of attributes that will be used for browse filtering.
    /// </summary>
    public virtual AttributeCollection? BrowsableAttributes
    {
        get => _parent?.BrowsableAttributes;
        set
        {
            if (_parent is not null)
            {
                _parent.BrowsableAttributes = value;
            }
        }
    }
 
    /// <summary>
    ///  Retrieves the component that is invoking the method on the formatter object. This may
    ///  return null if there is no component responsible for the call.
    /// </summary>
    public virtual IComponent? Component
        => GetValueOwner() is IComponent component ? component : _parent?.Component;
 
    protected virtual IComponentChangeService? ComponentChangeService => _parent?.ComponentChangeService;
 
    /// <summary>
    ///  Retrieves the container that contains the set of objects this formatter may work
    ///  with. It may return null if there is no container, or of the formatter should not
    ///  use any outside objects.
    /// </summary>
    public virtual IContainer? Container => Component?.Site?.Container;
 
    [AllowNull]
    protected GridEntryCollection ChildCollection
    {
        get => _children ??= [];
        set
        {
            Debug.Assert(value is null || !Disposed, "Why are we putting new children in after we are disposed?");
            if (_children != value)
            {
                if (_children is not null)
                {
                    _children.Dispose();
                    _children = null;
                }
 
                _children = value;
            }
        }
    }
 
    public int ChildCount => Children.Count;
 
    public virtual GridEntryCollection Children
    {
        get
        {
            if (_children is null && !Disposed)
            {
                CreateChildren();
            }
 
            return _children ??= [];
        }
    }
 
    /// <summary>
    ///  The <see cref="PropertyTab"/> that the <see cref="GridEntry"/> belongs to.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   The root grid entry <see cref="SingleSelectRootGridEntry"/> maintains this value.
    ///  </para>
    /// </remarks>
    public virtual PropertyTab? OwnerTab => _parent?.OwnerTab;
 
    /// <summary>
    ///  Returns the default child <see cref="GridEntry"/> of this item.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   The root grid entry <see cref="SingleSelectRootGridEntry"/> maintains this value.
    ///  </para>
    /// </remarks>
    internal virtual GridEntry? DefaultChild
    {
        get => null;
        set { }
    }
 
    /// <summary>
    ///  The currently active <see cref="IDesignerHost"/>, if any.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   The root grid entry <see cref="SingleSelectRootGridEntry"/> maintains this value. The owning
    ///   <see cref="PropertyGrid"/> will update this when <see cref="PropertyGrid.ActiveDesigner"/> is set.
    ///  </para>
    /// </remarks>
    internal virtual IDesignerHost? DesignerHost
    {
        get => _parent?.DesignerHost;
        set
        {
            if (_parent is not null)
            {
                _parent.DesignerHost = value;
            }
        }
    }
 
    internal bool Disposed => GetFlagSet(Flags.Disposed);
 
    /// <summary>
    ///  Returns true if there is a standard set of values that can be selected from.
    ///  <see cref="GetPropertyValueList"/> should return said values when this is true.
    /// </summary>
    internal virtual bool Enumerable => EntryFlags.HasFlag(Flags.StandardValuesSupported);
 
    public override bool Expandable
    {
        get
        {
            bool expandable = GetFlagSet(Flags.Expandable);
 
            if (expandable && _children is not null && _children.Count > 0)
            {
                return true;
            }
 
            if (GetFlagSet(Flags.ExpandableFailed))
            {
                return false;
            }
 
            if (expandable && _cacheItems?.LastValue is null && PropertyValue is null)
            {
                expandable = false;
            }
 
            return expandable;
        }
    }
 
    public override bool Expanded
    {
        get => InternalExpanded;
        set => OwnerGridView?.SetExpand(this, value);
    }
 
    internal virtual bool ForceReadOnly => (_flags & Flags.ForceReadOnly) != 0;
 
    protected void SetForceReadOnlyFlag() => _flags |= Flags.ForceReadOnly;
 
    internal virtual bool InternalExpanded
    {
        get
        {
            // Short circuit if we don't have children.
            if (_children is null || _children.Count == 0)
            {
                return false;
            }
 
            return GetFlagSet(Flags.Expand);
        }
        set
        {
            if (!Expandable || value == InternalExpanded)
            {
                return;
            }
 
            if (_children is not null && _children.Count > 0)
            {
                SetFlag(Flags.Expand, value);
            }
            else
            {
                SetFlag(Flags.Expand, false);
                if (value)
                {
                    bool childrenExpandable = CreateChildren();
                    SetFlag(Flags.Expand, childrenExpandable);
                }
            }
 
            // Notify accessibility clients of expanded state change. StateChange requires NameChange as well.
            // Accessible clients won't see this unless both events are raised.
 
            // Root item is hidden and should not raise events
            if (OwnerGridView is { } ownerGridView
                && ownerGridView.IsAccessibilityObjectCreated
                && GridItemType != GridItemType.Root)
            {
                int id = OwnerGridView.AccessibilityGetGridEntryChildID(this);
                if (id >= 0)
                {
                    var accessibleObject = (PropertyGridView.PropertyGridViewAccessibleObject)OwnerGridView.AccessibilityObject;
                    accessibleObject.NotifyClients(AccessibleEvents.StateChange, id);
                    accessibleObject.NotifyClients(AccessibleEvents.NameChange, id);
                }
            }
        }
    }
 
    public Flags EntryFlags
    {
        get
        {
            if (_flags.HasFlag(Flags.Checked))
            {
                return _flags;
            }
 
            _flags |= Flags.Checked;
 
            TypeConverter converter = TypeConverter;
            UITypeEditor? editor = UITypeEditor;
            object? value = Instance;
            bool forceReadOnly = ForceReadOnly;
 
            if (value is not null)
            {
                forceReadOnly |= TypeDescriptor.GetAttributes(value).Contains(InheritanceAttribute.InheritedReadOnly);
            }
 
            if (converter.GetStandardValuesSupported(this))
            {
                _flags |= Flags.StandardValuesSupported;
            }
 
            if (!forceReadOnly && converter.CanConvertFrom(this, typeof(string)) &&
                !converter.GetStandardValuesExclusive(this))
            {
                _flags |= Flags.TextEditable;
            }
 
            bool hasImmutableAttribute = TypeDescriptor.GetAttributes(PropertyType!)[typeof(ImmutableObjectAttribute)]!
                .Equals(ImmutableObjectAttribute.Yes);
            bool isImmutable = hasImmutableAttribute || converter.GetCreateInstanceSupported(this);
 
            if (isImmutable)
            {
                _flags |= Flags.Immutable;
            }
 
            if (converter.GetPropertiesSupported(this))
            {
                _flags |= Flags.Expandable;
 
                // If we're expandable, but we don't support editing,
                // make us read only editable so we don't paint grey.
 
                if (!forceReadOnly && !_flags.HasFlag(Flags.TextEditable) && !hasImmutableAttribute)
                {
                    _flags |= Flags.ReadOnlyEditable;
                }
            }
 
            if (Attributes.Contains(PasswordPropertyTextAttribute.Yes))
            {
                _flags |= Flags.RenderPassword;
            }
 
            if (editor is null)
            {
                return _flags;
            }
 
            if (editor.GetPaintValueSupported(this))
            {
                _flags |= Flags.CustomPaint;
            }
 
            // We only allow drop-downs if the object is NOT being inherited.
 
            bool allowButtons = !forceReadOnly;
 
            if (allowButtons)
            {
                switch (editor.GetEditStyle(this))
                {
                    case UITypeEditorEditStyle.Modal:
                        _flags |= Flags.ModalEditable;
                        if (!isImmutable && !(PropertyType?.IsValueType ?? false))
                        {
                            _flags |= Flags.ReadOnlyEditable;
                        }
 
                        break;
                    case UITypeEditorEditStyle.DropDown:
                        _flags |= Flags.DropDownEditable;
                        break;
                }
            }
 
            return _flags;
        }
    }
 
    protected void ClearFlags() => _flags = 0;
 
    /// <summary>
    ///  Checks if the entry is currently expanded.
    /// </summary>
    public bool HasFocus
    {
        get => _hasFocus;
        set
        {
            if (Disposed)
            {
                return;
            }
 
            if (_cacheItems is not null)
            {
                _cacheItems.LastValueString = null;
                _cacheItems.UseValueString = false;
                _cacheItems.UseShouldSerialize = false;
            }
 
            if (_hasFocus != value)
            {
                _hasFocus = value;
 
                // Notify accessibility applications that keyboard focus has changed.
                if (OwnerGridView is { } ownerGridView
                    && ownerGridView.IsAccessibilityObjectCreated
                    && value)
                {
                    int id = OwnerGridView.AccessibilityGetGridEntryChildID(this);
                    if (id >= 0)
                    {
                        var gridAccObj = (PropertyGridView.PropertyGridViewAccessibleObject)OwnerGridView.AccessibilityObject;
                        gridAccObj.NotifyClients(AccessibleEvents.Focus, id);
                        gridAccObj.NotifyClients(AccessibleEvents.Selection, id);
 
                        AccessibilityObject.SetFocus();
                    }
                }
            }
        }
    }
 
    /// <summary>
    ///  Returns the label including the object name, and properties. For example, the value
    ///  of the Font size property on a Button called Button1 would be "Button1.Font.Size"
    /// </summary>
    public string? FullLabel
    {
        get
        {
            string? label = _parent?.FullLabel;
 
            if (label is not null)
            {
                label = $"{label}.{PropertyLabel}";
            }
            else
            {
                label = PropertyLabel;
            }
 
            return label;
        }
    }
 
    public override GridItemCollection GridItems
    {
        get
        {
            ObjectDisposedException.ThrowIf(Disposed, typeof(GridItem));
            if (IsExpandable && _children is not null && _children.Count == 0)
            {
                CreateChildren();
            }
 
            return new GridItemCollection(Children);
        }
    }
 
    /// <summary>
    ///  The <see cref="PropertyGridView"/> that this <see cref="GridEntry"/> belongs to.
    /// </summary>
    [DisallowNull]
    internal virtual PropertyGridView? OwnerGridView
    {
        get => _parent?.OwnerGridView;
        set => throw new NotSupportedException();
    }
 
    public override GridItemType GridItemType => GridItemType.Property;
 
    /// <summary>
    ///  Returns true if this GridEntry has a value field in the right hand column.
    /// </summary>
    internal virtual bool HasValue => true;
 
    /// <summary>
    ///  Retrieves the keyword that Visual Studio dynamic help window will use when this entry is selected.
    /// </summary>
    public virtual string? HelpKeyword => _parent?.HelpKeyword ?? string.Empty;
 
    /// <summary>
    ///  Returns true when the entry has an <see cref="UITypeEditor"/> that custom paints a value.
    /// </summary>
    public bool IsCustomPaint
    {
        get
        {
            // Prevent full flag population if possible by not hitting EntryFlags if flags have not been checked yet.
            if (!_flags.HasFlag(Flags.Checked))
            {
                UITypeEditor? editor = UITypeEditor;
                if (editor is not null)
                {
                    if (_flags.HasFlag(Flags.CustomPaint) || _flags.HasFlag(Flags.NoCustomPaint))
                    {
                        return _flags.HasFlag(Flags.CustomPaint);
                    }
 
                    if (editor.GetPaintValueSupported(this))
                    {
                        _flags |= Flags.CustomPaint;
                        return true;
                    }
                    else
                    {
                        _flags |= Flags.NoCustomPaint;
                        return false;
                    }
                }
            }
 
            return EntryFlags.HasFlag(Flags.CustomPaint);
        }
    }
 
    public bool IsExpandable
    {
        get => Expandable;
        set
        {
            if (value != GetFlagSet(Flags.Expandable))
            {
                SetFlag(Flags.ExpandableFailed, false);
                SetFlag(Flags.Expandable, value);
            }
        }
    }
 
    public bool IsTextEditable => IsValueEditable && EntryFlags.HasFlag(Flags.TextEditable);
 
    public virtual bool IsValueEditable
        => !ForceReadOnly
        && GetFlagSet(Flags.DropDownEditable | Flags.TextEditable | Flags.ModalEditable | Flags.StandardValuesSupported);
 
    /// <summary>
    ///  Retrieves the component that is invoking the method on the formatter object. This may
    ///  return null if there is no component responsible for the call.
    /// </summary>
    public object? Instance => GetValueOwner() ?? _parent?.Instance;
 
    public override string? Label => PropertyLabel;
 
    public override PropertyDescriptor? PropertyDescriptor => null;
 
    /// <summary>
    ///  Returns the pixel indent of the current GridEntry's label.
    /// </summary>
    internal virtual int PropertyLabelIndent
    {
        get
        {
            int borderWidth = (OwnerGridView?.OutlineIconSize ?? 0) + OutlineIconPadding;
            return ((_propertyDepth + 1) * borderWidth) + 1;
        }
    }
 
    internal virtual Point GetLabelToolTipLocation(int mouseX, int mouseY) => _labelTipPoint;
 
    internal virtual string? LabelToolTipText => PropertyLabel;
 
    /// <summary>
    ///  The entry needs a drop down button to invoke its editor.
    /// </summary>
    public virtual bool NeedsDropDownButton => EntryFlags.HasFlag(Flags.DropDownEditable);
 
    /// <summary>
    ///  The entry needs a modal editor button ("...") to invoke its editor.
    /// </summary>
    public bool NeedsModalEditorButton
        => EntryFlags.HasFlag(Flags.ModalEditable) && (IsValueEditable || EntryFlags.HasFlag(Flags.ReadOnlyEditable));
 
    public PropertyGrid OwnerGrid { get; }
 
    /// <summary>
    ///  Returns the rectangle that the outline icon (+ or - or arrow) will be drawn into, relative
    ///  to the upper left corner of the <see cref="GridEntry"/>.
    /// </summary>
    public Rectangle OutlineRectangle
    {
        get
        {
            if (!_outlineRect.IsEmpty || OwnerGridView is null)
            {
                return _outlineRect;
            }
 
            int outlineSize = OwnerGridView.OutlineIconSize;
            int borderWidth = outlineSize + OutlineIconPadding;
            _outlineRect = new Rectangle(
                (_propertyDepth * borderWidth) + OutlineIconPadding / 2,
                (OwnerGridView.GridEntryHeight - outlineSize) / 2,
                outlineSize,
                outlineSize);
 
            return _outlineRect;
        }
    }
 
    /// <summary>
    ///  Recursively resets outline rectangles for this <see cref="GridEntry"/> and it's children.
    /// </summary>
    public void ResetOutlineRectangle()
    {
        _outlineRect = Rectangle.Empty;
        if (ChildCount > 0)
        {
            foreach (GridEntry child in Children)
            {
                child.ResetOutlineRectangle();
            }
        }
    }
 
    public GridEntry? ParentGridEntry
    {
        get => _parent;
        set
        {
            Debug.Assert(value != this, "how can we be our own parent?");
            _parent = value;
            if (value is not null)
            {
                _propertyDepth = value.PropertyDepth + 1;
 
                if (_children is not null)
                {
                    for (int i = 0; i < _children.Count; i++)
                    {
                        _children[i].ParentGridEntry = this;
                    }
                }
            }
        }
    }
 
    public override GridItem? Parent
    {
        get
        {
            ObjectDisposedException.ThrowIf(Disposed, typeof(GridItem));
            return ParentGridEntry;
        }
    }
 
    /// <summary>
    ///  Returns the category name of the current property.
    /// </summary>
    public virtual string PropertyCategory => CategoryAttribute.Default.Category;
 
    /// <summary>
    ///  Returns "depth" of this property. That is, how many parents between
    ///  this property and the root property. The root property has a depth of -1.
    /// </summary>
    public virtual int PropertyDepth => _propertyDepth;
 
    /// <summary>
    ///  Returns the description helpstring for this GridEntry.
    /// </summary>
    public virtual string? PropertyDescription => null;
 
    /// <summary>
    ///  Returns the label of this property. Usually this is the property name.
    /// </summary>
    public virtual string? PropertyLabel => null;
 
    /// <summary>
    ///  Returns non-localized name of this property.
    /// </summary>
    public virtual string? PropertyName => PropertyLabel;
 
    /// <summary>
    ///  Returns the Type of the value of this <see cref="GridEntry"/>, or null if the value is null.
    /// </summary>
    public virtual Type? PropertyType => PropertyValue?.GetType();
 
    /// <summary>
    ///  Gets or sets the value for the property that is represented by this <see cref="GridEntry"/>.
    /// </summary>
    public virtual object? PropertyValue
    {
        get => _cacheItems?.LastValue;
        set { }
    }
 
    public bool ShouldRenderPassword => EntryFlags.HasFlag(Flags.RenderPassword);
 
    public virtual bool ShouldRenderReadOnly
         => ForceReadOnly
            || EntryFlags.HasFlag(Flags.RenderReadOnly)
            || (!IsValueEditable && !EntryFlags.HasFlag(Flags.ReadOnlyEditable));
 
    /// <summary>
    ///  Returns the type converter for this entry.
    /// </summary>
    internal virtual TypeConverter TypeConverter
        => _typeConverter ??= TypeDescriptor.GetConverter((PropertyValue ?? PropertyType)!);
 
    /// <summary>
    ///  Returns the type editor for this entry. This may return null if there is no type editor.
    /// </summary>
    internal virtual UITypeEditor? UITypeEditor
    {
        get
        {
            if (Editor is null && PropertyType is not null)
            {
                Editor = (UITypeEditor?)TypeDescriptor.GetEditor(PropertyType, typeof(UITypeEditor));
            }
 
            return Editor;
        }
    }
 
    // Note: we don't do set because of the value class semantics, etc.
 
    public sealed override object? Value => PropertyValue;
 
    internal Point ValueToolTipLocation
    {
        get => ShouldRenderPassword ? InvalidPoint : _valueTipPoint;
        set => _valueTipPoint = value;
    }
 
    internal int VisibleChildCount
    {
        get
        {
            if (!Expanded)
            {
                return 0;
            }
 
            int count = ChildCount;
            int totalCount = count;
            for (int i = 0; i < count; i++)
            {
                totalCount += ChildCollection[i].VisibleChildCount;
            }
 
            return totalCount;
        }
    }
 
    /// <summary>
    ///  Add an event handler to be invoked when the label portion of the property entry is clicked.
    /// </summary>
    public void AddOnLabelClick(EventHandler h) => AddEventHandler(s_labelClickEvent, h);
 
    /// <summary>
    ///  Add an event handler to be invoked when the label portion of the property entry is double-clicked.
    /// </summary>
    public void AddOnLabelDoubleClick(EventHandler h) => AddEventHandler(s_labelDoubleClickEvent, h);
 
    /// <summary>
    ///  Add an event handler to be invoked when the value portion of the property entry is clicked.
    /// </summary>
    public void AddOnValueClick(EventHandler h) => AddEventHandler(s_valueClickEvent, h);
 
    /// <summary>
    ///  Add an event handler to be invoked when the value portion of the prop entry is double-clicked.
    /// </summary>
    public void AddOnValueDoubleClick(EventHandler h) => AddEventHandler(s_valueDoubleClickEvent, h);
 
    /// <summary>
    ///  Add an event handler to be invoked when the outline icon portion of the prop entry is clicked
    /// </summary>
    public void AddOnOutlineClick(EventHandler h) => AddEventHandler(s_outlineClickEvent, h);
 
    /// <summary>
    ///  Add an event handler to be invoked when the outline icon portion of the prop entry is double clicked.
    /// </summary>
    public void AddOnOutlineDoubleClick(EventHandler h) => AddEventHandler(s_outlineDoubleClickEvent, h);
 
    /// <summary>
    ///  Add an event handler to be invoked when the children grid entries are re-created.
    /// </summary>
    public void AddOnRecreateChildren(GridEntryRecreateChildrenEventHandler h) => AddEventHandler(s_recreateChildrenEvent, h);
 
    internal void ClearCachedValues(bool clearChildren = true)
    {
        if (_cacheItems is not null)
        {
            _cacheItems.UseValueString = false;
            _cacheItems.LastValue = null;
            _cacheItems.UseShouldSerialize = false;
        }
 
        if (clearChildren)
        {
            for (int i = 0; i < ChildCollection.Count; i++)
            {
                ChildCollection[i].ClearCachedValues();
            }
        }
    }
 
    /// <summary>
    ///  Converts the given string of text to a value.
    /// </summary>
    public object? ConvertTextToValue(string? text)
    {
        if (TypeConverter.CanConvertFrom(this, typeof(string)))
        {
            // We will return an empty string when text is null.
            return TypeConverter.ConvertFromString(this, text!);
        }
 
        return text;
    }
 
    /// <summary>
    ///  Create the root grid entry given an object or set of objects.
    /// </summary>
    /// <param name="objects">The objects to build the root entry on.</param>
    internal static GridEntry? CreateRootGridEntry(
        PropertyGridView view,
        object[] objects,
        IServiceProvider baseProvider,
        IDesignerHost? currentHost,
        PropertyTab tab,
        PropertySort initialSortType)
    {
        if (objects is null || objects.Length == 0)
        {
            return null;
        }
 
        return objects.Length == 1
            ? new SingleSelectRootGridEntry(
                view,
                objects[0],
                baseProvider,
                currentHost,
                tab,
                initialSortType)
            : new MultiSelectRootGridEntry(
                view,
                objects,
                baseProvider,
                currentHost,
                tab,
                initialSortType);
    }
 
    /// <summary>
    ///  Populates the children of this grid entry.
    /// </summary>
    /// <param name="useExistingChildren">
    ///  When set to true, will check existing children to see if they need to be recreated. If they
    ///  haven't changed, the existing children will be used.
    /// </param>
    /// <returns>True if the children are expandable.</returns>
    protected virtual bool CreateChildren(bool useExistingChildren = false)
    {
        Debug.Assert(!Disposed, "Why are we creating children after we are disposed?");
 
        if (!GetFlagSet(Flags.Expandable))
        {
            if (_children is not null)
            {
                _children.Clear();
            }
            else
            {
                _children = [];
            }
 
            return false;
        }
 
        if (!useExistingChildren && _children is not null && _children.Count > 0)
        {
            return true;
        }
 
        GridEntry[]? childProperties = GetChildEntries();
 
        bool expandable = childProperties is not null && childProperties.Length > 0;
 
        if (useExistingChildren && _children is not null && _children.Count > 0)
        {
            bool same = true;
            if (childProperties is not null && childProperties.Length == _children.Count)
            {
                for (int i = 0; i < childProperties.Length; i++)
                {
                    if (!childProperties[i].EqualsIgnoreParent(_children[i]))
                    {
                        same = false;
                        break;
                    }
                }
            }
            else
            {
                same = false;
            }
 
            if (same)
            {
                return true;
            }
        }
 
        if (!expandable)
        {
            SetFlag(Flags.ExpandableFailed, true);
            if (_children is not null)
            {
                _children.Clear();
            }
            else
            {
                _children = [];
            }
 
            if (InternalExpanded)
            {
                InternalExpanded = false;
            }
        }
        else
        {
            if (_children is not null && childProperties is not null)
            {
                _children.Clear();
                _children.AddRange(childProperties);
            }
            else
            {
                _children = new GridEntryCollection(childProperties);
            }
        }
 
        return expandable;
    }
 
    public void Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }
 
    protected virtual void Dispose(bool disposing)
    {
        // Make sure we don't accidentally check flags while disposing.
        _flags |= Flags.Checked;
 
        SetFlag(Flags.Disposed, true);
 
        _cacheItems = null;
        _typeConverter = null;
        Editor = null;
        ReleaseUiaProvider();
 
        if (disposing)
        {
            DisposeChildren();
        }
    }
 
    internal void ReleaseUiaProvider()
    {
        if (_children?.Count > 0)
        {
            foreach (GridEntry gridEntry in _children)
            {
                gridEntry.ReleaseUiaProvider();
            }
        }
 
        PInvoke.UiaDisconnectProvider(_accessibleObject);
 
        _accessibleObject = null;
    }
 
    public virtual void DisposeChildren()
    {
        _children?.Dispose();
        _children = null;
    }
 
    ~GridEntry() => Dispose(disposing: false);
 
    /// <summary>
    ///  Invokes the type editor for this item.
    /// </summary>
    internal virtual void EditPropertyValue(PropertyGridView gridView)
    {
        if (UITypeEditor is null)
        {
            return;
        }
 
        try
        {
            object? originalValue = PropertyValue;
            object? value = UITypeEditor.EditValue(this, this, originalValue);
 
            // Since edit value can push a modal loop there is a chance that this gridentry will be zombied
            // before it returns. Make sure we're not disposed.
            if (Disposed)
            {
                return;
            }
 
            // Push the new value back into the property.
            if (value != originalValue && IsValueEditable)
            {
                gridView.CommitValue(this, value);
            }
 
            if (InternalExpanded && OwnerGridView is not null)
            {
                // If the edited property is expanded to show sub-properties, then we want to
                // preserve the expanded states of it and all of its descendants. RecreateChildren()
                // has logic that is supposed to do this, but it doesn't do so correctly.
                PropertyGridView.GridPositionData positionData = OwnerGridView.CaptureGridPositionData();
                InternalExpanded = false;
                RecreateChildren();
                positionData.Restore(OwnerGridView);
            }
            else
            {
                // If edited property has no children or is collapsed, we don't need to preserve expanded states.
                RecreateChildren();
            }
        }
        catch (Exception e)
        {
            if (this.TryGetService(out IUIService? uiService))
            {
                uiService.ShowError(e);
            }
            else
            {
                RTLAwareMessageBox.Show(
                    OwnerGridView,
                    e.Message,
                    SR.PBRSErrorTitle,
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error,
                    MessageBoxDefaultButton.Button1,
                    0);
            }
        }
    }
 
    /// <summary>
    ///  Compares equality, ignoring the parent.
    /// </summary>
    internal bool EqualsIgnoreParent(GridEntry entry)
    {
        if (entry == this)
        {
            return true;
        }
 
        return entry is not null
            && entry.PropertyLabel is not null
            && entry.PropertyLabel.Equals(PropertyLabel)
            && entry.PropertyType is not null
            && entry.PropertyType.Equals(PropertyType)
            && entry.PropertyDepth == PropertyDepth;
    }
 
    public override bool Equals(object? obj)
    {
        if (obj is GridEntry entry && EqualsIgnoreParent(entry))
        {
            return entry.ParentGridEntry == ParentGridEntry;
        }
 
        return false;
    }
 
    /// <summary>
    ///  Searches for a value of a given property for a value editor user.
    /// </summary>
    private object? FindPropertyValue(string propertyName, Type propertyType)
    {
        object? owner = GetValueOwner();
        if (owner is not null)
        {
            PropertyDescriptor? property = TypeDescriptor.GetProperties(owner)[propertyName];
            if (property is not null && property.PropertyType == propertyType)
            {
                return property.GetValue(owner);
            }
        }
 
        return _parent?.FindPropertyValue(propertyName, propertyType);
    }
 
    /// <summary>
    ///  Returns the index of a child <see cref="GridEntry"/>.
    /// </summary>
    internal int GetChildIndex(GridEntry entry) => Children.IndexOf(entry);
 
    /// <summary>
    ///  Gets the components that own the current value. This is usually the value of the root entry, which is the
    ///  object being browsed. Walks up the <see cref="GridEntry"/> tree looking for an owner that is an
    ///  <see cref="IComponent"/>.
    /// </summary>
    public virtual IComponent[]? GetComponents()
    {
        IComponent? component = Component;
        if (component is not null)
        {
            return [component];
        }
 
        return null;
    }
 
    protected int GetLabelTextWidth(string? text, Graphics graphics, Font font)
    {
        if (_cacheItems is null)
        {
            _cacheItems = new CacheItems();
        }
        else if (_cacheItems.UseCompatTextRendering == OwnerGrid.UseCompatibleTextRendering
            && _cacheItems.LastLabel == text
            && font.Equals(_cacheItems.LastLabelFont))
        {
            return _cacheItems.LastLabelWidth;
        }
 
        SizeF textSize = PropertyGrid.MeasureTextHelper.MeasureText(OwnerGrid, graphics, text, font);
 
        _cacheItems.LastLabelWidth = (int)textSize.Width;
        _cacheItems.LastLabel = text;
        _cacheItems.LastLabelFont = font;
        _cacheItems.UseCompatTextRendering = OwnerGrid.UseCompatibleTextRendering;
 
        return _cacheItems.LastLabelWidth;
    }
 
    public int GetValueTextWidth(string text, Graphics graphics, Font font)
    {
        if (_cacheItems is null)
        {
            _cacheItems = new CacheItems();
        }
        else if (_cacheItems.LastValueTextWidth != -1
            && _cacheItems.LastValueString == text
            && font.Equals(_cacheItems.LastValueFont))
        {
            return _cacheItems.LastValueTextWidth;
        }
 
        // Value text is rendered using GDI directly but always measured/adjusted using GDI+,
        // so don't use MeasureTextHelper.
        _cacheItems.LastValueTextWidth = (int)graphics.MeasureString(text, font).Width;
        _cacheItems.LastValueString = text;
        _cacheItems.LastValueFont = font;
        return _cacheItems.LastValueTextWidth;
    }
 
    /// <summary>
    ///  Gets the owner of the current value. This is usually the value of the root entry,
    ///  which is the object being browsed.
    /// </summary>
    public object? GetValueOwner() => _parent is null ? PropertyValue : _parent.GetValueOwnerInternal();
 
    /// <summary>
    ///  Gets the owner of the current value. This is usually the value of the root entry,
    ///  which is the object being browsed.
    /// </summary>
    /// <devdoc>
    ///  This internal override allows <see cref="CategoryGridEntry"/> to skip to its parent <see cref="PropertyValue"/>
    ///  and <see cref="MultiPropertyDescriptorGridEntry"/> to return it's set of owners.
    /// </devdoc>
    internal virtual object? GetValueOwnerInternal() => PropertyValue;
 
    /// <summary>
    ///  Returns a string with info about the currently selected <see cref="GridEntry"/>.
    /// </summary>
    public virtual string GetTestingInfo()
        => $@"object = ({FullLabel}), property = ({PropertyLabel},{(PropertyType ?? typeof(object)).AssemblyQualifiedName})
                , value = [{(GetPropertyTextValue()?.Replace('\0', ' ') ?? "null")}], expandable = {Expandable}
                , readOnly = {ShouldRenderReadOnly}";
 
    /// <summary>
    ///  Returns the child <see cref="GridEntry"/> items for this <see cref="GridEntry"/>.
    /// </summary>
    private GridEntry[]? GetChildEntries()
    {
        object? value = PropertyValue;
        Type? objectType = PropertyType;
 
        // We don't want to create child entries for null objects.
        if (value is null)
        {
            return null;
        }
 
        GridEntry[]? entries = null;
 
        AttributeCollection? browsableAttributes = BrowsableAttributes;
        Attribute[]? attributes = null;
        if (browsableAttributes is not null)
        {
            attributes = new Attribute[browsableAttributes.Count];
            browsableAttributes.CopyTo(attributes, 0);
        }
 
        PropertyTab? ownerTab = OwnerTab;
        Debug.Assert(ownerTab is not null, "No current tab!");
 
        try
        {
            bool forceReadOnly = ForceReadOnly;
 
            if (!forceReadOnly)
            {
                forceReadOnly = TypeDescriptorHelper.TryGetAttribute(value, out ReadOnlyAttribute? readOnlyAttribute)
                    && !readOnlyAttribute.IsDefaultAttribute();
            }
 
            if (this is not IRootGridEntry && !TypeConverter.GetPropertiesSupported(this))
            {
                // We can't get properties on this sub entry.
                return null;
            }
 
            // Ask the owning tab for properties if we have one.
            PropertyDescriptorCollection? properties = null;
            PropertyDescriptor? defaultProperty = null;
            if (ownerTab is not null)
            {
                properties = ownerTab.GetProperties(this, value, attributes);
                defaultProperty = ownerTab.GetDefaultProperty(value);
            }
            else
            {
                properties = TypeConverter.GetProperties(this, value, attributes);
                defaultProperty = TypeDescriptor.GetDefaultProperty(value);
            }
 
            if (properties is null)
            {
                return null;
            }
 
            if ((_propertySort & PropertySort.Alphabetical) != 0)
            {
                if (objectType is null || !objectType.IsArray)
                {
                    properties = properties.Sort(DisplayNameComparer);
                }
 
                var propertyDescriptors = new PropertyDescriptor[properties.Count];
                properties.CopyTo(propertyDescriptors, 0);
 
                properties = new PropertyDescriptorCollection(SortParenProperties(propertyDescriptors));
            }
 
            if (defaultProperty is null && properties.Count > 0)
            {
                defaultProperty = properties[0];
            }
 
            // If the target object is an array and nothing else has provided a set of properties to use,
            // then expand the array.
 
            if ((properties is null || properties.Count == 0) && objectType is not null && objectType.IsArray && value is not null)
            {
                var objArray = (Array)value;
 
                entries = new GridEntry[objArray.Length];
 
                for (int i = 0; i < entries.Length; i++)
                {
                    entries[i] = new ArrayElementGridEntry(OwnerGrid, this, i);
                }
            }
            else
            {
                // Otherwise, create the proper GridEntries.
                bool createInstanceSupported = TypeConverter.GetCreateInstanceSupported(this);
                if (properties is null)
                {
                    return entries;
                }
 
                entries = new GridEntry[properties.Count];
                int index = 0;
 
                // Loop through all the props we got and create property descriptors.
                foreach (PropertyDescriptor property in properties)
                {
                    GridEntry newEntry;
 
                    // Make sure we've got a valid property, otherwise hide it.
                    bool hide = false;
                    try
                    {
                        object? owner = value;
                        if (value is ICustomTypeDescriptor descriptor)
                        {
                            owner = descriptor.GetPropertyOwner(property);
                        }
 
                        property.GetValue(owner);
                    }
                    catch (Exception w)
                    {
                        Debug.Assert(!s_pbrsAssertPropsSwitch.Enabled, $"Bad property '{PropertyLabel}.{property.Name}': {w}");
 
                        hide = true;
                    }
 
                    newEntry = createInstanceSupported
                        ? new ImmutablePropertyDescriptorGridEntry(OwnerGrid, this, property, hide)
                        : new PropertyDescriptorGridEntry(OwnerGrid, this, property, hide);
 
                    if (forceReadOnly)
                    {
                        newEntry._flags |= Flags.ForceReadOnly;
                    }
 
                    // Check to see if we've come across the default item.
                    if (property.Equals(defaultProperty))
                    {
                        DefaultChild = newEntry;
                    }
 
                    // Add it to the array.
                    entries[index++] = newEntry;
                }
            }
 
            return entries;
        }
        catch (Exception e)
        {
            Debug.Fail($"Failed to get properties: {e.GetType().Name},{e.Message}\n{e.StackTrace}");
        }
 
        return entries;
    }
 
    /// <summary>
    ///  Resets the current item.
    /// </summary>
    public virtual void ResetPropertyValue()
    {
        SendNotificationToParent(Notify.Reset);
        Refresh();
    }
 
    /// <summary>
    ///  Returns if the property can be reset
    /// </summary>
    public virtual bool CanResetPropertyValue() => SendNotificationToParent(Notify.CanReset);
 
    /// <summary>
    ///  Called when the item is double clicked.
    /// </summary>
    public virtual bool DoubleClickPropertyValue() => SendNotificationToParent(Notify.DoubleClick);
 
    /// <summary>
    ///  Returns the text value of this property.
    /// </summary>
    public virtual string GetPropertyTextValue() => GetPropertyTextValue(PropertyValue);
 
    /// <summary>
    ///  Returns the text value of this property.
    /// </summary>
    public virtual string GetPropertyTextValue(object? value)
    {
        string? textValue = null;
 
        TypeConverter converter = TypeConverter;
        try
        {
            textValue = converter.ConvertToString(this, value);
        }
        catch (Exception t)
        {
            Debug.Fail($"Bad Type Converter! {t.GetType().Name}, {t.Message},{converter}", t.ToString());
        }
 
        textValue ??= string.Empty;
 
        return textValue;
    }
 
    /// <summary>
    ///  Returns the standard text values of this property.
    /// </summary>
    public virtual object[] GetPropertyValueList()
    {
        if (TypeConverter.GetStandardValues(this) is { } values)
        {
            object[] valueArray = new object[values.Count];
            values.CopyTo(valueArray, 0);
            return valueArray;
        }
 
        return [];
    }
 
    public override int GetHashCode() => HashCode.Combine(PropertyLabel, PropertyType, GetType());
 
    /// <summary>
    ///  Checks if any given flags are set.
    /// </summary>
    protected bool GetFlagSet(Flags flags) => (flags & EntryFlags) != 0;
 
    protected Font GetFont(bool boldFont)
    {
        if (OwnerGridView is null)
        {
            return Control.DefaultFont;
        }
 
        return boldFont
            ? OwnerGridView.GetBoldFont()
            : OwnerGridView.GetBaseFont();
    }
 
    /// <summary>
    ///  Retrieves the requested service. This may return null if the requested service is not available.
    /// </summary>
    public virtual object? GetService(Type serviceType)
        => serviceType == typeof(GridItem) ? this : (_parent?.GetService(serviceType));
 
    /// <summary>
    ///  Paints the label portion of this <see cref="GridEntry"/> into the given <see cref="Graphics"/> object.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   This is called by the <see cref="GridEntry"/> host (the <see cref="PropertyGridView"/>) when this
    ///   <see cref="GridEntry"/> needs to be painted.
    ///  </para>
    /// </remarks>
    public virtual void PaintLabel(
        Graphics g,
        Rectangle rect,
        Rectangle clipRect,
        bool selected,
        bool paintFullLabel)
    {
        if (OwnerGridView is not PropertyGridView ownerGrid)
        {
            throw new InvalidOperationException();
        }
 
        string? label = PropertyLabel;
        int borderWidth = ownerGrid.OutlineIconSize + OutlineIconPadding;
 
        // Fill the background if necessary.
        Color backColor = selected ? ownerGrid.SelectedItemWithFocusBackColor : BackgroundColor;
 
        // If we don't have focus, paint with the line color.
        if (selected && !_hasFocus)
        {
            backColor = ownerGrid.LineColor;
        }
 
        bool bold = EntryFlags.HasFlag(Flags.LabelBold);
        Font font = GetFont(boldFont: bold);
 
        int labelWidth = GetLabelTextWidth(label, g, font);
 
        int neededWidth = paintFullLabel ? labelWidth : 0;
        int stringX = rect.X + PropertyLabelIndent;
 
        using var backBrush = backColor.GetCachedSolidBrushScope();
        if (paintFullLabel && (neededWidth >= (rect.Width - (stringX + 2))))
        {
            // GdiPlusSpace = extra needed to ensure text draws completely and isn't clipped.
            int totalWidth = stringX + neededWidth + PropertyGridView.GdiPlusSpace;
 
            // Blank out the space we're going to use.
            g.FillRectangle(backBrush, borderWidth - 1, rect.Y, totalWidth - borderWidth + 3, rect.Height);
 
            // Draw an end line.
            using var linePen = ownerGrid.LineColor.GetCachedPenScope();
            g.DrawLine(linePen, totalWidth, rect.Y, totalWidth, rect.Height);
 
            // Set the new width that we can draw into.
            rect.Width = totalWidth - rect.X;
        }
        else
        {
            // Normal case -- no pseudo-tooltip for the label.
            g.FillRectangle(backBrush, rect.X, rect.Y, rect.Width, rect.Height);
        }
 
        // Draw the border stripe on the left.
        using var stripeBrush = ownerGrid.LineColor.GetCachedSolidBrushScope();
        g.FillRectangle(stripeBrush, rect.X, rect.Y, borderWidth, rect.Height);
 
        if (selected && _hasFocus)
        {
            using var focusBrush = ownerGrid.SelectedItemWithFocusBackColor.GetCachedSolidBrushScope();
            g.FillRectangle(
                focusBrush,
                stringX, rect.Y, rect.Width - stringX - 1, rect.Height);
        }
 
        int maxSpace = Math.Min(rect.Width - stringX - 1, labelWidth + PropertyGridView.GdiPlusSpace);
        Rectangle textRect = new(stringX, rect.Y + 1, maxSpace, rect.Height - 1);
 
        if (!Rectangle.Intersect(textRect, clipRect).IsEmpty)
        {
            Region oldClip = g.Clip;
            g.SetClip(textRect);
 
            // We need to invert the color only if in Highcontrast mode, targeting 4.7.1 and above, Gridcategory and
            // not a developer override. This is required to achieve required contrast ratio.
            bool shouldInvert = ColorInversionNeededInHighContrast && (bold || (selected && !_hasFocus));
 
            // Do actual drawing.
 
            // A brush is needed if using GDI+ only (UseCompatibleTextRendering); if using GDI, only the color is needed.
            Color textColor = selected && _hasFocus
                ? ownerGrid.SelectedItemWithFocusForeColor
                : shouldInvert
                    ? InvertColor(OwnerGrid.LineColor)
                    : g.FindNearestColor(LabelTextColor);
 
            if (OwnerGrid.UseCompatibleTextRendering)
            {
                using var textBrush = textColor.GetCachedSolidBrushScope();
                StringFormat stringFormat = new(StringFormatFlags.NoWrap)
                {
                    Trimming = StringTrimming.None
                };
                g.DrawString(label, font, textBrush, textRect, stringFormat);
            }
            else
            {
                TextRenderer.DrawText(g, label, font, textRect, textColor, PropertyGrid.MeasureTextHelper.GetTextRendererFlags());
            }
 
            g.SetClip(oldClip, CombineMode.Replace);
            oldClip.Dispose();   // SetClip copies the passed in Region.
 
            _labelTipPoint = maxSpace <= labelWidth ? new Point(stringX + 2, rect.Y + 1) : InvalidPoint;
        }
 
        rect.Y -= 1;
        rect.Height += 2;
 
        if (Expandable)
        {
            PaintOutlineGlyph(g, rect);
        }
    }
 
    /// <summary>
    ///  Paints the outline portion (the expand / collapse area to the left) of this <see cref="GridEntry"/> into
    ///  the given <see cref="Graphics"/> object.
    /// </summary>
    private void PaintOutlineGlyph(Graphics g, Rectangle r)
    {
        if (OwnerGridView is { } owner && owner.IsExplorerTreeSupported)
        {
            // Draw tree-view glyphs with the current ExplorerTreeView UxTheme
 
            if (!_lastPaintWithExplorerStyle)
            {
                // Size of Explorer Tree style glyph (triangle) is different from +/- glyph, so when we change the
                // visual style (such as changing the Windows theme), we need to recalculate outlineRect.
 
                _outlineRect = Rectangle.Empty;
                _lastPaintWithExplorerStyle = true;
            }
 
            PaintOutlineWithExplorerTreeStyle(g, r, OwnerGridView.HWNDInternal);
        }
        else
        {
            // Draw tree-view glyphs as +/-
 
            if (_lastPaintWithExplorerStyle)
            {
                // Size of Explorer Tree style glyph (triangle) is different from +/- glyph, so when we change the
                // visual style (such as changing the Windows theme), we need to recalculate outlineRect.
 
                _outlineRect = Rectangle.Empty;
                _lastPaintWithExplorerStyle = false;
            }
 
            PaintOutlineWithClassicStyle(g, r);
        }
 
        // Draw the expansion glyph with the Explorer tree style.
        void PaintOutlineWithExplorerTreeStyle(Graphics g, Rectangle r, HWND hwnd)
        {
            // Make sure we're in our bounds.
            Rectangle outline = Rectangle.Intersect(r, OutlineRectangle);
            if (outline.IsEmpty)
            {
                return;
            }
 
            bool expanded = InternalExpanded;
 
            // Invert color if it is not overridden by developer.
            if (g is not null && ColorInversionNeededInHighContrast)
            {
                Color textColor = InvertColor(OwnerGrid.LineColor);
                using var brush = textColor.GetCachedSolidBrushScope();
                g.FillRectangle(brush, outline);
            }
 
            if (g is null)
            {
                throw new InvalidOperationException();
            }
 
            if (ColorInversionNeededInHighContrast || !expanded)
            {
                VisualStyleElement element = expanded
                    ? VisualStyleElement.ExplorerTreeView.Glyph.Opened
                    : VisualStyleElement.ExplorerTreeView.Glyph.Closed;
 
                VisualStyleRenderer explorerTreeRenderer = new(element);
                RedrawExplorerTreeViewClosedGlyph(g, explorerTreeRenderer, outline, hwnd);
            }
            else
            {
                using DeviceContextHdcScope hdc = new(g);
                VisualStyleRenderer explorerTreeRenderer = new(VisualStyleElement.ExplorerTreeView.Glyph.Opened);
                explorerTreeRenderer.DrawBackground(hdc, outline, hwnd);
            }
 
            unsafe void RedrawExplorerTreeViewClosedGlyph(
                Graphics graphics,
                VisualStyleRenderer explorerTreeRenderer,
                Rectangle rectangle,
                HWND hwnd)
            {
                Color backgroundColor = ColorInversionNeededInHighContrast ? InvertColor(OwnerGrid.LineColor) : OwnerGrid.LineColor;
                using CreateDcScope compatibleDC = new(default);
 
                int planes = PInvokeCore.GetDeviceCaps(compatibleDC, GET_DEVICE_CAPS_INDEX.PLANES);
                int bitsPixel = PInvokeCore.GetDeviceCaps(compatibleDC, GET_DEVICE_CAPS_INDEX.BITSPIXEL);
                using HBITMAP compatibleBitmap = PInvokeCore.CreateBitmap(rectangle.Width, rectangle.Height, (uint)planes, (uint)bitsPixel, lpBits: null);
                using SelectObjectScope targetBitmapSelection = new(compatibleDC, compatibleBitmap);
 
                using CreateBrushScope brush = new(backgroundColor);
                compatibleDC.HDC.FillRectangle(new Rectangle(0, 0, rectangle.Width, rectangle.Height), brush);
                explorerTreeRenderer.DrawBackground(compatibleDC, new Rectangle(0, 0, rectangle.Width, rectangle.Height), hwnd);
 
                using Bitmap bitmap = Image.FromHbitmap(compatibleBitmap);
                ControlPaint.InvertForeColorIfNeeded(bitmap, backgroundColor);
                graphics.DrawImage(bitmap, rectangle, 0, 0, bitmap.Width, bitmap.Height, GraphicsUnit.Pixel);
            }
        }
 
        // Draw the expansion glyph as a plus/minus.
        void PaintOutlineWithClassicStyle(Graphics g, Rectangle r)
        {
            // Make sure we're in our bounds.
            Rectangle outline = Rectangle.Intersect(r, OutlineRectangle);
            if (outline.IsEmpty)
            {
                return;
            }
 
            bool expanded = InternalExpanded;
 
            Color penColor = OwnerGridView?.TextColor ?? default;
 
            if (ColorInversionNeededInHighContrast)
            {
                // Inverting text color to background to get required contrast ratio.
                penColor = InvertColor(OwnerGrid.LineColor);
            }
            else
            {
                // Fill the background when not inverting.
                using var brush = BackgroundColor.GetCachedSolidBrushScope();
                g.FillRectangle(brush, outline);
            }
 
            // Draw the border.
            using var pen = penColor.GetCachedPenScope();
            g.DrawRectangle(pen, outline.X, outline.Y, outline.Width - 1, outline.Height - 1);
 
            // Draw horizontal line for +/-
            int indent = 2;
            g.DrawLine(
                pen,
                outline.X + indent,
                outline.Y + outline.Height / 2,
                outline.X + outline.Width - indent - 1,
                outline.Y + outline.Height / 2);
 
            // Draw vertical line to make a +
            if (!expanded)
            {
                g.DrawLine(
                    pen,
                    outline.X + outline.Width / 2,
                    outline.Y + indent,
                    outline.X + outline.Width / 2,
                    outline.Y + outline.Height - indent - 1);
            }
        }
    }
 
    /// <summary>
    ///  Paints the value portion of this <see cref="GridEntry"/> into the given <see cref="Graphics"/> object.
    ///  This is called by the <see cref="GridEntry"/> host (the <see cref="PropertyGridView"/>) when this
    ///  <see cref="GridEntry"/> is to be painted.
    /// </summary>
    /// <param name="text">
    ///  Optional text representation of the value. If not specified, will use the <see cref="PropertyValue"/> directly.
    /// </param>
    public virtual void PaintValue(
        Graphics g,
        Rectangle rect,
        Rectangle clipRect,
        PaintValueFlags paintFlags,
        string? text = null)
    {
        if (OwnerGridView is not PropertyGridView ownerGrid)
        {
            throw new InvalidOperationException();
        }
 
        Color textColor = ShouldRenderReadOnly ? ownerGrid.GrayTextColor : ownerGrid.TextColor;
        object? value;
 
        if (text is null)
        {
            if (_cacheItems is not null && _cacheItems.UseValueString)
            {
                text = _cacheItems.LastValueString;
                value = _cacheItems.LastValue;
            }
            else
            {
                value = PropertyValue;
                text = GetPropertyTextValue(value);
 
                _cacheItems ??= new CacheItems();
                _cacheItems.LastValueString = text;
                _cacheItems.UseValueString = true;
                _cacheItems.LastValueTextWidth = -1;
                _cacheItems.LastValueFont = null;
                _cacheItems.LastValue = value;
            }
        }
        else
        {
            value = ConvertTextToValue(text);
            text = GetPropertyTextValue(value);
        }
 
        // Paint out the main rect using the appropriate brush.
        Color backColor = BackgroundColor;
 
        if (paintFlags.HasFlag(PaintValueFlags.DrawSelected))
        {
            backColor = ownerGrid.SelectedItemWithFocusBackColor;
            textColor = ownerGrid.SelectedItemWithFocusForeColor;
        }
 
        using var backBrush = backColor.GetCachedSolidBrushScope();
        g.FillRectangle(backBrush, clipRect);
 
        int paintIndent = 0;
        if (IsCustomPaint)
        {
            paintIndent = ownerGrid.ValuePaintIndent;
            Rectangle rectPaint = new(
                rect.X + 1,
                rect.Y + 1,
                ownerGrid.ValuePaintWidth,
                ownerGrid.GridEntryHeight - 2);
 
            if (!Rectangle.Intersect(rectPaint, clipRect).IsEmpty)
            {
                UITypeEditor?.PaintValue(new PaintValueEventArgs(this, value, g, rectPaint));
 
                // Paint a border around the area
                rectPaint.Width--;
                rectPaint.Height--;
                g.DrawRectangle(SystemPens.WindowText, rectPaint);
            }
        }
 
        rect.X += paintIndent + PropertyGridView.ValueStringIndent;
        rect.Width -= paintIndent + 2 * PropertyGridView.ValueStringIndent;
 
        // Bold the property if we need to persist it (e.g. it's not the default value).
        bool valueModified = paintFlags.HasFlag(PaintValueFlags.CheckShouldSerialize) && ShouldSerializePropertyValue();
 
        // If we have text to paint, paint it.
        if (string.IsNullOrEmpty(text))
        {
            return;
        }
 
        if (text.Length > MaximumLengthOfPropertyString)
        {
            text = text[..MaximumLengthOfPropertyString];
        }
 
        int textWidth = GetValueTextWidth(text, g, GetFont(valueModified));
        bool doToolTip = false;
 
        // Check if text contains multiple lines.
        if (textWidth >= rect.Width || HasMultipleLines(text))
        {
            doToolTip = true;
        }
 
        if (Rectangle.Intersect(rect, clipRect).IsEmpty)
        {
            return;
        }
 
        // Do actual drawing, shifting to match the PropertyGridView.GridViewListbox content alignment.
 
        if (paintFlags.HasFlag(PaintValueFlags.PaintInPlace))
        {
            rect.Offset(1, 2);
        }
        else
        {
            // Only go down one pixel when we're painting in the listbox.
            rect.Offset(1, 1);
        }
 
        Rectangle textRectangle = new(
            rect.X - 1,
            rect.Y - 1,
            rect.Width - 4,
            rect.Height);
 
        backColor = OwnerGridView is not { } owner
            ? default
            : paintFlags.HasFlag(PaintValueFlags.DrawSelected)
                ? owner.SelectedItemWithFocusBackColor
                : owner.BackColor;
 
        DRAW_TEXT_FORMAT format = DRAW_TEXT_FORMAT.DT_EDITCONTROL | DRAW_TEXT_FORMAT.DT_EXPANDTABS | DRAW_TEXT_FORMAT.DT_NOCLIP
            | DRAW_TEXT_FORMAT.DT_SINGLELINE | DRAW_TEXT_FORMAT.DT_NOPREFIX;
 
        if (ownerGrid.DrawValuesRightToLeft)
        {
            format |= DRAW_TEXT_FORMAT.DT_RIGHT | DRAW_TEXT_FORMAT.DT_RTLREADING;
        }
 
        // For password mode, replace the string value with a bullet.
        if (ShouldRenderPassword)
        {
            if (s_passwordReplaceChar == '\0')
            {
                // Bullet is 2022, but edit box uses round circle 25CF
                s_passwordReplaceChar = '\u25CF';
            }
 
            text = new string(s_passwordReplaceChar, text.Length);
        }
 
        TextRenderer.DrawTextInternal(
            g,
            text,
            GetFont(boldFont: valueModified),
            textRectangle,
            textColor,
            backColor,
            (TextFormatFlags)format | PropertyGrid.MeasureTextHelper.GetTextRendererFlags());
 
        ValueToolTipLocation = doToolTip ? new Point(rect.X + 2, rect.Y - 1) : InvalidPoint;
 
        static bool HasMultipleLines(string value) => value.IndexOf('\n') > 0 || value.IndexOf('\r') > 0;
    }
 
    public virtual bool OnComponentChanging()
    {
        try
        {
            object? owner = GetValueOwner();
            if (owner is not null)
            {
                ComponentChangeService?.OnComponentChanging(owner, PropertyDescriptor);
                return true;
            }
 
            return false;
        }
        catch (CheckoutException e) when (e == CheckoutException.Canceled)
        {
            return false;
        }
    }
 
    public virtual void OnComponentChanged()
    {
        object? owner = GetValueOwner();
        if (owner is not null)
        {
            ComponentChangeService?.OnComponentChanged(owner, PropertyDescriptor);
        }
    }
 
    /// <summary>
    ///  Called when the GridEntry is clicked.
    /// </summary>
    public virtual bool OnMouseClick(int x, int y, int count, MouseButtons button)
    {
        // Where are we at?
        PropertyGridView gridHost = OwnerGridView!;
        Debug.Assert(gridHost is not null, "No prop entry host!");
 
        // Make sure it's the left button.
        if ((button & MouseButtons.Left) != MouseButtons.Left)
        {
            return false;
        }
 
        int labelWidth = gridHost.LabelWidth;
 
        // Are we in the label?
        if (x >= 0 && x <= labelWidth)
        {
            // Are we on the outline?
            if (Expandable)
            {
                Rectangle outlineRect = OutlineRectangle;
                if (outlineRect.Contains(x, y))
                {
                    if (count % 2 == 0)
                    {
                        RaiseEvent(s_outlineDoubleClickEvent, EventArgs.Empty);
                    }
                    else
                    {
                        OnOutlineClick(EventArgs.Empty);
                    }
 
                    return true;
                }
            }
 
            RaiseEvent(count % 2 == 0 ? s_labelDoubleClickEvent : s_labelClickEvent, EventArgs.Empty);
            return true;
        }
 
        // Are we in the value?
        labelWidth += PropertyGridView.SplitterWidth;
        if (x >= labelWidth && x <= labelWidth + gridHost.ValueWidth)
        {
            RaiseEvent(count % 2 == 0 ? s_valueDoubleClickEvent : s_valueClickEvent, EventArgs.Empty);
            return true;
        }
 
        return false;
    }
 
    /// <summary>
    ///  Called when the outline icon portion of this <see cref="GridEntry"/> is clicked.
    /// </summary>
    protected void OnOutlineClick(EventArgs e) => RaiseEvent(s_outlineClickEvent, e);
 
    internal bool OnValueReturnKey() => SendNotificationToParent(Notify.Return);
 
    /// <summary>
    ///  Sets the specified flag.
    /// </summary>
    protected void SetFlag(Flags flag, bool value)
        => _flags = (EntryFlags & ~flag) | (value ? flag : 0);
 
    public override bool Select()
    {
        if (Disposed)
        {
            return false;
        }
 
        try
        {
            if (OwnerGridView is not PropertyGridView propertyGridView)
            {
                return false;
            }
 
            propertyGridView.SelectedGridEntry = this;
 
            return true;
        }
        catch (Exception ex) when (!ex.IsCriticalException())
        {
        }
 
        return false;
    }
 
    /// <summary>
    ///  Checks if this value should be persisted.
    /// </summary>
    internal bool ShouldSerializePropertyValue()
    {
        if (_cacheItems is not null)
        {
            if (_cacheItems.UseShouldSerialize)
            {
                return _cacheItems.LastShouldSerialize;
            }
            else
            {
                _cacheItems.LastShouldSerialize = SendNotificationToParent(Notify.ShouldPersist);
                _cacheItems.UseShouldSerialize = true;
            }
        }
        else
        {
            _cacheItems = new CacheItems
            {
                LastShouldSerialize = SendNotificationToParent(Notify.ShouldPersist),
                UseShouldSerialize = true
            };
        }
 
        return _cacheItems.LastShouldSerialize;
    }
 
    private static PropertyDescriptor[] SortParenProperties(PropertyDescriptor[] props)
    {
        PropertyDescriptor[]? newProperties = null;
        int newPosition = 0;
 
        // First scan the list and move any parenthesized properties to the front.
        for (int i = 0; i < props.Length; i++)
        {
            if (props[i].GetAttribute<ParenthesizePropertyNameAttribute>()?.NeedParenthesis ?? false)
            {
                newProperties ??= new PropertyDescriptor[props.Length];
                newProperties[newPosition++] = props[i];
                props[i] = null!;
            }
        }
 
        // Second pass, copy any that didn't have the parenthesis.
        if (newProperties is not null)
        {
            for (int i = 0; i < props.Length; i++)
            {
                if (props[i] is not null)
                {
                    newProperties[newPosition++] = props[i];
                }
            }
 
            props = newProperties;
        }
 
        return props;
    }
 
    /// <summary>
    ///  Sends a notification to the given owner.
    /// </summary>
    /// <param name="owner">
    ///  The owner of the <see cref="GridItem"/>.
    /// </param>
    /// <returns>
    ///  The result of the notification.
    /// </returns>
    protected virtual bool SendNotification(object? owner, Notify notification) => false;
 
    /// <summary>
    ///  Sends a notification to the owner of the given <paramref name="entry"/>.
    /// </summary>
    /// <returns>
    ///  The result of the notification.
    /// </returns>
    internal virtual bool SendNotification(GridEntry entry, Notify notification)
        => entry.SendNotification(entry.GetValueOwner(), notification);
 
    /// <summary>
    ///  Sends a notification to the owner of the <see cref="ParentGridEntry"/> if it exists.
    /// </summary>
    /// <returns>
    ///  The result of the notification. Returns true if there is no parent.
    /// </returns>
    internal bool SendNotificationToParent(Notify type)
        => _parent is null || _parent.SendNotification(this, type);
 
    protected void RecreateChildren() => RecreateChildren(-1);
 
    protected void RecreateChildren(int oldCount)
    {
        // Cause the flags to be rebuilt as well.
        bool wasExpanded = InternalExpanded || oldCount > 0;
 
        if (oldCount == -1)
        {
            oldCount = VisibleChildCount;
        }
 
        ResetState();
        if (oldCount == 0)
        {
            return;
        }
 
        foreach (GridEntry child in ChildCollection)
        {
            child.RecreateChildren();
        }
 
        DisposeChildren();
        InternalExpanded = wasExpanded;
 
        if (GetEventHandler(s_recreateChildrenEvent) is GridEntryRecreateChildrenEventHandler handler)
        {
            handler(this, new(oldCount, VisibleChildCount));
        }
    }
 
    /// <summary>
    ///  Refresh the current GridEntry's value and it's children.
    /// </summary>
    public void Refresh()
    {
        Type? type = PropertyType;
        if (type is not null && type.IsArray)
        {
            CreateChildren(useExistingChildren: true);
        }
 
        if (_children is not null)
        {
            // Check to see if the value has changed.
            if (InternalExpanded && _cacheItems?.LastValue is not null && _cacheItems.LastValue != PropertyValue)
            {
                ClearCachedValues();
                RecreateChildren();
                return;
            }
            else if (InternalExpanded)
            {
                // Otherwise just do a refresh.
                IEnumerator childEnum = _children.GetEnumerator();
                while (childEnum.MoveNext())
                {
                    object o = childEnum.Current;
                    Debug.Assert(o is not null, "Collection contains a null element.");
                    ((GridEntry)o).Refresh();
                }
            }
            else
            {
                DisposeChildren();
            }
        }
 
        ClearCachedValues();
    }
 
    public void RemoveOnLabelClick(EventHandler handler) => RemoveEventHandler(s_labelClickEvent, handler);
 
    public void RemoveOnLabelDoubleClick(EventHandler handler) => RemoveEventHandler(s_labelDoubleClickEvent, handler);
 
    public void RemoveOnValueClick(EventHandler handler) => RemoveEventHandler(s_valueClickEvent, handler);
 
    public void RemoveOnValueDoubleClick(EventHandler handler) => RemoveEventHandler(s_valueDoubleClickEvent, handler);
 
    public void RemoveOnOutlineClick(EventHandler handler) => RemoveEventHandler(s_outlineClickEvent, handler);
 
    public void RemoveOnOutlineDoubleClick(EventHandler handler) => RemoveEventHandler(s_outlineDoubleClickEvent, handler);
 
    public void RemoveOnRecreateChildren(GridEntryRecreateChildrenEventHandler handler)
        => RemoveEventHandler(s_recreateChildrenEvent, handler);
 
    private void ResetState()
    {
        ClearFlags();
        ClearCachedValues();
    }
 
    /// <summary>
    ///  Sets the value of this <see cref="GridEntry"/> from text.
    /// </summary>
    public bool SetPropertyTextValue(string? text)
    {
        bool childrenPrior = _children is not null && _children.Count > 0;
        PropertyValue = ConvertTextToValue(text);
        CreateChildren();
        bool childrenAfter = _children is not null && _children.Count > 0;
        return childrenPrior != childrenAfter;
    }
 
    public override string ToString() => $"{GetType().FullName} {PropertyLabel}";
 
    protected virtual void AddEventHandler(object key, Delegate handler)
    {
        lock (_lock)
        {
            if (handler is null)
            {
                return;
            }
 
            for (EventEntry? e = _eventList; e is not null; e = e.Next)
            {
                if (e.Key == key)
                {
                    e.Handler = Delegate.Combine(e.Handler, handler);
                    return;
                }
            }
 
            _eventList = new EventEntry(_eventList, key, handler);
        }
    }
 
    protected virtual void RaiseEvent(object key, EventArgs e)
    {
        Delegate? handler = GetEventHandler(key);
        if (handler is not null)
        {
            ((EventHandler)handler)(this, e);
        }
    }
 
    protected virtual Delegate? GetEventHandler(object key)
    {
        lock (_lock)
        {
            for (EventEntry? e = _eventList; e is not null; e = e.Next)
            {
                if (e.Key == key)
                {
                    return e.Handler;
                }
            }
 
            return null;
        }
    }
 
    protected virtual void RemoveEventHandler(object key, Delegate handler)
    {
        lock (_lock)
        {
            if (handler is null)
            {
                return;
            }
 
            for (EventEntry? entry = _eventList, previous = null; entry is not null; previous = entry, entry = entry.Next)
            {
                if (entry.Key == key)
                {
                    entry.Handler = Delegate.Remove(entry.Handler, handler);
                    if (entry.Handler is null)
                    {
                        if (previous is null)
                        {
                            _eventList = entry.Next;
                        }
                        else
                        {
                            previous.Next = entry.Next;
                        }
                    }
 
                    return;
                }
            }
        }
    }
 
    protected virtual void RemoveEventHandlers() => _eventList = null;
}
 
internal delegate void GridEntryRecreateChildrenEventHandler(object sender, GridEntryRecreateChildrenEventArgs rce);