C#

Extending C# Listview with Collapsible Groups (Part II)


The GroupedList Control Container

This post is part two of a short series on extending the Winforms Listview control. If you missed the previous post, you can review it HERE. Also, the Source Code for this project can be found in my GitHub repo.

In our previous post, we examined the first component of what I am calling the “GroupedList Control” – essentially, a list of contained and extended Listview controls which act as independent groups. Individual ListGroups (which is how I refer to them) may contain independent column headers, and are expandable/collapsible, much like what I believe is called a “slider” control.

A brief note – I am posting somewhat abbreviated code here. I have omitted many common overloads and other features we might discuss in a future post. For now, the code posted here contains only the very core functionality under discussion. The Source, however, contains all my work so far on this control.

Also note – the GroupedListControl arose out of my need for a quick-and-dirty combination of the functionality of the Winforms Listview and a Treeview. A group of columnar lists which could be independently expanded or collapsed.

A Quick Look at a Very Plain Demo:

Gl Demo 4 Widen Column

In the last post, we had assembled our basic ListGroup component, which is essentially an extension of the Winforms Listview control, modified to handle some events related to column and item addition and removal. Where we left off, it was time to assemble our container, the GroupedListControl.

I figured the quickest way to accomplish what I needed (remember – under the gun, here) would be to extend the FlowLayoutPanel such that I could use this ready-made container to manage a collection of ListGroup controls, stack them vertically, and such. There were a few issues with this approach that we will discuss in a bit. First, let’s look at the basic code required to bring the control to life:

The GroupedList Control – Basic Code:

public class GroupListControl : FlowLayoutPanel
{

    public GroupListControl()
    {
        // Default configuration. Adapt to suit your needs:
        this.FlowDirection = System.Windows.Forms.FlowDirection.TopDown;
        this.AutoScroll = true;
        this.WrapContents = false;

        // Add a local handler for the ControlAdded Event.
        this.ControlAdded += new ControlEventHandler(GroupListControl_ControlAdded);
    }

    void GroupListControl_ControlAdded(object sender, ControlEventArgs e)
    {
        ListGroup lg = (ListGroup)e.Control;
        lg.Width = this.Width;
        lg.GroupCollapsed += new ListGroup.GroupExpansionHandler(lg_GroupCollapsed);
        lg.GroupExpanded += new ListGroup.GroupExpansionHandler(lg_GroupExpanded);
    }

    public bool SingleItemOnlyExpansion { get; set; }

    void lg_GroupExpanded(object sender, EventArgs e)
    {
        // Grab a reference to the ListGroup which sent the message:
        ListGroup expanded = (ListGroup)sender;

        // If Single item only expansion, collapse all ListGroups in except
        // the one currently exanding:
        if (this.SingleItemOnlyExpansion)
        {
            this.SuspendLayout();
            foreach (ListGroup lg in this.Controls)
            {
                if (!lg.Equals(expanded))
                    lg.Collapse();
            }
            this.ResumeLayout(true);
        }

    }

    void lg_GroupCollapsed(object sender, EventArgs e)
    {
        // No need.
    }

    public void ExpandAll()
    {
        foreach (ListGroup lg in this.Controls)
        {
            lg.Expand();
        }
    }

    public void CollapseAll()
    {
        foreach (ListGroup lg in this.Controls)
        {
            lg.Collapse();
        }
    }

}

Of particular note here is the GroupListControl_ControlAdded Event Handler. Sadly, when one adds controls to the FlowLayoutPanel Controls collection, they are just that. The Controls property of the FlowLayout panel represents a ControlCollection object, which accepts a parameter of type (wait for it . . . ) Control.

