Menu

Adding buttons to a Windows Forms ListView

February 2, 2016 - .NET, C#, Programming

A Demo Project covering the content of this post can be downloaded Here.

Sometimes, a standard Windows.Forms.ListView just doesn’t cut it. Sometimes you just want to present more information, or customize the way the information is presented. With Windows Presentation Foundation, you typically accomplish this with Data Templates. Unlike Windows Forms adding a Progress bar to a ListView, for example, is fairly straightforward, since WPF effectively allows elements to be used as content anywhere.

One feature that sometimes finds itself to be useful is the addition of a Standard button within a ListView subitem. Generally speaking, the preferred approach is to use a right-click menu, but depending on the context of the application and the intended userbase, there could be a different set of expectations. Furthermore, a button in a subitem is much more clear with regards to the possible functionality then hiding a capability behind a otherwise invisible right-click context menu.

In the particular case that I found purpose for this ability, was for a invoice/finance related task. In particular, when voiding/nullifying an invoice, users had to provide a reason for the invoice to be voided. In the application that lists invoices, it made sense to make this reason available, but the maximum length of such reasons made it untenable to simply add a new column, and as I mentioned, placing it in a right-click menu was rejected because users simply wouldn’t know to try that to find new features. As a result, the consensus was to try to add a button within the subitem to allow viewing the Reason, and only make that button visible where a reason was present.

For obvious reasons I cannot share the actual code involved, but since it was specifically tailored to that application, it wouldn’t be particularly useful anyway. It makes more sense when sharing code such as this to provide a more generic solution, so I set about creating a more generic implementation.

The Windows Forms ListView

As I mentioned in the introductory paragraph, the Windows Forms ListView is not as flexible as the Windows Presentation Foundation ListView. The reason for this is largely a result of Windows Forms being a Framework built in .NET that abstracts the standard Windows API. There are some advantages to this approach, in particular Windows will be more easily inspectable by accessibility programs. (without additional work). One of the disadvantages is that Windows Forms inherits the limitations of those original controls. One of these limitations is that, by default, the ListView itself really only knows about drawing small images and text for various subitems. If you want to do something fancier, then you have to do it yourself.

Owner-draw

One common extensibility approach that is taken throughout the Win32 API is the concept of “owner-draw”. This is a capability whereby the control effectively defers to the Owner (eg. your code) to perform the task of drawing. This provides a rather copious amount of flexibility in terms of the presentation, without making the control logic itself too complicated, so from a design perspective it made sense.

Taking the above into account, we can see a clearer picture of how we can have a button in a ListView’s subitem. Clearly, the solution is that we take over the drawing of that subitem, and we draw a button. But now we have a new problem- How do we draw the button?

We could mimic a button control by drawing a square with some text in the middle, but that seems like a cop-out- If we draw a button, we want it to look like all the other buttons in the User Interface. But we also aren’t about to commit to a study of the particulars of the buttons on various Windows Platforms and their appearance. Thankfully, the Win32 API does in fact provide us a solution- well, two solutions, really. We can use functions in uxtheme to draw themed components, and- if theming is off, for some reason- we can fallback to using DrawEdge.

The Native Methods

As per standard .NET Convention, we’ll slap out Native methods into an appropriate “NativeMethods” class. I’ve opted to have the class as a private class within the larger class (ListViewButton) such that a single source file can be included in projects to use the capability. But of course different classes can easily be separated into their own files. Here is the NativeMethods class:

Additionally, we will want to define some enumerations and constants:

And now, we can write the code to Draw the button. Naturally, we make it a separate routine, rather then embedding it into, say, the ListView Ownerdraw method like some sort of uncultured savage:

Alright, so now we have some logic to draw the button. For convenience, we’ll plop the functionality into a class, that can be used at run-time to basically say “hey, Class instance, draw a button for this column alright? In the code this is accomplished by Storing state information in a Dictionary which indexes Column information by column index, and the column information class has it’s own item information which maps a ListView Item to the appropriate button state information for the button, which includes information such as whether to draw the button at all. We can map an X-coordinate in the ListView’s client area to an appropriate column by taking the columns in display order and finding the column where it “crosses over” width wise. This can be done by ordering the columns by their display index. I took a rather straightforward/basic approach and just go through them and add them to an int=>int Dictionary, then return the sorted values:

The column information has it’s own dictionary associating ListView Items, which is used to retrieve the appropriate state information. We can determine the “Row” position fairly easily by using the ListView’s HitTest and then using that ListItem (if any) as the indexer into this dictionary paired with the determined column. This allows us to take a mouse position and determine what “button” it is over and react accordingly.

For mousing over a button, we want to have it show the “Hot” button image. Furthermore, we of course also want to have any other “Hot” button stop being hot. Since a Windows system can only have one mouse, we can presume there can only ever be one hot button and one pressed button, so we’ll store the button information for the pressed and hot buttons into an instance variable. On Mouse Over, if we detect the mouse is not over the hot button, we’ll change the current hot button back to normal; if it is over a button, we then change that to the hot button. For added effect, we also unpress the pressed button while the mouse is not over the button, to emulate a typical Windows Button control. Mouse Up will only activate the event for the button- fired by the helper class, and passing along information about the listview item and the column index clicked – if the mouseup event occurs over the pressed button. Lastly, there is some helper logic where the MouseLeave event is hooked to make sure there are no buttons pressed or hot when the mouse leaves the listview.

One interesting caveat I found is that if FullRowSelect and MultiSelect are off, the ListView appears to fire a MouseUp event immediately after the MouseDown event; It’s unclear if this can be fixed through code, but I wasn’t able to do so; it’s something to keep in mind. Of course, with Multiselect on, the selection rectangle looks a tad wonky with the buttons there, too.

Have something to say about this post? Comment!