File: System\Windows\Forms\Controls\ListView\ListViewGroup.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.ComponentModel;
using System.Runtime.Serialization;
 
namespace System.Windows.Forms;
 
/// <summary>
///  Represents a group within a ListView.
/// </summary>
[TypeConverter(typeof(ListViewGroupConverter))]
[ToolboxItem(false)]
[DesignTimeVisible(false)]
[DefaultProperty(nameof(Header))]
[Serializable] // This type is participating in resx serialization scenarios.
public sealed partial class ListViewGroup : ISerializable
{
    private string? _header;
    private HorizontalAlignment _headerAlignment = HorizontalAlignment.Left;
    private string? _footer;
    private HorizontalAlignment _footerAlignment = HorizontalAlignment.Left;
    private ListViewGroupCollapsedState _collapsedState = ListViewGroupCollapsedState.Default;
    private string? _subtitle;
    private string? _taskLink;
 
    private ListView.ListViewItemCollection? _items;
 
    private static int s_nextID;
 
    private static int s_nextHeader = 1;
 
    private ListViewGroupImageIndexer? _imageIndexer;
    private AccessibleObject? _accessibilityObject;
 
    /// <summary>
    ///  Creates a ListViewGroup.
    /// </summary>
    public ListViewGroup() : this(string.Format(SR.ListViewGroupDefaultHeader, s_nextHeader++))
    {
    }
 
    /// <summary>
    ///  Creates a ListViewItem object from an Stream.
    /// </summary>
    private ListViewGroup(SerializationInfo info, StreamingContext context) : this()
    {
        Deserialize(info);
    }
 
    /// <summary>
    ///  Creates a ListViewItem object from a Key and a Name
    /// </summary>
    public ListViewGroup(string? key, string? headerText) : this()
    {
        Name = key;
        _header = headerText;
    }
 
    /// <summary>
    ///  Creates a ListViewGroup.
    /// </summary>
    public ListViewGroup(string? header)
    {
        _header = header;
        ID = s_nextID++;
    }
 
    /// <summary>
    ///  Creates a ListViewGroup.
    /// </summary>
    public ListViewGroup(string? header, HorizontalAlignment headerAlignment) : this(header)
    {
        _headerAlignment = headerAlignment;
    }
 
    internal AccessibleObject AccessibilityObject
        => _accessibilityObject ??= new ListViewGroupAccessibleObject(this, ListView?.Groups.Contains(this) == false);
 
    /// <summary>
    ///  The text displayed in the group header.
    /// </summary>
    [AllowNull]
    [DefaultValue("")]
    [SRCategory(nameof(SR.CatAppearance))]
    public string Header
    {
        get => _header ?? string.Empty;
        set
        {
            if (_header == value)
            {
                return;
            }
 
            _header = value;
            UpdateListView();
        }
    }
 
    /// <summary>
    ///  The alignment of the group header.
    /// </summary>
    [DefaultValue(HorizontalAlignment.Left)]
    [SRCategory(nameof(SR.CatAppearance))]
    public HorizontalAlignment HeaderAlignment
    {
        get => _headerAlignment;
        set
        {
            SourceGenerated.EnumValidator.Validate(value);
 
            if (_headerAlignment == value)
            {
                return;
            }
 
            _headerAlignment = value;
            UpdateListView();
        }
    }
 
    /// <summary>
    ///  The text displayed in the group footer.
    /// </summary>
    [AllowNull]
    [DefaultValue("")]
    [SRCategory(nameof(SR.CatAppearance))]
    public string Footer
    {
        get => _footer ?? string.Empty;
        set
        {
            if (_footer == value)
            {
                return;
            }
 
            _footer = value;
            UpdateListView();
        }
    }
 
    /// <summary>
    ///  The alignment of the group footer.
    /// </summary>
    /// <value>
    ///  One of the <see cref="HorizontalAlignment"/> values that specifies the alignment of the footer text.
    ///  The default is <see cref="HorizontalAlignment.Left"/>.
    /// </value>
    /// <exception cref="InvalidEnumArgumentException">
    ///  The specified value when setting this property is not a valid <see cref="HorizontalAlignment"/> value.
    /// </exception>
    [DefaultValue(HorizontalAlignment.Left)]
    [SRCategory(nameof(SR.CatAppearance))]
    public HorizontalAlignment FooterAlignment
    {
        get => _footerAlignment;
        set
        {
            SourceGenerated.EnumValidator.Validate(value);
 
            if (_footerAlignment == value)
            {
                return;
            }
 
            _footerAlignment = value;
            UpdateListView();
        }
    }
 
    internal bool Focused { get; set; }
 