I wanted MY GroupedListControl to contain a collection of ListGroup objects. However, I have not yet figured out a way to do this while retaining the functionality of the FlowLayout panel. As far as I can tell, we can’t narrow the type requirement of the native ControlCollection. One option I considered would be to add a new method to the class, named AddListGroup, which could then accept a parameter of type ListGroup, and pass THAT to the Controls.Add(Control) method. However, that seems a bit mindless, as the Controls.Add(0 method would remain publicly exposed, thus creating opportunity for confusion.

For now, I decided that those using this control will have to realize that passing anything other than a ListGroup object as the parameter will likely be disappointed in the performance of the control! It is less than elegant, but I didn’t have time to figure out a more elegant solution, and for the moment it works. I would love to hear suggestions for improvement.

The next thing to notice about the GroupListControl_ControlAdded method is that for each ListGroup we add, we are subscribing to the GroupExpanded and GroupCollapsed events sourced by each individual ListGroup. This is mainly because there are use cases in which we might want to limit group expansion to a single group at a time, such that expanding one group collapses any other expanded group. This is accomplished by providing the boolean SingleItemOnlyExpansion property. The GroupListControl_ControlAdded method checks the state of this property, and if true, collapses any expanded groups which are not equal to (as in, referencing the same object instance as) the current group (the “sender” in the method’s signature).

The last thing to note is the manner in which we set the width of each ListGroup in the GroupListControl_ControlAdded method. I tried setting the Dock property instead, and ran into difficulties with that.

Given the code above, you would think all that was pretty simple, no? Yeah. Right. A problem arose in the form of ugly scrollbars. The code above will run, and do everything represented. However, for the GroupedListControl to look like anything other than ass, we need to do something about the horizontal scrollbar which appears at the bottom of the GroupedListControl , due to the width of each ListGroup being essentially the same as the container control. This, I must say was initially giving me pains. The FlowLayoutPanel does not, apparently, afford us the ability to control the appearance of the horizontal and vertical scrollbars individually.

Some research on the interwebs yielded, after no small amount of digging, the following solution. Sadly, it involves Windows messages and API calls, neither of which I am particularly well-versed in. More sadly, I seem to have misplaced the link to where I found the solution. If YOU know where the concept below came from, please forward me a link, so I can link back, and attribute properly.

Add the following code to the end of the GroupedListControl class:

Handling Scrollbars by Intercepting Windows Messages:

private enum ScrollBarDirection
{
    SB_HORZ = 0,
    SB_VERT = 1,
    SB_CTL = 2,
    SB_BOTH = 3
}

protected override void WndProc(ref System.Windows.Forms.Message m)
{
    // Call to unmanaged WinAPI:
    ShowScrollBar(this.Handle, (int)ScrollBarDirection.SB_HORZ, false);
    base.WndProc(ref m);
}

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool ShowScrollBar(IntPtr hWnd, int wBar, bool bShow);

The above code essentially listens to windows messages, and when it “hears” one related to showing scrollbars in the FlowLayoutPanel base class, performs the appropriate action. in this case, some sort of WinAPI magic related to NOT showing the horizontal scrollbar.

Note that we WANT the vertical scrollbar to show up, anytime the height of the collected ListGroups exceeds the height of the GroupedList client area. But I decided I would prefer to have the horizontal scrolling option available within each individual ListGroup where needed, without the extra screen clutter of another horizontal scrollbar a the bottom of the container control.

Scroll Bars in the Grouped List Control (note horizontal scroll in individual ListGroup, and vertical scroll for container control . . .)

Gl Demo 5 Vert and Horiz Scroll

Summary

What we have done to this point is examine the core essentials of creating a composite control which provides some very basic behaviors I needed for a project at work. Some things to remember:

  • The code in this and the previous post is somewhat abbreviated. For example, there are a number of overloads for the Add() method on both the ListViewItemCollection and the ListViewColumnCollection which we did not address here. They are, however, mostly addressed in the Source Code on Github. I will say not all the overloads have been properly tested.
  • Another requirement I had for my control was the ability to detect Right-Mouse-Clicks on the column headers in each individual GroupedList. This capability is not built into the Listview control, and in fact it was a bit of an exercise to make it happen. More adventures with external calls to the WinApi. I will likely examine this in my next post.
  • Populating the GroupedList control takes only a little more thought and planning that doing the same with a regular Listview. In many ways, it is akin to populating a two-tiered Treeview control. The Example project in the source code repo demonstrates this in a very, very basic way. I know thus far it has met my own needs rather nicely. I needed to make a large amount of data available to the user with a minimal number of clicks, and with minimal return trips to the database.
  • I would love to hear about improvements, and especially where I have done something dumb. I am here to learn, so bring it. Feel free to fork the source, and please do put in a pull request for any changes or improvements you make.

I will try to follow up with a post about adding Right-Click detection for the ListGroup column Headers in a day or two. This enables us to deploy a different ContextMenuStrip when the user right-clicks on a columnheader vs. the standard context menu for the ListView Control.

Thanks for reading do far . . .

CodeProject
Managing Nested Libraries Using the GIT Subtree Merge Workflow
CodeProject
Git: Interactively Stage Portions of a Single Changed File for Commit Using git add -p
ASP.Net
Configuring Db Connection and Code-First Migration for Identity Accounts in ASP.NET MVC 5 and Visual Studio 2013
  • jatten

    jattenjatten

    Author Reply

    Hi!

    I hadn't thought about that, but actually, runtime editing of sub-items within a listview (which is what you want to do, if I understand you correctly) is not supported directly within the listview control. I requires some calls to the WinApi.

    I may do an update to this to include it. In the meantime, there is a very informative post on this at the Code Project site: http://www.codeproject.com/Articles/6646/In-place-editing-of-ListView-subitems


  • Shyam

    ShyamShyam

    Author Reply

    Hi Sudly,

    In the demo project, I am not able to edit the listview items. In my case, I need two columns, the first being the 'name' (non-editable) and the second column is the 'value' (editable). How to make it editable at runtime ??


  • jatten

    jattenjatten

    Author Reply

    I hate recaptcha as well, but I gotta do something to fend off the spam . . .


  • jatten

    jattenjatten

    Author Reply

    I just updated the Github repo. Thanks for pointing that out. Not sure what I did wrong there, but I believe it is fixed now. Please let me know if you have any other problems, and ESPECIALLY if you find ways to improve the code! :-)


  • Dmitry

    DmitryDmitry

    Author Reply

    Thanks, sudly but solution in latest commit in is broken, there is no some of resources and no Form1 with example, I think you commit not all files.

    ps, hate recaptcha