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:
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()
{
this.FlowDirection = System.Windows.Forms.FlowDirection.TopDown;
this.AutoScroll = true;
this.WrapContents = false;
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)
{
ListGroup expanded = (ListGroup)sender;
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)
{
}
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)
{
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 . . .)
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 . . .