Grouping ListView items dynamically


Description:

Grouping ListView items dynamically can be confusing at first, the reason is that you can't simply "tell" the item what's his group's header is. You have to find the proper group from the group-collection and add the item to that existing group. And if no matching group is found, you have to create a new group. Okay, let's get started:

I'm going to skip form-designing. The form that I'm going to use for demonstration looks like this: 

1.gif

Drop-down list items:
  • Size: Small, Medium, Large
  • Color: Red, Yellow, Green, Blue, Violet
  • Group: By name, By size, By color
Our grouping function:

private void GroupItem(ListViewItem item)
{
    // This flag will tell us if proper group already exists
    bool group_exists = false;
    // Check each group if it fits to the item
    foreach (ListViewGroup group in this.listView.Groups)
    {
        // Compare group's header to selected subitem's text
        if (group.Header == item.SubItems[this.groupBox.SelectedIndex].Text)
        {
            // Add item to the group.
            // Alternative is: group.Items.Add(item);
            item.Group = group;
            group_exists = true;
            break;
        }
    }
    // Create new group if no proper group was found
    if (!group_exists)
    {
        // Create group and specify its header by
        // getting selected subitem's text
        ListViewGroup group = new ListViewGroup(item.SubItems[this.groupBox.SelectedIndex].Text);
        // We need to add the group to the ListView first
        this.listView.Groups.Add(group);
        item.Group = group;
    }
}

Make sure some items are selected from the start:

this.sizeBox.SelectedIndex = 0;
this.colorBox.SelectedIndex = 0;
this.groupBox.SelectedIndex = 0;

Add an event handler to the SelectedIndexChanged event, which occurs when different type of grouping is selected :

this.groupBox.SelectedIndexChanged += (object o, EventArgs e) =>
{
    // Clear group collection
    this.listView.Groups.Clear();
    // Loop through all existing items to group them properly
    foreach (ListViewItem item in this.listView.Items)
    {
        GroupItem(item);
    }
};

Event handler for a click on the "Add" button:

this.add.Click += (object o, EventArgs e) =>
{
    // Display messagebox if textbox with name is empty
    if (this.nameBox.Text == "")
    {
        MessageBox.Show("No name was specified.", "Name missing!",
            MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
    else
    {
        // ListViewItem has a useful constructor that
        // allows us to add collection of subitems
        // in form of a string array
        ListViewItem item = new ListViewItem(new string[] {
            this.nameBox.Text,
            // Unbox the selected item by using a cast
            (string)this.sizeBox.SelectedItem,
            (string)this.colorBox.SelectedItem
        });
        // Call our item-grouping function
        GroupItem(item);
        // Finally, add the item to the ListView
        this.listView.Items.Add(item);
    }
};

2.gif

For more complex grouping use enumeration (for types of grouping) and a switch statement that assigns proper checking actions to a boolean delegate.  Use the boolean delegate in the if statement that checks if existing group matches the condition.

For example, let's group items either by first letter or by name lenght:

Our delegate and its variable:

delegate bool GroupMatch(ListViewItem item, ListViewGroup group);
GroupMatch groupMatch;

Enumeration and its variable with "ByLetter" as initial value:

enum Grouping { ByLetter, ByLenght };
Grouping grouping = Grouping.ByLetter;

Switch statement that assigns proper checking actions to our delegate variable

switch (grouping)
{
    case Grouping.ByLetter:
        groupMatch = (ListViewItem item, ListViewGroup group) =>
        {
            return item.Text.ToUpper()[0] == group.Header[0];
        };
        break;
    case Grouping.ByLenght:
        groupMatch = (ListViewItem item, ListViewGroup group) =>
        {
            return item.Text.Length == group.Header.Length;
        };
        break;
}

The body of our new GroupItem function:

bool group_exists = false;
foreach (ListViewGroup group in this.listView.Groups)
{
    if (groupMatch(item, group))
    {
        item.Group = group;
        group_exists = true;
        break;
    }
}
if (!group_exists)
{
    ListViewGroup group;
    switch (grouping)
    {
        case Grouping.ByLetter:
            group = new ListViewGroup(item.Name.ToUpper()[0].ToString());
            break;
        case Grouping.ByLenght:
            group = new ListViewGroup(item.Name.Length.ToString());
            break;
        default:
            // even though this will never execute
            // we have add default case, otherwise
            // the program won't compile, because group
            // supposedly isn't assigned in all cases
            return;
    }
    this.listView.Groups.Add(group);
    item.Group = group;
}

If you want to sort items by a value that isn't in the collection of subitems, one way to do it is to assign the value to ListViewItem.Tag. Tag's type is Sytem.Object (keyword object), which means that you can add whole collection of data to the item without adding any subitems. But be sure to box and unbox your data properly.

Thanks for reading, code safely!