    /// <summary>
    ///  Controls which <see cref="ListViewGroupCollapsedState"/> the group will appear as.
    /// </summary>
    /// <value>
    ///  One of the <see cref="ListViewGroupCollapsedState"/> values that specifies how the group is displayed.
    ///  The default is <see cref="ListViewGroupCollapsedState.Default"/>.
    /// </value>
    /// <exception cref="InvalidEnumArgumentException">
    ///  The specified value when setting this property is not a valid <see cref="ListViewGroupCollapsedState"/> value.
    /// </exception>
    [DefaultValue(ListViewGroupCollapsedState.Default)]
    [SRCategory(nameof(SR.CatAppearance))]
    public ListViewGroupCollapsedState CollapsedState
    {
        get => _collapsedState;
        set
        {
            SourceGenerated.EnumValidator.Validate(value);
 
            if (_collapsedState == value)
            {
                return;
            }
 
            _collapsedState = value;
            UpdateListView();
        }
    }
 
    /// <summary>
    ///  The text displayed in the group subtitle.
    /// </summary>
    [AllowNull]
    [DefaultValue("")]
    [SRCategory(nameof(SR.CatAppearance))]
    public string Subtitle
    {
        get => _subtitle ?? string.Empty;
        set
        {
            if (_subtitle == value)
            {
                return;
            }
 
            _subtitle = value;
            UpdateListView();
        }
    }
 
    /// <summary>
    ///  The name of the task link displayed in the group header.
    /// </summary>
    [AllowNull]
    [DefaultValue("")]
    [SRCategory(nameof(SR.CatAppearance))]
    public string TaskLink
    {
        get => _taskLink ?? string.Empty;
        set
        {
            if (value == _taskLink)
            {
                return;
            }
 
            _taskLink = value;
            UpdateListView();
        }
    }
 
