. . . Or, why the STL ALT.NET User Group kicks so much ass.
One of the best moves I ever made was getting involved with a developer user group, or “meet up” as they are sometimes known. Here’s why.
I am a self-taught developer, having been touched by the progressive disease of code addiction a few years back, in conjunction with a project at work. I have had to learn most of this addictive trade the hard way, e.g. long bouts of going down the wrong road to a solution, and hours spent surfing the web, seeking solutions to problems which, for an experienced dev, would be rudimentary.
In all this, I have learned much.
One of the biggest lessons I learned is, hijack the education and experience of others, by whatever means necessary. The other thing I learned is the old saw “when all you have is a hammer, everything looks like a nail.” I’ll come back to this in a minute.
I had lived in Portland, Oregon for most of the past nine years. As my coding addiction grew worse and worse, I found myself joining some local user groups, and regularly attended the PADNUG (Portland Area Dot Net Users Group, of course). I found here a group of like-minded individuals, who all spoke the same language (that would be C#, for the most part, but I will generalize and use “.NET” to represent all the .NET languages – yes, I KNOW .NET is a framework, not a language, but this is MY blog, and I can call things what I want!).
I moved, rather suddenly, from Portland to St. Louis, Missouri in October of 2011. Before I was on the ground more than 24 hours in my new neighborhood of Clayton, I was scouring the inter-webs for the St. Louis version of the PADNUG. I found the Saint Louis ALT.NET group. Upon following the Google link to the group’s home page, the very first thing I saw was THIS:
“The ALT.NET community is a loosely coupled, highly cohesive group of like-minded individuals who believe that the best developers do not align themselves with platforms and languages, but with principles and ideas. “
See my previous note about hammers and nails.
I loved this. As I continued my scan of the page, I found the following list, describing the “Alt.Net philosophy” espoused by David Laribee:
The alt.net Philosophy
“1. You're the type of developer who uses what works while keeping an eye out for a better way.
2. You reach outside the mainstream to adopt the best of any community: Open Source, Agile, Java, Ruby, etc.
3. You're not content with the status quo. Things can always be better expressed, more elegant and simple, more mutable, higher quality, etc.
4. You know tools are great, but they only take you so far. It's the principles and knowledge that really matter. The best tools are those that embed the knowledge and encourage the principles (e.g. Resharper.)
The STL ALT.NET Meetup
The St. Louis ALT.NET meetup group is a place where .NET developers can learn, share, and critique approaches to software development on the .NET stack. We cater to the highest common denominator, not the lowest, and want to help all St. Louis .NET developers achieve a superior level of software craftsmanship.”
THIS was the group I had been looking for. While I had enjoyed the single-minded focus at the PADNUG group, I found the above ideas refreshing. As soon as I was settled (and learned my way around St. Louis!) I attended my first meet up, How Ruby is Making Me a Stronger C# Developer and a Better Man, presented by Darren Cauthon. The group itself was welcoming, and relaxed. Pizza and refreshments were on hand, and the monthly meet up is a Bring-Your-Own-Beer type of after-work affair.
LOVED IT.
The next month was a presentation by Jessica Kerr on Powerful Pattern Matching in F# / UX Techniques. Loved this too.
Since joining the STL ALT.NET group, I have been consistently impressed with the organization of the meet ups, and the energy with which Nicholas Cloud, the group’s principal and most visible organizer keeps the group moving. In keeping with my personal quest to expand my know-how, steal the hard-won education of others, and broaden my technological horizons, this was exactly the group I needed.
New Challenges to Face
It has been difficult for me to attend the group in the past few months, as a result of a heavy work travel schedule. I am hoping to get back in the swing of things in July now though, because I believe that participation in user groups (especially good ones) is of prime benefit to both the individual and the developer community. Even more, I believe that maximum benefit is obtained through consistent participation, and making a contribution. Even now, I am trying to find a topic on which I might present to the group. For me, the challenge in this is there is very little I might share with the experienced pros that they don’t already know. But I am going to try, BECAUSE of the challenge this represents.
Why YOU should Join a User Group
If you are a new and inexperienced programmer just starting to figure things out, or if you are an experienced pro who has never attended a user group or meetup, I strongly recommend you GET OFF YOUR ASS AND JOIN ONE. Yes, the initial social interaction thing is uncomfortable for most of us (we are, after all, geeks, right?), but there are few things which can bring a new vigor to your work more effectively than “getting involved” and “Giving back.”
And if you are in the Saint Louis area, I strongly recommend dropping into the STLALT.NET group. There will be pizza. You can bring beer. You WILL learn something. Plus, you probably have an education and professional experience I can steal while you are there.
If you are not in the St. Louis Area, or this specific group is not what you are looking for, you can most likely find one that IS what you seek HERE: Meetups in your area
If you are intrigued by the STLALT.NET group, you can find more info HERE: STL ALT.NET
You can follow the group on Twitter at : @stlaltdotnet
John on Google
I love my iPad. Truly, it covers nearly all the bases very, very well. Email-on-the-go. Surfing the web. Streaming Netflix and other video content that is not Flash-dependent. I thought the iPad would be awesome for reading as well. I mean, swiping page-to-page on a beautiful hi-res screen (in color), and most importantly, having instant access to an enormous library right from a single tablet device.
I was wrong. I do NOT enjoy reading on the iPad. Under ideal conditions it's ok, but:
- It becomes hard on the eyes after just a short while. The backlit display makes extended reading difficult.
- Reading on iPad is out of the question in the great outdoors. The high-gloss touch display surface is simply too reflective, and is like looking into a mirror on anything but the darkest of cloudy days.
More to the point, I have long been one of those crusty hold-outs for whom a printed book, made of paper (an "analog" reading experience, if you will) was the only way to go. The iPad, for all its wonder in other areas, only seemed to confirm this.
Two things encouraged me to check out the Kindle. First, the idea of having my book collection in my pocket, much like my music collection on my iPod, held a lot of appeal. I am an inveterate reader, and it is rare you might find me out and about without my latest book somewhere on my person. In the same vein, since I do most of my shopping online anymore, the idea of instant gratification in the form of immediate digital delivery from Amazon to my device was also appealing.
The other thing was some reading I did online about people who reluctantly gave the humble-looking Kindle a try, and had nothing but good things to say by the time they had finished their first book.
Alright dude, so tell us about it then
Ok. So having decided to purchase a Kindle, I decided on the Kindle Touch
Wi-Fi version. While the 3G edition comes with free 3G connectivity, it costs $50.00 more, and I am rarely so far from decent Wi-Fi that I would use this feature anyway.
The Kindle Touch is roughly 5in x 7in, with a single textured button on the front face just under the screen. In the center of the bottom edge of the device are a mini USB connector port, a 1/8' stereo mini port for headphones, and the power button. Headphones, you say? Yes. Headphones. More on that in a bit.
The Kindle weighs just 7.5 ounces, or 1/2 pound, and is .40in (between 1/4 and 1/2 inch) thick. It is a very compact, lightweight device, and feels quite sturdy, although in contrast to a new phone or tablet, the look and feel
are somehow disappointing. I write this up to context. We’re not buying a tablet or a phone. Amazon claims the Kindle touch will hold roughly 3000 books, or 3GB of user content. However, this is supplemented by the fact that any content purchased from Amazon will also be stored in the Amazon cloud, available as-needed.
I mentioned earlier that the Kindle does not look or feel like your typical high-tech, “touch-enabled” device. When I thought about it for a minute, I can see why this is a positive, not a negative. Kindle is supposed to replace your books. We want to be comfortable using Kindle in the same context as we might a paperback novel. In other words, I am glad the thing doesn’t feel or look like something I would be afraid to take camping.
Display
The biggest difference you will notice when you power on a Kindle is that the screen looks, well, rather dull, and there is no backlighting. In fact, at first it looks a little like an Etch-a-Sketch on steroids (if you are too young to know what an Etch-a-Sketch is, ask your mom or dad). Believe it or not, this is by design, and once you use Kindle for a few, you will begin to get it. Remember me mentioning the strain on the eyes form the backlit display on my iPad? Right. The Kindle display is optimized for reading print. In black and white and gray. In fact, the Kindle is designed to mimic, to the degree possible, the experience of reading from a page. This works well, but is contrary to what I intuitively expect from a high-tech touch-enabled device. I have to continually remind myself that the Kindle is not trying to be a tablet computer or smart phone.
Kindle utilizes a technology called "e-ink" to achieve this. Remember this, because it affects much of the Kindle experience. The display on the Kindle touch boasts a pixel resolution of 600 x 800 at 176 ppi, so text renders very crisply and clean, with no "pixelization", against a roughly off-white background. In natural lighting, this turns out to be very easy on the eyes. However, the e-ink technology appears to use a different screen refresh mechanism, and this makes interacting with the screen seem sluggish when compared to my other touch devices. Kindle can also render 16 levels of gray scale. This affords the Kindle some limited graphic display capability. For most illustrations this is sufficient. However, don't plan on gasping with delight over a coffee table book of fine art.
Battery Life
Because Kindle does not employ backlighting for its display, and requires minimal processing power, battery life on a Kindle is a world apart from more familiar portable devices. A single charge on a Kindle will last anywhere from 30 to 60 days, depending upon your reading habits, and your use of wireless. The Kindle will charge fully in approximately 4 hours, using the included USB 2.0 cable. An AC charger is sold separately, but for most, simply connecting the Kindle to a USB port on your computer will do just fine.
Content Formats
Kindle supports content in the following formats natively: Kindle (AZW), TXT, PDF, Audible (Audible Enhanced(AA,AAX)), MP3, unprotected MOBI, PRC.
Kindle also supports the following through a conversion process: HTML, DOC, DOCX, JPEG, GIF, PNG, BMP through conversion. This is one of the cool things I did not know about the Kindle until after I bought one. More on this in a bit.
Wait. Did I say "MP3 in that list above? Why, yes I did. As it turns out, the Kindle support audio playback (they call it an "experimental" feature). Yes, you can plug in headphones, and play your favorite background music while reading, if you are so inclined.
Kindle will Read to Me
Another feature is Text-to-Speech, by which Kindle can read aloud to you. While not for everyone, this may come in handy for those who enjoy audio books. Also note that not all content allows this feature. Having played with it briefly, I can say that beyond a certain "wow" factor, I would not be likely to use it. There is too much in reading that is dependent upon what I will call "situational implied vocal inflection" that is not present when the machine reads back in a monotone. Might be good for educational or technical content, however.
Content Management
Here is where the Kindle really begins to sell itself. First off, you can fit a LOT of books on a Kindle. Like, 3,000 or so. Essentially, anything you know you’ll want to read, plus a bunch of stuff you think you MIGHT want to read, as well as anything else you might keep on hand for reference only. You can keep your entire library on here.
Then there is the dubious blessing of shopping for reading materials. Amazon has made it so damn easy to browse, find, and buy new reading material that, without some discipline, one could quickly go bankrupt. The best way to go about it is to shop on your Home PC (or laptop), select the titles you want, and click the “One-Click” purchase button. Voila – Instantly delivered via wireless (or 3G) to your Kindle. Done. I should also mention that for the moment, anyway, books on Kindle tend to cost significantly less than their analog (paper) counterparts. Partly this is due to the reduced production costs (no paper. No binding. No shipping, etc.). Also, Amazon has been pushing titles at low cost in an effort to increase adoption of the eReader in general, and Kindle specifically. If you can cut the cord with your paper fetish, you can get a lot more book-bang for your buck with Kindle.
New release novels and such (likely available only in hardback at the bookstore) tend to run between $5.00 and $15.00. Titles available in stores as paperback run the gamut from $2.99 up to $8.00. In any case, Kindle prices tend to undercut paper by a significant margin.
On top of all this, your Kindle purchases are always available in the cloud from your Amazon account. Once you have downloaded them to your kindle (and/or PC/MAC/iPAD etc.) they also remain on Amazon’s servers, just in case. Further, when you feel the need, there are Kindle apps for just about every device out there, including (as mentioned) PC, MAC, iOS devices, Android, etc. Your content is available for all of them.
Convert PDF Files to Kindle Format
Amazon has created a conversion feature, by which you can send a .pdf to your Kindle as an email attachment, adding “CONVERT” to the subject line. In this case, your .pdf file will be converted to a Kindle-format document. Amazon considers this to be an experimental feature. Some .pdf’s render to Kindle format better than others, and the process is not always 100% successful. It is handy though, and I suspect, if Amazon can work out the kinks, this will be a winner.
Using the Kindle Touch
I mentioned earlier that the e-ink display utilized by kindle does not perform in the manner we associate with “touch-enabled” devices (think iPad here). This is important to keep in mind when reading on Kindle. The upside of e-ink is that it renders beautifully for reading print, under almost any lighting condition. The downside is that the display mechanism is completely different from back-lit displays used by iPads and smart phones. There is no cutesy “swiping” to turn pages, and no accompanying visual page-turn effect. With Kindle Touch, you touch the screen, and the next page renders. To go back, you touch the screen close to the left-hand edge of the screen.
There is also a very, very noticeable latency to the touch response, particularly when typing on the pop-out QWERTY keyboard. While the keyboard contextually as needed (much like other mobile devices) there is a noticeable (and frustrating) lag between touching a key, and the on-screen response. This is probably the most disorienting thing about the Kindle in general, in that we have come to think of “touch” devices from the tablet and phone perspective. If you keep in mind that Kindle is not trying to be either of those, it will help. At least until Amazon catches the e-ink technology up.
Beyond the kludgy touch I/O, I have found that Kindle has completely taken over as my reading tool of choice. Most of my friends and family know that I am a consummate nerd. For most of my life, it has been a rare day that I have not had a book tucked into a satchel or pocket nearby, just in case I have to kill a minute or two waiting in line, or for a table. I am now sold on the eReader format. When I come across a tome that I simply MUST own, I will follow up an buy the analog version (usually in hardback). But for most things, having my digital library available, anywhere, on a whim, is more than good enough.
John on Google
A work in Progress
It becomes more and more apparent each year that the historical frameworks governing copyrights and patents in this country (if not the world) has become strained under advances in modern technology, and particularly, the internet. The digital millennium has changed the face of content creation, distribution, and consumption to such a degree that legislation governing copyright, infringement, and plagiarism can scarcely keep up.
One of the principal tools in use today in the United States is the Digital Millennium Copyright Act (DMCA), passed into law in October of 1998. We are all at least somewhat familiar with the Digital Millennium Copyright Act. At least, we know it has to do with “protecting” copyrighted works online, and that it allows online content to be “removed with” the simple issuance of a “takedown” notice by the party alleging infringement, usually to the hosting entity.
In broad strokes, the DCMA requires that, upon receipt of an infringement notice, the Internet Service provider or hosting site must block or remove public access to the alleged infringing material in order to be protected from liability under “safe harbor” rules built into the DMCA. In the meantime, the content host generally notifies the user or customer who posted the content, who is then free to file a counter-notice establishing that the content does not, in fact, infringe as claimed.
The above description is intentionally broad and vague, because I am not an expert in copyright or the DMCA. In addition, more detailed information abounds online. You can start HERE if you want more information. In short, the DMCA was a step forward in that it established explicit measures protecting internet service providers and content hosting sites from being held liable for infringing content uploaded by users.
Big Media, for the Takedown
I am proposing that the DMCA has potential to be a reasonable compromise in terms of bringing copyright enforcement kicking and screaming into the modern age (ok, at least to the eighties or nineties). The problem at present is that, like many things, the current implementation is one-sided in the extreme, unduly favoring the plaintiff, which is most often a heavily-lawyered, well-healed large corporation. Essentially, all one need do to get somebody’s content blocked, even temporarily, is issue the takedown notice. It then becomes the problem of the content publisher to demonstrate that the posted content does not, in fact, infringe upon the plaintiff’s work.
Certain major media companies have taken to issuing indiscriminant take down notices, apparently using internet search algorithms and “web crawling” technology to identify potentially infringing content. In some cases, it appears that content is targeted based upon keywords, titles, or other criteria which may well identify infringing works, but also a good many other sites which are not in fact plagiarizing or pirating the work of others. Often acting through surrogates, big media companies seem to favor a shoot first, ask questions later approach, issuing the take down notices indiscriminately. The current law provides no penalty for this, and the result is that, if YOUR content happens to contain the right combination of keywords, your content might be yanked, pending resolution through a counter notice.
Balancing the Equation – a Common-Sense Solution
I propose that the DMCA be modified to penalize those who issue a takedown notice which turns out to be invalid. If someone is going to claim MY content infringes upon their previously published work, they should be damn sure that it does if they are going to step on my livelihood. Just as in civil court, if I sue you, and I lose, I will be required to pay your attorney’s fees and other legal costs. Under the modification I propose, at the very least, those who are found to have issued a bogus or insufficiently researched takedown notice should have to pay for my costs to get my content restored, plus any lost revenue arising from the period during which my content was off line. Other penalties should also be levied, since at present, there seems to be no mechanism to regulate the profligate issuance of these takedown notices in a slash-and-burn manner by big media companies.
I am very much against plagiarism and piracy. I think that those who seek to profit by illegally distributing copyrighted materials, and/or through plagiarism, should be punished accordingly. But I don’t feel that the laws by which this is accomplished are balanced fairly at the moment, and the situation is skewed too far in the direction of the big publishers.
Don’t steal. Don’t plagiarize. And don’t throw out the baby with the bathwater.
We can do better.
John on Google
The GroupedList Control Container
CodeProject
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()
{
// 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);
}
/// <summary>
/// Handles the ControlAdded Event for the current instance.
/// </summary>
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);
}
/// <summary>
/// Gets or Sets a boolean value indicating whether multiple ListGroups
/// may be in the expanded state at the same time. When set to true, the current expanded
/// ListGroup is collapsed when a new ListGroup is expanded.
/// </summary>
public bool SingleItemOnlyExpansion { get; set; }
/// <summary>
/// Handles the Expanded event for the current instance.
/// </summary>
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);
}
}
/// <summary>
/// Handles the Collapsed event for the current instance.
/// </summary>
void lg_GroupCollapsed(object sender, EventArgs e)
{
// No need.
}
/// <summary>
/// Expands all listgroups contained in the current instance.
/// </summary>
public void ExpandAll()
{
foreach (ListGroup lg in this.Controls)
{
lg.Expand();
}
}
/// <summary>
/// Collapses all ListGroups contained in the current instance.
/// </summary>
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:
/// <summary>
/// Consumed by the Win API calls below:
/// </summary>
private enum ScrollBarDirection
{
SB_HORZ = 0,
SB_VERT = 1,
SB_CTL = 2,
SB_BOTH = 3
}
/// <summary>
/// Disables the horizontal scrollbar in the primary container control.
/// Individual ListGroups within the GroupList have their own scrollbars
/// if needed.
/// </summary>
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);
}
/// <summary>
/// Imported from WinAPI: Method to control Scrollbar visibility.
/// </summary>
[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 . . .
John on Google
NOTE: This post is kinda long. However, most of the length is a result of code postings (even after removing some extra stuff). Bear with me!
CodeProject
I’ve been deep in a project for work for the past two months. Sadly, it is nothing sexy, no exciting bleeding-edge technology, just another enterprise database, using the very mature and slightly dull Winforms library in the .NET platform.
However, I did stumble across an interesting project requirement for what is essentially an expandable group of the venerable Listview control, what one might get if one combined a Listview with a Treeview, or if the “groups” built into the Listview control could be expanded/collapsed, right-clicked, etc.
Fig. 1 – Single Group Expanded:

Fig. 2 – Multiple Groups Expanded (Note Scrollbar on Container Control):

Fig. 3 – On Widen Column (Note Scrollbar on Specific ListGroup):

For those who are about to point out that such a control exists in the ObjectListView, I am aware. However, I needed to do this using standard .NET Libraries. I am also aware that the standard Listview Group can be forced to expand/Collapse, but I needed this to be a faster solution. Also, causing the standard Listview Group to expand/collapse looked to rely on a whole lot of Windows API calls, and I am not so fluent in that arcane area.
My solution was to extend the Listview control, and then assemble multiple Listview controls within a FlowLayoutPanel control. The ColumnHeaders of each Listview double as the “Group.” Clicking on the left-most column toggles group expansion/collapse. The expanded/collapsed state is indicated by a solid arrow image at the left end of the column. In cases where the group is empty (containing no ListViewItems, and looking for all the world like a collapsed group) the arrow image is empty.
For the purpose of clarity, I refer to the aggregate control as a “GroupedListControl",” and each contained ListView as a “ListGroup".” There is a wide potential for improvement in this naming scheme, I am sure. For the purpose of this narrative, assume the following:
- A GroupedListControl Contains one or more ListGroups, which contain ListViewItems.
- The GroupedListControl is a container which inherits from FlowLoyoutPanel.
- The ListGroup is a container which inherits from the Winforms ListView.
Extending Native Listview Behaviors with Inner Classes
First, I needed to extend some of the basic behaviors of the stock .net Listview control. For example, in order to treat the leftmost column (Column [0]) differently, and to monitor the addition and removal of columns for the purpose of controlling and adjusting for the appearance of scrollbars, I needed an event to be fired when columns are added and removed. I did a little digging on the interwebs, and found my solution in a post on the Code Project site. I was able to take the core concept there and achieve what I needed:
A basic list of desired behaviors for each ListGroup include:
- Clicking on the leftmost ColumnHeader of a ListGroup should toggle the expansion/collapse of the group.
- When the first column is added, the Expanded/Collapsed indicator arrows should be added to the leftmost ColumnHeader.
- If the total width of the columns in any given ListGroup exceed the width of the client area of the containing GroupedListControl, the ListGroup should show a horizontal scrollbar.
- The height of each ListGroup should be adjusted such that all Listview items contained should be displayed in the expanded state, up to an optional maximum height determined either at design-time or runtime. If the number of ListViewItems contained exceeds this maximum, the Individual ListGroup Vertical Scrollbar will appear.
- Any time the Horizontal Scrollbar is displayed for a specific ListGroup, the client area for that ListGroup should be adjusted such that the Horizontal scrollbar does not partially obscure the last displayed ListViewItem.
- Detect Mouse Right-Clicks on specific ListView ColumnHeaders and allow for a context menu specific to right-clicking on ColumnHeaders vs. ListView Items.
This covers some minimums for the control to function properly. Let’s look at what a basic code skeleton would look like here. We will fill in some of the empty code stubs a little later in the post.
Complete source code for this project is available at GroupedListControl Project Source Code On GitHub. The source includes additional code not covered here. We will discuss some of it in upcoming posts.
First, I defined some Custom Event Argument Classes which will be utilized within the control. While they are functionally similar to some existing ListView Event Argument classes, I wanted to maintain clear naming to the degree possible. These are required by subsequent code.
Note that all examples require the following references at the head of your code file:
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Windows.Forms.Layout;
using System.Runtime.InteropServices;
using System.ComponentModel;
Custom Event Arguments:
public class ListGroupColumnEventArgs : EventArgs
{
public ListGroupColumnEventArgs(int ColumnIndex)
{
this.ColumnIndex = ColumnIndex;
}
public ListGroupColumnEventArgs(int[] ColumnIndexes)
{
this.ColumnIndexes = ColumnIndexes;
}
public int ColumnIndex { get; set; }
public int[] ColumnIndexes { get; set; }
}
public class ListGroupItemEventArgs : EventArgs
{
public ListGroupItemEventArgs(ListViewItem Item)
{
this.Item = Item;
}
public ListGroupItemEventArgs(ListViewItem[] Items)
{
this.Items = Items;
}
public ListViewItem Item { get; set; }
public ListViewItem[] Items { get; set; }
}
Now, the ListGroup Class itself. I have cut out some additional methods for the sake of brevity here (even at that, it’s a long chunk of code . . .), and left some code stubs to be filled in shortly. This is just to give an idea of the most basic class structure, and core functionality requirements. As it is right here, this code is not functional.
The ListGroup Class – Essentials and Code Stubs
public class ListGroup : ListView
{
// DELEGATES AND ASSOCIATED EVENTS:
// NOTE: The events and delegates related to Column and Item Addition/Removal are
// called by the inner classes ListGroupColumnCollection and LIstGroupItemCollection.
// The proper function of the control depends upon these.
// Delegates to handle Column addition and removal Events:
public delegate void ColumnAddedHandler(object sender, ListGroupColumnEventArgs e);
public delegate void ColumnRemovedHandler(object sender, ListGroupColumnEventArgs e);
// Events related to Column Addition and removal:
public event ColumnAddedHandler ColumnAdded;
public event ColumnRemovedHandler ColumnRemoved;
// Delegates to handle Item Addition and Removal events:
public delegate void ItemAddedHandler(object sender, ListGroupItemEventArgs e);
public delegate void ItemRemovedHandler(object sender, ListGroupItemEventArgs e);
// Events related to Item Addition and Removal:
public event ItemAddedHandler ItemAdded;
public event ItemRemovedHandler ItemRemoved;
// Delegate and related events to process Group Expansion and Collapse:
public delegate void GroupExpansionHandler(object sender, EventArgs e);
public event GroupExpansionHandler GroupExpanded;
public event GroupExpansionHandler GroupCollapsed;
// Delegate and related Events to handle Listview Header Right Clicks:
public delegate void ColumnRightClickHandler(object sender, ColumnClickEventArgs e);
public event ColumnRightClickHandler ColumnRightClick;
// PRIVATE INSTANCES OF INNER CLASSES
// Instances of our inner classes, declared as private members so
// that our public Property accessors can be Read-only, yet allow
// direct manipulation from within the control instance
private ListGroupItemCollection _Items;
private ListGroupColumnCollection _Columns;
/// <summary>
/// Constructor Stub
/// </summary>
public ListGroup() : base()
{
// Implementation Code . . .
}
// INNER CLASS INSTANCE ACCESSORS:
/// <summary>
/// Hides the ListViewItemCollection internal to the base class,
/// and uses the new implementation defined as an inner class,
/// which sources an "ItemAdded" Event:
/// </summary>
public new ListGroupItemCollection Items
{
get { return _Items; }
}
/// <summary>
/// Hides the ColumnHeaderCollection internal to the base class,
/// and uses the new implementation defined as an inner class,
/// which sources a "ColumnAdded" Event:
/// </summary>
public new ListGroupColumnCollection Columns
{
get { return _Columns; }
}
// INNER CLASS DEFINITIONS:
/// <summary>
/// Inner class used to hide the ListViewColumnHeaderCollection
/// built in to the ListView Control and provide required extended behaviors.
/// </summary>
public class ListGroupColumnCollection : ListView.ColumnHeaderCollection
{
// Implementation Code Here . . .
}
/// <summary>
/// Inner class defined for ListGroup to contain List items.
/// Derived from ListViewItemCollection and modified to source events
/// indicating item addition and removal.
/// </summary>
public class ListGroupItemCollection : ListView.ListViewItemCollection
{
// Implementation Code Here . . .
}
// ITEM ADDITION AND REMOVAL:
/// <summary>
/// Raises the ItemAdded Event when a new item is
/// added to the items collection.
/// </summary>
private void OnItemAdded(ListViewItem Item)
{
// Code to set the size of the control to display all the items
// so far . . .
// Raise the ItemAddded event to any subscribers:
if (ItemAdded != null)
this.ItemAdded(this, new ListGroupItemEventArgs(Item));
}
/// <summary>
/// Raises the ItemRemoved Event when an item is
/// removed from the items collection.
/// </summary>
private void OnItemRemoved(ListViewItem Item)
{
// Code to set the size of the control to display all the items
// remaining after the current one is removed . . .
// Raise the ItemRemoved event to any subscribers:
if (ItemRemoved != null)
this.ItemRemoved(this, new ListGroupItemEventArgs(Item));
}
// COLUMN ADDITION AND REMOVAL:
/// <summary>
/// Raises the ColumnAdded Event when a new column is
/// added to the ColumnHeaders collection.
/// </summary>
private void OnColumnAdded(int ColumnIndex)
{
// Code to manage column additions. The first column added
// needs to have the Expand/Collapse/Empty image added . . .
// Raise the ColumnAdded event to any subscribers:
if (this.ColumnAdded != null)
this.ColumnAdded(this, new ListGroupColumnEventArgs(ColumnIndex));
}
/// <summary>
/// Raises the ColumnRemoved Event when a column is
/// remmoved from the ColumnHeaders collection.
/// </summary>
private void OnColumnRemoved(int ColumnIndex)
{
// Code to manage column removals . . .
// Raise the ColumnRemoved event to any subscribers:
if (this.ColumnRemoved != null)
this.ColumnRemoved(this, new ListGroupColumnEventArgs(ColumnIndex));
}
// USER ACTIONS:
/// <summary>
/// Handles the ListGroup ColumnClick Event sourced by the base.
/// </summary>
void ListGroup_ColumnClick(object sender, ColumnClickEventArgs e)
{
// Code to manage ColumnHeader Clicks. If the Event is sourced from
// the first Column (Column[0], toggle expansion/collapse of the list . . .
}
// CONTROL BEHAVIORS:
/// <summary>
/// Causes the list of items to expand, showing all items in the
/// Items collection.
/// </summary>
public void Expand()
{
// Do stuff to make the control expand . . .
// Raise the Expanded event to notify client code that the ListGroup has expanded:
if (this.GroupExpanded != null)
this.GroupExpanded(this, new EventArgs());
}
/// <summary>
/// Causes the Displayed list of items to collapse, hiding all items and
/// displaying only the columnheaders.
/// </summary>
public void Collapse()
{
// Do stuff to make the control collapse
// Raise the Collapsed event to notify client code that the ListGroup has expanded:
if (this.GroupCollapsed != null)
this.GroupCollapsed(this, new EventArgs());
}
}
Now, pay close attention to the code stubs where we define our Inner Classes, ListGroupColumnCollection and ListGroupItemCollection. Note that each derives from its respective counterpart in the Winforms ListView Control. This is where we achieve a number of our custom event sourcing behaviors. Once again, I have simplified the class definitions here, leaving out various overloads of the core methods required (for example, there are multiple ways to “Add” an item to either collection – here I only cover the most basic. The rest are defined in the GroupedListControl Project Source Code, obtainable from my GitHub Repo).
Notice how the code within each inner class causes events to be raised within the containing ListGroup class? This provides our event sourcing for the addition/removal of Columns and ListViewItems, since these events are not defined in the base Winforms ListView Class. We need them in order to affect proper control expansion and collapse in response to additions and removals
NOTE: Core Concepts for the use of Inner Classes in this manner was adapted from THIS ARTICLE by Simon Segal on Code Project.
The following code replaces the code stub for the ListGroupColumnCollection Class in our ListGroup class definition:
The ListGroupColumnCollection Class
/// <summary>
/// Inner class defined for ListGroup to contain ColumnHeaders.
/// Derived from ListView.ColumnHeaderCollection and modified to
/// source events indicating column addition and removal.
/// </summary>
public class ListGroupColumnCollection : ListView.ColumnHeaderCollection
{
// Reference to the containing ListGroup Control
private ListGroup _Owner;
public ListGroupColumnCollection(ListGroup Owner) : base(Owner)
{
_Owner = Owner;
}
/// <summary>
/// Gets the total width of all columns currently defined in the control.
/// </summary>
public int TotalColumnWidths
{
get
{
int totalColumnWidths = 0;
foreach(ColumnHeader clm in this)
totalColumnWidths = totalColumnWidths + clm.Width;
return totalColumnWidths;
}
}
/// <summary>
/// Adds a column to the current collection and raises
/// the OnColumnAddedEvent on the parent control.
/// </summary>
public new ColumnHeader Add(string text, int width, HorizontalAlignment textAlign)
{
ColumnHeader clm = base.Add(text, width, textAlign);
_Owner.OnColumnAdded(clm.Index);
return clm;
}
/// <summary>
/// Removes a column from the current collection and
/// raises the OnColumnRemoved Event on the parent control.
/// </summary>
public new void Remove(ColumnHeader column)
{
int index = column.Index;
base.Remove(column);
_Owner.OnColumnRemoved(index);
}
public new void Clear()
{
base.Clear();
}
} // ListGroupColumnCollection
The following code replaces the empty stub for the ListGroupItemCollection class in our original ListGroup class definition:
The ListGroupItemCollection Class
/// <summary>
/// Inner class defined for ListGroup to contain List items. Derived from ListViewItemCollection
/// and modified to source events indicating item addition and removal.
/// </summary>
public class ListGroupItemCollection : System.Windows.Forms.ListView.ListViewItemCollection
{
private ListGroup _Owner;
public ListGroupItemCollection(ListGroup Owner) : base(Owner)
{
_Owner = Owner;
}
/// <summary>
/// New implementation of Add method hides Add method defined on base class
/// and causes an event to be sourced informing the parent about item additions.
/// </summary>
public new ListViewItem Add(string text)
{
ListViewItem item = base.Add(text);
_Owner.OnItemAdded(item);
return item;
}
/// <summary>
/// New implementation of Remove method hides Remove method defined on base class
/// and causes an event to be sourced informing the parent about item Removals.
/// </summary>
public new void Remove(ListViewItem Item)
{
base.Remove(Item);
_Owner.OnItemRemoved(Item);
}
} // ListGroupItemCollection
Filling it all in
The most basic extension of the existing capability of the standard ListView control we are seeking is the ability for each ListGroup to “expand” and/or “collapse” in response to certain user inputs. We will want to define a singular method call which causes the desired action to be performed, so we’ll add a method to our ListGroup class named SetControlHeight. This method evaluates the current state of the control (collapsed or expanded), and calls the appropriate method to toggle that state to the opposite.
The minimum collapsed height of each ListGroup control should be just enough to display the Column Headers. The Expanded height may be unlimited, or may be constrained by setting the MaximumHeight property. In either case, however, the control height should include enough space to display the Column Headers, and an even number of ListViewItems such that the last item is fully visible in the display.
The collapsed state is recognizable if the height of the control is equal to the height of the Column Headers and the item count is greater than 0. Otherwise, the control must be in an expanded state. I had to arbitrarily set the header height as a constant which matches the default header height for the standard ListView Control (25). However, this can be modified to suit.
In order to accomplish all of the above, we will need to define a few more private members in our ListGroup Class. Add the following code in the declaration area of your class (I am still a little old-school, in that I place most of my declarations at the top of the class, just after the class declaration itself). In the Source file, the following appear just before the Constructor:
Additional Member Declarations:
// Text strings used as Image keys for the expanded/Collapsed image in the
// left-most columnHeader:
static string COLLAPSED_IMAGE_KEY = "CollapsedImage";
static string EXPANDED_IMAGE_KEY = "ExpandedImageKey";
static string EMPTY_IMAGE_KEY = "EmptyImageKey";
// "Magic number" approximates the height of the List View Column Header:
static int HEADER_HEIGHT = 25;
We also need a Constructor at this point. Note that the Constructor initializes the ListView.SmallImageList with some images stored in the project resources (Properties.Resources). The images are included with the GroupedListControl Project Source Code. Replace the Constructor code stub with the following:
The Constructor:
public ListGroup() : base()
{
this.Columns = new ListGroupColumnCollection(this);
this.Items = new ListGroupItemCollection(this);
// The Imagelist is used to hold images for the expanded and contracted icons in the
// Left-most columnheader:
this.SmallImageList = new ImageList();
// The tilting arrow images are available in the app resources:
this.SmallImageList.Images.Add
(COLLAPSED_IMAGE_KEY, Properties.Resources.CollapsedGroupSmall_png_1616);
this.SmallImageList.Images.Add
(EXPANDED_IMAGE_KEY, Properties.Resources.ExpandedGroupSmall_png_1616);
this.SmallImageList.Images.Add
(EMPTY_IMAGE_KEY, Properties.Resources.EmptyGroupSmall_png_1616);
// Default configuration (for this sample. Obviously, configure to fit your needs:
this.View = System.Windows.Forms.View.Details;
this.FullRowSelect = true;
this.GridLines = true;
this.LabelEdit = false;
this.Margin = new Padding(0);
this.SetAutoSizeMode(AutoSizeMode.GrowAndShrink);
this.MaximumSize = new System.Drawing.Size(1000, 2000);
// Subscribe to local Events:
this.ColumnClick += new ColumnClickEventHandler(ListGroup_ColumnClick);
this.ItemAdded += new ItemAddedHandler(ListGroup_ItemAdded);
}
Now we can add code to manage the re-sizing of the control in response to user actions. While we have existing stubs for the Expand() and Collapse() methods because these formed obvious behaviors for our control, the next two will have to be added. The SetControlHeight() method is our one-stop call to adjust the height of the control:
The SetControlHeight Method:
/// <summary>
/// Adjusts the item display area of the control in response to changes in the
/// expanded or collapsed state of the control.
/// </summary>
public void SetControlHeight()
{
if (this.Height == HEADER_HEIGHT && this.Items.Count != 0)
this.Expand();
else
this.Collapse();
}
Add the above, along with the next three methods to our ListGroup Class. The next three methods actually perform the heavy lifting in terms of adjusting the control expanded/collapsed state. The first is a function which returns the proper control height after evaluating several factors (explained in the comments). We don’t have a stub for this in our existing structure, so add it right under the SetControlHeight method:
PreferredControlHeight Function:
private int PreferredControlHeight()
{
int output = HEADER_HEIGHT;
int rowHeight = 0;
// determine the height of an individual list item:
if(this.Items.Count > 0)
rowHeight = this.Items[0].Bounds.Height;
// In case the horizontal scrollbar makes an appearance, we will
// need to modify the height of the expanded list so that it does not
// obscure the last item (default is 10 px to leave a little space
// no matter what):
int horizScrollBarOffset = 10;
// if the Width of the columns is greater than the width of the control,
// the vertical scroll bar will be shown. Increase that offset height by the
// height of the scrollbar (approximately the same as the height of a row):
if (this.Columns.TotalColumnWidths > this.Width)
horizScrollBarOffset = rowHeight + 10;
// Increase the height of the control to accomodate the Columnheader,
// all of the current items, and the value of the
// horizontal scroll bar (if present):
output = HEADER_HEIGHT + (this.Items.Count) * rowHeight
+ horizScrollBarOffset + this.Groups.Count * HEADER_HEIGHT;
return output;
}
Then replace the Expand() and Collapse() code stubs with the following. While the PreferredControlHeight function provides the optimal height for a ListGroup, the Expand and Collapse methods perform the requested action and also cause the Expanded/Collapsed/Empty images to display properly in the left-most column:
The Expand and Collapse Methods:
/// <summary>
/// Causes the list of items to expand, showing all items in the
/// Items collection.
/// </summary>
public void Expand()
{
if (this.Columns.Count > 0)
{
this.Height = this.PreferredControlHeight();
if (this.Items.Count > 0)
// Set the image in the first column to indicate an expanded state:
this.Columns[0].ImageKey = EXPANDED_IMAGE_KEY;
else
// Set the image in the first column to indicate an empty state:
this.Columns[0].ImageKey = EMPTY_IMAGE_KEY;
this.Scrollable = true;
// Raise the Expanded event to notify client code
// that the ListGroup has expanded:
if (this.GroupExpanded != null)
this.GroupExpanded(this, new EventArgs());
}
}
/// <summary>
/// Causes the Displayed list of items to collapse, hiding all items and
/// displaying only the columnheaders.
/// </summary>
public void Collapse()
{
if (this.Columns.Count > 0)
{
this.Scrollable = false;
// Collapse the ListGroup to show only the header:
this.Height = HEADER_HEIGHT;
if (this.Items.Count > 0)
// Set the image in the first column to indicate a collapsed state:
this.Columns[0].ImageKey = COLLAPSED_IMAGE_KEY;
else
// Set the image in the first column to indicate an empty state:
this.Columns[0].ImageKey = EMPTY_IMAGE_KEY;
// Raise the Collapsed event to notify client code that
// the ListGroup has expanded:
if (this.GroupCollapsed != null)
this.GroupCollapsed(this, new EventArgs());
}
}
Wiring it all up
Now we need to wire up behaviors (Expand/Collapse) to the appropriate events. Some of these are obvious. When the user clicks on the left-most column (with the Expanded/Collapsed state image), the control should toggle this state. However, we also need the control to adjust its displayed area (and possible toggle the state image) when items are added/removed, and when columns are added/removed (because when columns are added, there may be a need to add the state image to the first column).
First, we will address Column addition/removal. When a column is added to the control, if it is the FIRST column, it will need the initial state image added, and the control will need to size itself. Since it is the first column, it is reasonably safe (but not 100%) to assume that there have been no items added yet, so the ListGroup is empty, and the state image should reflect this.
Add the highlighted items to the OnColumnAdded() Method:
private void OnColumnAdded(int ColumnIndex)
{
if (ColumnIndex == 0){this.Columns[0].ImageKey = EMPTY_IMAGE_KEY;this.SetControlHeight();}
if(this.ColumnAdded != null)
this.ColumnAdded(this, new ListGroupColumnEventArgs(ColumnIndex));
}
What happens if the ListGroup is populated with items, and the last column is removed? I don’t have a good answer for this, other than to clear the ListItems. It seems to me that the control loses its identity and purpose. If you have thoughts about this, please do discuss in the comments, or fork the code on Github. In any case, when removing columns, we need to test and see if the column removed is the last column in the control. If so, call the Clear() Method:
private void OnColumnRemoved(int ColumnIndex)
{
if (this.Columns.Count == 0){this.Items.Clear(); }
// Raise the ColumnRemoved event to any subscribers:
if (this.ColumnRemoved != null)
this.ColumnRemoved(this, new ListGroupColumnEventArgs(ColumnIndex));
}
Now, the final piece in our simplified ListGroup is the whole thing where the user clicks on the left-most column (Column[0]), and is rewarded by the control expanding or collapsing. The state image provides a sort of visual cue/affordance to the user which implies the current state. All we have to do is handle the ColumnClick event (sourced by the base class ListView) and we’re done with this part of our (abbreviated) control
Replace the ListGroup_ColumnClick code stub with the following:
Handling the ColumnClick Event:
void ListGroup_ColumnClick(object sender, ColumnClickEventArgs e)
{
int columnClicked = e.Column;
// The first column (Column[0]) is what activates the expansion/collapse of the
// List view item group:
if (columnClicked == 0)
{
this.SuspendLayout();
this.SetControlHeight();
this.ResumeLayout();
}
}
Summing Up Part I
Sadly, I need to break this project up into two parts. Even this one is too long (although much of the length is simply code samples.
In this first post, we have examined extending the Winforms ListView class so that it can serve as a component within a container control. We have also examined extending the events sourced by the ListView control through the use of inner classes, used to extend the ListViewColumnCollection and ListViewItemCollection classes.
In the next post, we will fold our ListGroup class into the container GroupedListControl. IN doing so , we will need to make a few calls to the Windows API. I hate it when that happens, but so it is (I hate it because I am not well-schooled in the Win32 API, so that kind of thing is HARD for me!). After that, we will examine some special methods to source a custom Context Menu specific to right-clicks on the List Group column headers, and a few other interesting tidbits which were necessary to make the overall control work properly.
Report Bugs. Submit Improvements. Do Good. Help Me Get Better!
I will try to get the next post up is a day or two. In the meantime, if you find over bugs in the code, or see areas for improvement in the overall implementation, please report bugs (in the comments here, or on Github), and feel free to fork the source and submit pull requests for improvements. I have said many times, I need all the help I can get!
Referenced in This Article:
John on Google