    /// <summary>
    ///  Gets or sets the index of the image that is displayed for the group.
    /// </summary>
    /// <value>
    ///  The zero-based index of the image in the ImageList that is displayed for the group. The default is -1.
    /// </value>
    /// <exception cref="ArgumentOutOfRangeException">
    ///  The value specified is less than -1.
    /// </exception>
    [DefaultValue(-1)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [Editor($"System.Windows.Forms.Design.ImageIndexEditor, {AssemblyRef.SystemDesign}", typeof(Drawing.Design.UITypeEditor))]
    [Localizable(true)]
    [RefreshProperties(RefreshProperties.Repaint)]
    [RelatedImageList("ListView.GroupImageList")]
    [SRCategory(nameof(SR.CatBehavior))]
    [TypeConverter(typeof(NoneExcludedImageIndexConverter))]
    public int TitleImageIndex
    {
        get
        {
            ImageList? imageList = ImageIndexer.ImageList;
            return imageList is null || ImageIndexer.Index < imageList.Images.Count
                ? ImageIndexer.Index
                : imageList.Images.Count - 1;
        }
        set
        {
            ArgumentOutOfRangeException.ThrowIfLessThan(value, ImageList.Indexer.DefaultIndex);
 
            if (ImageIndexer.Index == value && value != ImageList.Indexer.DefaultIndex)
            {
                return;
            }
 
            ImageIndexer.Index = value;
            UpdateListView();
        }
    }
 
    /// <summary>
    ///  Gets or sets the key of the image that is displayed for the group.
    /// </summary>
    /// <value>
    ///  The key for the image that is displayed for the group.
    /// </value>
    [DefaultValue("")]
    [TypeConverter(typeof(ImageKeyConverter))]
    [Editor($"System.Windows.Forms.Design.ImageIndexEditor, {AssemblyRef.SystemDesign}", typeof(Drawing.Design.UITypeEditor))]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [RefreshProperties(RefreshProperties.Repaint)]
    [RelatedImageList("ListView.GroupImageList")]
    [SRCategory(nameof(SR.CatBehavior))]
    [Localizable(true)]
    public string TitleImageKey
    {
        get => ImageIndexer.Key;
        set
        {
            if (ImageIndexer.Key == value && value != ImageList.Indexer.DefaultKey)
            {
                return;
            }
 
            ImageIndexer.Key = value;
            UpdateListView();
        }
    }
 
    internal ListViewGroupImageIndexer ImageIndexer => _imageIndexer ??= new ListViewGroupImageIndexer(this);
 
    internal int ID { get; }
 
    /// <summary>
    ///  The items that belong to this group.
    /// </summary>
    [Browsable(false)]
    public ListView.ListViewItemCollection Items => _items ??= new ListView.ListViewItemCollection(new ListViewGroupItemCollection(this));
 
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public ListView? ListView { get; internal set; }
 
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.ListViewGroupNameDescr))]
    [Browsable(true)]
    [DefaultValue("")]
    public string? Name { get; set; }
 
    [SRCategory(nameof(SR.CatData))]
    [Localizable(false)]
    [Bindable(true)]
    [SRDescription(nameof(SR.ControlTagDescr))]
    [DefaultValue(null)]
    [TypeConverter(typeof(StringConverter))]
    public object? Tag { get; set; }
 
    private void Deserialize(SerializationInfo info)
    {
        int count = 0;
 
        foreach (SerializationEntry entry in info)
        {
            if (entry.Name == "Header")
            {
                Header = (string)entry.Value!;
            }
            else if (entry.Name == "HeaderAlignment")
            {
                HeaderAlignment = (HorizontalAlignment)entry.Value!;
            }
            else if (entry.Name == "Footer")
            {
                Footer = (string)entry.Value!;
            }
            else if (entry.Name == "FooterAlignment")
            {
                FooterAlignment = (HorizontalAlignment)entry.Value!;
            }
            else if (entry.Name == "Tag")
            {
                Tag = entry.Value;
            }
            else if (entry.Name == "ItemsCount")
            {
                count = (int)entry.Value!;
            }
            else if (entry.Name == "Name")
            {
                Name = (string)entry.Value!;
            }
        }
 
        if (count > 0)
        {
            ListViewItem[] items = new ListViewItem[count];
            for (int i = 0; i < count; i++)
            {
                items[i] = (ListViewItem)info.GetValue($"Item{i}", typeof(ListViewItem))!;
            }
 
            Items.AddRange(items);
        }
    }
 
    internal ListViewGroupCollapsedState GetNativeCollapsedState()
    {
        if (ListView is null)
        {
            throw new InvalidOperationException(nameof(ListView));
        }
 
        if (!ListView.GroupsEnabled)
        {
            return ListViewGroupCollapsedState.Default;
        }
 
        LIST_VIEW_GROUP_STATE_FLAGS state = (LIST_VIEW_GROUP_STATE_FLAGS)(uint)PInvokeCore.SendMessage(
            ListView,
            PInvoke.LVM_GETGROUPSTATE,
            (WPARAM)ID,
            (LPARAM)(uint)(LIST_VIEW_GROUP_STATE_FLAGS.LVGS_COLLAPSIBLE | LIST_VIEW_GROUP_STATE_FLAGS.LVGS_COLLAPSED));
 
        if (!state.HasFlag(LIST_VIEW_GROUP_STATE_FLAGS.LVGS_COLLAPSIBLE))
        {
            return ListViewGroupCollapsedState.Default;
        }
 
        return state.HasFlag(LIST_VIEW_GROUP_STATE_FLAGS.LVGS_COLLAPSED) ? ListViewGroupCollapsedState.Collapsed : ListViewGroupCollapsedState.Expanded;
    }
 
    internal void ReleaseUiaProvider()
    {
        if (OsVersion.IsWindows8OrGreater() && _accessibilityObject is ListViewGroupAccessibleObject accessibleObject)
        {
            PInvoke.UiaDisconnectProvider(accessibleObject, skipOSCheck: true);
        }
 
        _accessibilityObject = null;
    }
 
    // Should be used for the cases when sending the message `PInvoke.LVM_SETGROUPINFO` isn't required
    // (for example, collapsing/expanding groups with keyboard is performed inside the native control already,
    // so this message isn't needed)
    internal void SetCollapsedStateInternal(ListViewGroupCollapsedState state)
    {
        SourceGenerated.EnumValidator.Validate(state);
 
        if (_collapsedState == state)
        {
            return;
        }
 
        _collapsedState = state;
    }
 
    public override string ToString() => Header;
 
    private void UpdateListView()
    {
        if (ListView is not null && ListView.IsHandleCreated)
        {
            ListView.UpdateGroupNative(this);
        }
    }
 
    void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
    {
        ArgumentNullException.ThrowIfNull(info);
 
        info.AddValue(nameof(Header), Header);
        info.AddValue(nameof(HeaderAlignment), HeaderAlignment);
        info.AddValue(nameof(Footer), Footer);
        info.AddValue(nameof(FooterAlignment), FooterAlignment);
        info.AddValue(nameof(Tag), Tag);
        if (!string.IsNullOrEmpty(Name))
        {
            info.AddValue(nameof(Name), Name);
        }
 
        if (_items is not null && _items.Count > 0)
        {
            info.AddValue("ItemsCount", Items.Count);
            for (int i = 0; i < Items.Count; i++)
            {
                info.AddValue($"Item{i}", Items[i], typeof(ListViewItem));
            }
        }
    }
}