5,449,204 members and growing! (20,955 online)
Email Password   helpLost your password?
Desktop Development » List Controls » ListView controls     Intermediate License: The GNU General Public License (GPL)

A Much Easier to Use ListView

By Phillip Piper

This article describes a much easier to use ListView that supports sorting and grouping.
C# 2.0, C#, Windows, .NET, .NET 2.0VS.NET2003, VS2005, Visual Studio, Dev

Posted: 17 Oct 2006
Updated: 24 Jul 2008
Views: 231,607
Bookmarked: 466 times
Announcements
Want a new Job?



Search    
Advanced Search
Sitemap
180 votes for this Article.
Popularity: 10.80 Rating: 4.79 out of 5
4 votes, 2.2%
1
1 vote, 0.6%
2
0 votes, 0.0%
3
17 votes, 9.6%
4
156 votes, 87.6%
5

A much easier to use ListView...

Screenshot - ObjectListView.jpg

... And the report that comes from it.

Screenshot - ReportModernExample.jpg

Foreword

All projects suffer creeping featurism. Things that start simple and elegant end up as the "before" image in a weight-loss ad. This control has grown considerably since it was first written. For those in a hurry, this control has the following major features:

  • It can easily display a list of objects in a ListView, including automatically sorting and grouping.
  • It has a version (FastObjectListView) that can build a list of 10,000 objects in less than 0.1 seconds. [v1.9.1]
  • It can easily edit the values shown in the ListView [v1.8].
  • It can trivially produce nice reports from the ListView [v1.7].
  • It supports data binding.
  • It supports millions of rows through ListView's virtual mode.
  • It supports all ListView views (report, tile, large and small icons).
  • It supports owner drawing, including rendering animated GIFs.
  • Its columns can be fixed-width or limited to a minimum/maximum.
  • It shows a "list is empty" message when the list is empty (obviously).
  • Its row height can be explicitly set.
  • It supports user selection of visible columns by right clicking on the header [v1.9].
  • It supports columns that automatically resize to fill any unoccupied width [v1.10].

This control has many features. If you want to do something with a ListView, this code probably has some code to help you do it.

This control now has its own website, hosted by SourceForge: ObjectListView - How I Learned To Stop Worrying and Love .NET's ListView. This is not an empty shell site. It actually has lots of useful information.

Those who aren't in a hurry can now read the rest of the article. :-)

Introduction

Larry Wall, the author of Perl, once wrote that the three essential character flaws of any good programmer were sloth, impatience and hubris. Good programmers want to do the minimum amount of work (sloth). They want their programs to run quickly (impatience). They take inordinate pride in what they have written (hubris). It is in the interests of encouraging "slothfulness" that I wrote this control.

The ListView "Problem"

I often find that I have a collection of objects which I want to present to the user in some sort of tabular format. It could be the list of clients for a business, a list of known FTP servers or even something as mundane as a list of files in a directory. User interface-wise, the ListView is the perfect control for these situations. However, I find myself groaning at the thought of using the ListView and secretly hoping that I can use a ListBox instead.

The reason for wanting to avoid the ListView is all the boilerplate code it needs to work: make the ListViewItems, add all the SubItems, catch the header click events and sort the items depending on the data type. Each of these tasks is slightly different for each instance of a ListView. If you want to support grouping, there's an even bigger chunk of boilerplate code to copy and then modify slightly.

For a basically lazy person, this is far too much work. ObjectListView was born to relieve this workload.

First Steps

To use an ObjectListView in your project:

  1. Add ObjectListView.cs to your project
  2. Make sure your project has references to:
    • System.Data
    • System.Design
    • System.Drawing
    • System.Windows.Forms (obviously)
  3. Build your project

Once your project has been built, there should now be a new section in your Toolbox, "[YourProjectName] Components". In that section should be ObjectListView and its friends. You can then drag an ObjectListView onto your window, and use it as you would a standard ListView control.

Unlearn You Must

For those of you who have struggled with a ListView before, you must unlearn. An ObjectListView is not a drop in replacement for a ListView. If you have an existing project, you cannot simply create an ObjectListView instead of creating a ListView. An ObjectListView operates in a more declarative manner than a ListView -- you tell an ObjectListView what you want it to do, and it does it for you.

Beware of ListViewItems. You never need to add ListViewItems to an ObjectListView. If you find yourself adding things to the Items collection, creating ListViewItems, or adding sub-items to anything, then you need to stop — you are being seduced to the dark side. An ObjectListView does all that work for you. You tell it the aspects you want to show on each object (via the OLVColumn objects) and then you give it the list of objects to show.

Resist the temptation to add ListViewItems -- it will not work.

There is also no need to hide information in a ListViewItem. Old style ListView programming often required attaching a key of some sort to each ListViewItem, so that when the user did something with a row, the programmer would know which model object that row was related to. This attaching was often done by creating one or more zero-width columns, or by setting the Tag property on the ListViewItem. With an ObjectListView, you do not need to do this anymore. The ObjectListView already know which model object is behind each row. In many cases, the programmer simply uses the SelectedObjects property to find out which model objects the user wants to do something to.

Using the Code

"Simple things should be simple. Complex things should be possible." The major design goal of ObjectListView was that it should make the programmer's life simpler. Usually the control is configured within the IDE and then, with a single line of code, it is put into action like this:

this.objectListView1.SetObjects(allPeople);

And that is it!

Simple, fast, uncomplicated, non-fattening and without a single line of boilerplate code. Without further work, this ListView will handle column-click sorting and data formatting, as well as grouping and possibly editing too. The "Simple Example" tab in the demo project shows what is possible with only IDE configuration and this one line of code.

Simplicity of use is also the reason why I implemented all of the classes in one file. It's much easier to drop one file into a new project than it is to add 4 or 5 files. It's also impossible to get your file versions out of sync if there is only one of them.

Adding Complexity: Images

That is all well and good, but real-world applications need more than just sorting and grouping.

The obvious first enhancement to this simple example is to display images in the ListView. As with most of the customization of ObjectListView, this is done by installing a delegate. If the word "delegate" worries you, think of them as function pointers where the parameter and return types can be verified. To show images against a column, you need a method that matches the ImageGetterDelegate signature. That is, it takes a single object parameter and returns an int. A somewhat frivolous example follows:

int PersonColumnImageGetter (object rowObject)
{
    // People whose names start with a vowel get a star,
    // otherwise the first half of the alphabet gets hearts
    // and the second half gets music
    Person p = (Person)rowObject;
    if ("AEIOU".Contains(p.Name.Substring(0, 1)))
        return 0; // star
    else if (p.Name.CompareTo("N") < 0)
        return 1; // heart

    else
        return 2; // music
};

To install it, you do this:

this.personColumn.ImageGetter = new
    ImageGetterDelegate(this.PersonColumnImageGetter);

.NET 2.0 added the convenience of anonymous delegates. In an anonymous delegate, the code for the function is inlined, like this:

this.personColumn.ImageGetter = delegate (object rowObject)
{
    Person p = (Person)rowObject;
    if ("AEIOU".Contains(p.Name.Substring(0, 1)))
        return 0; // star

    else if (p.Name.CompareTo("N") < 0)
        return 1; // heart
    else
        return 2; // music
};

Anonymous delegates save you from having to add lots of little methods to your class. However, if the anonymous delegates start to become too big or if you find yourself copying them verbatim from one place to another, it's a good sign that you need to put some new methods on your model class.

[1.3] ImageGetters can now return an int, string or an Image itself. If it returns an int or a string, these are used as indices into the image list. If it returns an Image, the image will be drawn. However, drawing an Image only works in OwnerDrawn mode.

Other Customizations

Delegates can also be used to customize:

  • The way the aspect is extracted from the model object of the row. If there is no method or property on the model that will give the data that's required, you can install an AspectGetter delegate to calculate the information.
  • The way the aspect is converted to a string. Without an installed delegate, this is done using the String.Format() method. The delegate can do whatever it likes.
  • The way the group key for this row is calculated. A group key is simply a value used to partition all of the model objects into groups. All of the model objects with the same group key are put into the same group.
  • The way a group key is converted into a group title.
  • The way edited values are written back into the model objects, using the AspectPutter delegate[v1.8].

The complex example tab in the demo project has examples of using all of these delegates. For example, turn on "Show Groups" and click on the "Cooking Skill" or "Hourly Rate" column to see what's possible.

Data Unaware

The control is written to be data-agnostic. It doesn't care what type of data it is working on. The only requirement is that the object passed to the SetObjects method must support the IEnumerable interface, which isn't too heavy a requirement. This means that it works equally well with an ArrayList, a DataTable or a list of compiler errors returned from CompilerResults.Errors. For example, to display information from DataTable, you could install AspectGetter delegates like this:

columnName.AspectGetter =
    delegate(object row) { return ((DataRow)row)["Name"]; };
columnCompany.AspectGetter =
    delegate(object row) { return ((DataRow)row)["Company"]; };
columnAge.AspectGetter =
    delegate(object row) { return ((DataRow)row)["Age"]; };

Then install the table like this:

this.objectListView1.SetObjects(ds1.Tables["Persons"].Rows);

Note that this code installed the Rows, not the DataTable itself.

[v1.3] A Little More Data Aware

In response to intense public demand -- OK, a couple of people asked for it -- I've added in v1.3 a DataListView class. This is a data-bindable version of ObjectListView. DataListView accepts various kinds of data sources and populates the list with data from that source.

For those programmers for whom even one line of code is too much, DataListView can be configured completely within the IDE. Give it a DataSource, set up the columns and it will simply work. DataListView also listens for ListChanged events on its data source and uses those to keep its list in sync with the contents of the data source. Add a new row to the data table and it will automatically appear in the ListView! The DataListView can accept several types of objects as data sources:

  • DataView
  • DataTable
  • DataSet
  • DataViewManager
  • BindingSource

To use DataListView, you give each column the name of the data column you want it to display in the AspectName property. Then set the DataSource member to your data source. That's it! Even more slothfulness! So, we could accomplish the same thing as the "data unaware" example above by configuring AspectNames for the columns and then setting the data table directly, like this:

this.dataListView1.DataSource = ds1.Tables["Persons"];

Alternatively, for the monumentally slothful, the whole thing could be done through IDE configuration. Set the AspectName property on the columns; set the DataSource property to the appropriate dataset and then set the DataMember property to tell which table from the dataset you want to show. Hey, presto, a fully functional data set viewer. All without writing a single line of code.

[v1.3] You Want How Many Rows in That List View?!

Also new in v1.3 is VirtualListView. If you've ever wanted to thoroughly overwhelm your users with 10 million rows of data, then go ahead and knock them out with VirtualListView.

Let's get this out of the way first: ListViews are NOT good interfaces for large number of items. If you are trying to show a ListView that has 10 million rows, you need to rethink your interface. However, if you really have to use a ListView with that many rows, then VirtualListView is your answer.

Normal ObjectListViews keep a list of model objects that they can read, sort or group at will. VirtualListViews do not keep such a list. Instead, they only fetch model objects when they need to be displayed. For large lists, this is a massive reduction in resources. If the user never looks at the 4-millionth row, the ListView will never ask for it, so the program will never have to create it.

The downside to virtual lists is that they cannot do anything that requires all the items to be present. They cannot sort items. They cannot make rows into groups. They cannot find rows by typing into the list. They cannot use Tile view (not sure why this is but Microsoft enforces it). Programmatically, they cannot use any flavour of SelectObject, since the control has no way of knowing which row is showing which model object. However, if you are desperate to show your users 10 million full-text search results, these will be considered as minor inconveniences.

To make VirtualListView work, you have to tell the ListView how many rows it has via the VirtualListSize property. Then you must install a delegate that will be called whenever a particular model object is needed. VirtualListView will use that delegate to fetch only the 50 model objects that the user can currently see.

As just mentioned, virtual ListViews do not support grouping or sorting, since that would defeat the entire purpose of making them virtual! There is nothing we can do about the grouping, but we do have a way around the lack of sorting. If you have a backing store that can sort your 10 million rows, you can install a CustomSorter delegate that will be called when the user wants to sort the ListView. The delegate can do whatever is necessary to sort the data and then call BuildList() to show the newly sorted data to the user. The code to set up VirtualListView looks like this:

listViewVirtual.VirtualListSize = 10000000;
listViewVirtual.RowGetter = delegate(int i)
{
    return GetNthMailingAddressFromStorage(i);
};
listViewVirtual.CustomSorter = delegate(OLVColumn column, SortOrder order)
{
    SortMailingAddressBy(column.AspectName, order);
    this.listViewVirtual.BuildList();
};

CustomSorter is optional. However, VirtualListSize and RowGetter are not.

[v1.6] Fixed-width and Restricted-width Columns

Sometimes it makes no sense to allow the user to resize a column. A column that simply shows a 16x16 status icon has no reason to be resizable. Extending this idea one step further, you can imagine cases where a column should not be less than a given size or wider than a maximum size. So, it would be good if we could give columns a minimum and a maximum width. Setting the same value for both would give a fixed-sized column.

However, controlling the resizing of columns turns out to be a non-trivial problem. It is easy to find examples of fixing the width of all columns in a ListView: Chris Morgan has a nice implementation available here. Unfortunately, that technique cannot be used to restrict individual column widths. In fact, I could not find any example anywhere of how to restrict a column width to be within a given range.

Regardless, as of v1.6, columns can now be given a MinimumWidth and a MaximumWidth. Even within the IDE, these settings will prevent the user from setting the width of the column to outside of the given values. See below for a more detailed discussion of the complexities and potential limitations of my implementation.

[v1.7] Reports from ListViews

Example of ListViewPrinter output

Now that you've gone to all that work to make a very pretty ListView, wouldn't it be nice if you could just print it? Yes, I know there is always the PrntScrn key, but I have noticed that some upper management do not think very highly of that as a reporting solution.

The ListViewPrinter is the answer to your printing problem. Configure an instance of it in the IDE (the ListView property controls which list is printed) and then call:

this.listViewPrinter1.PrintPreview();

Thus, for nothing you can have a very pretty report that looks like this one.

Admittedly, the formatting in this example is too much, but you can modify all the formatting to suit your tastes. See the demo for some more sedate examples and read the code to see how to make it work. It really is very cool.

It was also a logically separate piece of code, so I relented and put it into its own source file. If you want to use it, you must add the ListViewPrinter.cs file to your project. After that, you'll have to recompile before you can use a ListViewPrinter from within the IDE.

[v1.8] Cell Editing

ListViews are normally used for displaying information. The standard ListView allows the value at column 0 (the primary cell) to be edited, but nothing beyond that. As of v1.8, ObjectListView allows all cells to be edited. Depending on how the data for a cell is sourced, the edited values can be automagically written back into the model object.

The "editability" of an ObjectListView is controlled by the CellEditActivation property. This property can be set to one of the following values:

  • CellEditActivateMode.None - The ObjectListView cannot be edited (this is the default).
  • CellEditActivateMode.SingleClick - Subitem cells will be edited when the user single clicks on the cell. Single clicking on the primary cell does not start an edit operation - it selects the row, just like normal. Editing the primary cell only begins when the user presses F2.
  • CellEditActivateMode.DoubleClick - Double clicking on any cell, including the primary cell, will begin an edit operation. In addition, pressing F2 will begin an edit operation on the primary cell.
  • CellEditActivateMode.F2Only - Pressing F2 begins an edit operation on the primary cell. Clicking or double clicking on subitem cells does nothing.

Individual columns can be marked as editable via the IsEditable property (default value is true), though this only has meaning once the ObjectListView itself is editable. If you know that the user should not be allowed to change cells in a particular column, set IsEditable to false. Be aware, though, that this may create some UI surprises (resulting in complaints like "How come I can't edit this value by clicking on it like I can on all the other cells?"). You have been warned.

Once a cell editor is active, the normal editing conventions apply:

  • Enter or Return finishes the edit and commits the new value to the model object.
  • Escape cancels the edit.
  • Tab commits the current edit, and starts a new edit on the next editable cell. Shift-Tab edits the previous editable cell.

How are Cells Edited and How Can You Customise It

The default processing creates a cell editor based on the type of the data in the cell. It can handle bool, int, string, DateTime, float and double data types. When the user has finished editing the value in the cell, the new value will be written back into the model object (if possible).

To do something other than the default processing, you can listen for two events: CellEditStarting and CellEditFinishing.

The CellEditStarting event is triggered after the user has requested to edit a cell but before the cell editor is placed on the screen. This event passes a CellEditEventArgs object to the event handlers. In the handler for this event, if you set e.Cancel to true, the cell editing operation will not begin. If you don't cancel the edit operation, you will almost certainly want to play with the Control property of CellEditEventArgs. You can use this to customise the default editor, or to replace it entirely.

For example, if your ObjectListView is showing a Color in a cell, there is no default editor to handle a Color. You could make your own ColorCellEditor, set it up correctly, and then set the Control property to be your color cell editor. The ObjectListView would then use that control rather than the default one. If your cell editor has a read/write property called Value, ObjectListView will use that to get and put the cell value into the control. If it doesn't, the Text property will be used instead.

When the user wants to finish the edit operation, a CellEditFinishing event is triggered. If the user has cancelled the edit (e.g. by pressing Escape), the Cancel property will already be set to true. In that case, you should simply cleanup without updating any model objects. If the user hasn't cancelled the edit, you can by setting Cancel to true -- this will force the ObjectListView to ignore any value that the user has entered into the cell editor.

No prizes for guessing that you can refer to the Control property to extract the value that the user has entered and then use that value to do whatever she or he wants. During this event, you should also undo any event listening that you have setup during the CellEditStarting event.

Note: There is no way to prevent the cell edit operation from finishing (i.e. you cannot force the cell editor to stay on the screen).

You can look in the demo at listViewComplex_CellEditStarting() and listViewComplex_CellEditFinishing() to see an example of handling these events.

Updating the Model Object

Once the user has entered a new value into a cell and pressed Enter, the ObjectListView tries to store the modified value back into the model object.

There are three ways this can happen:

  • You create an event handler for the CellEditFinishing event, write the code to get the modified value from the control, put that new value into the model object, and set Cancel to true so that the ObjectListView knows that it doesn't have to do anything else. You will also need to call at least RefreshItem() or RefreshObject(), so that the changes to the model object are shown in the ObjectListView. There are cases where this is necessary, but as a general solution, it doesn't fit my philosophy of slothfulness.
  • You give the corresponding OLVColumn an AspectPutter delegate. If supplied, this callback will be invoked with the model object and the new value that the user entered. This is a neat solution.
  • If the column's AspectName is the name of a writable property, the ObjectListView will try to write the new value into that property. This requires no coding and certainly qualifies as the most slothful solution. But it only works if AspectName contains the name of a writable property. If the AspectName is dotted (e.g. Owner.Address.Postcode) only the last property needs to be writable.

If none of these three things happen, the user's edit will be discarded. The user will enter her or his new value into the cell editor, press Enter, and the old value will be still be displayed. If it seems as if the user cannot update a cell, check to make sure that one of the three things above is occurring.

[v1.9.1] A Much Faster ListView - the FastObjectListView

So far, ObjectListView has been catering to the slothful, those of us who want to do the least work and get the most results. If impatience is more your key character flaw, then the FastObjectListView is for you. In exchange for losing a few features, you gain a great amount of speed.

How fast is it? On my mid-range laptop, a normal ObjectListView builds a list of 10,000 objects in about 10-15 seconds. A FastObjectListView builds the same list in less than 0.1 seconds.

What do you lose? With a FastObjectListView, you cannot show groups and you cannot use Tile view. Apart from that, all features of ObjectListView are available in the FastObjectListView.

[v1.10] Auto-resizing Columns

There are situations where it would be nice if a column (normally the rightmost one) would expand as the listview expands, so that as much of the column was visible as possible without having to scroll horizontally (you should never, ever make your users scroll anything horizontally!). A free space filling column does exactly that. The "Comments" column in the Simple tab of the demo shows this in action.

When an ObjectListView is resized, the space occupied by all the fixed width columns is totalled. The difference between that total and the width of the control is then divided between the free space filling columns. If you only have one such column, it is given all the space.

Be a little cautious with these space filling columns. Their behaviour is not standard and can sometimes be quite surprising, especially if the columns aren't the right most columns. One surprise is that these columns cannot be resized by dragging their divider -- their size depends on the free space available in the listview.

By default, when the user is changing the width of a column by dragging its divider, space filling columns are not immediately resized. The space filling columns are resized when the user releases the mouse. If they are resized while the user is dragging a divider, it just looks too weird! If you really want to allow it, you can set UpdateSpaceFillingColumnsWhenDraggingColumnDivider to true, but you have been warned.

Interesting Bits of Code

Reflection

Reflection is used in a couple of ways: to get data from the model objects, to put data into cell editors, and to put data into the model objects.

Getting data from the model object uses reflection to dynamically invoke a method, property or field by its name.

protected object GetAspectByName(object rowObject)
{
    if (String.IsNullOrEmpty(this.aspectName))
        return null;

    BindingFlags flags = BindingFlags.Public | BindingFlags.Instance |
                         BindingFlags.InvokeMethod |
                         BindingFlags.GetProperty | BindingFlags.GetField;
    try
    {
        return rowObject.GetType().InvokeMember(this.aspectName,
            flags, null, rowObject, null);
    }
    catch (System.MissingMethodException)
    {
        return String.Format("Missing method: {0}",
            this.aspectName);
    }
}

Things to note in this code:

  • There is no point in trying to invoke anything if the aspect name is null or empty.
  • BindingFlags say to only invoke public instance methods, properties or fields. Static methods and protected or private methods will not be called.
  • The InvokeMember() method is called on the "type," not on the "object".
  • Catching MissingMethodException is necessary, since the method or property name could be wrong.

Reflection is easier to code, but you pay the penalty in speed. On my machine, reflection is 5-10x slower than using delegates. On a list of only 10-20 items, it doesn't matter. However, if your list has hundreds of items, it's worth installing AspectGetter delegates.

[1.3] This is actually slightly more complicated, since it now supports using a dot notation to access sub-properties. It is now valid to have several method or property names, joined by dots, as an aspect name. Each method or property name is de-referenced and the result of that de-referencing is used as the target for the next method or property. It's more intuitive to use than it is to explain :-)

So, Owner.Address.Postcode is a valid aspect name. This will fetch the Owner property from the initial model object and then ask that owner object for its Address. Then it will ask that address for its Postcode.

Putting a Value into a Cell Editor

When we put a cell editor onto the screen, we need to get the value from the cell and somehow give it to the control. Unfortunately, there is no standard way for giving a Control a value. Some controls have a Value property, which is exactly what we want, but others do not. Where there is Value property, we want to use it, but where there isn't, the best we can do is use the Text method.

protected void SetControlValue(Control c, Object value, String stringValue)
{
    // Look for a property called "Value". We have to look twice
    // since the first time we might get an ambiguous result
    PropertyInfo pinfo = null;
    try {
        pinfo = c.GetType().GetProperty("Value");
    } catch (AmbiguousMatchException) {
        // The lowest level class of the control must have overridden
        // the "Value" property.
        // We now have to specifically  look for only public instance properties
        // declared in the lowest level class.
        BindingFlags flags = BindingFlags.DeclaredOnly |
            BindingFlags.Instance | BindingFlags.Public;
        pinfo = c.GetType().GetProperty("Value", flags);
    }

    // If we found it, use it to assign a value, otherwise simply set the text
    if (pinfo == null)
        c.Text = stringValue;
    else {
        try {
            pinfo.SetValue(c, value, null);
        } catch (ArgumentException) {
            c.Text = stringValue;
        }
    }
}

So, what's going on here?

Firstly, we use GetProperty to try and get information about the Value property on the control. We have to allow for ambiguous matches, which will occur if the controls immediate class has overridden the base class's Value property. In that case, we use some BindingFlags to say that we want the Value property that was declared in the lowest level class. To any language lawyers, yes, I know it's not foolproof, but it works in almost all cases.

Once we have the property info, we can simply call the SetValue method. We have to catch the ArgumentException just in case the value can't be set.

If any of this has gone wrong, we simply use the Text method to put the value into the control and hope that it does what we want.

Showing List View Sub-item Images

ObjectListView was written to make a ListView easier to use, not to add swathes of new functionality. Initially, sub-item images were the only additional functionality. A plain vanilla ListView only supports images in the first column. ObjectListView doesn't have this restriction; any column can show images. To show images on sub-items, there are basically two strategies:

  1. Owner-draw the sub-items
  2. Force the underlying ListView control to draw them

Owner drawing is a can of worms that I did not want to open, so I initially chose the second option. The ListView control in Windows has the ability to draw images against sub-items, but that functionality was not exposed in .NET. We can send messages to the underlying ListView control to make it show the images. Remember that these tricks rely on the underlying ListView control, so they may not work in future versions of Windows. It's certain that they will not work on non-Microsoft platforms. To make the ListView control draw sub-item images, we need to:

  1. Set the extended style LVS_EX_SUBITEMIMAGES on the ListView control itself
  2. Tell the ListView control which image to display against which sub-item

Setting the extended style would be simple except that .NET doesn't expose the extended styles flag. So, we have to pull in the SendMessage() function and define the constants we want to use.

[DllImport("user32.dll", CharSet=CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg,
    int wParam, int lParam);

private const int LVM_SETEXTENDEDLISTVIEWSTYLE = 0x1000 + 54; // LVM_FIRST+54

private const int LVS_EX_SUBITEMIMAGES         = 0x0002;

Then, at some convenient point, you turn on the flag:

SendMessage(this.Handle, LVM_SETEXTENDEDLISTVIEWSTYLE,
    LVS_EX_SUBITEMIMAGES, LVS_EX_SUBITEMIMAGES);

This would be enough, except that .NET Framework erases all unknown extended styles when an extended style is set. Examples are FullRowSelect and GridLines. So, the above code will have to be called after all other initialization is complete.

Our second task is to tell the ListView control which sub-item will show which image. To do this, we need a new structure, LVITEM, and some more constants. We don't use most of the LVIF_ constants, but they're included for completeness.

private const int LVM_SETITEM = 0x1000 + 76; // LVM_FIRST + 76

private const int LVIF_TEXT        = 0x0001;
private const int LVIF_IMAGE       = 0x0002;
private const int LVIF_PARAM       = 0x0004;
private const int LVIF_STATE       = 0x0008;
private const int LVIF_INDENT      = 0x0010;
private const int LVIF_NORECOMPUTE = 0x0800;

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)]
private struct LVITEM
{
    public int     mask;
    public int     iItem;
    public int     iSubItem;
    public int     state;
    public int     stateMask;
    [MarshalAs(UnmanagedType.LPTStr)]
    public string  pszText;
    public int     cchTextMax;
    public int     iImage;
    public int     lParam;
    // These are available in Common Controls >= 0x0300
    public int     iIndent;
    // These are available in Common Controls >= 0x056
    public int     iGroupId;
    public int     cColumns;
    public IntPtr  puColumns;
};

We also need to import SendMessage a second time, but with a slightly different signature. We use the parameter EntryPoint to import a function using a name other than the C# function name.

[DllImport("user32.dll", EntryPoint="SendMessage", CharSet=CharSet.Auto)]
private static extern IntPtr SendMessageLVI(IntPtr hWnd, int msg,
    int wParam, ref LVITEM lvi);

Finally, we can set up the sub-item images using a method like this:

public void SetSubItemImage(int itemIndex, int subItemIndex, int imageIndex)
{
    LVITEM lvItem = new LVITEM();
    lvItem.mask = LVIF_IMAGE;
    lvItem.iItem = itemIndex;
    lvItem.iSubItem = subItemIndex;
    lvItem.iImage = imageIndex;
    SendMessageLVI(this.Handle, LVM_SETITEM, 0, ref lvItem);
}

In the above member, itemIndex is the 0-based index of the row in question. subItemIndex is the 1-based index of the sub-item and imageIndex is the 0-based index into the image list associated with the listview.

[v1.4] (Owner) Drawn and Quartered

Remember that can of worms I didn't want to open? Owner drawing the ListView? Well, one afternoon when I had too little to do (ha!), I decided that it really couldn't be too bad and I got out my can opener. Several evenings later, I could only confirm my original estimate: owner drawing is a can of worms. It should be easy. It should just work. But it doesn't.

Regardless, ObjectListViews can now be owner-drawn and it is owner drawing on steroids! Like most of ObjectListView, owner drawing is accomplished by installing a delegate. Inside the renderer delegate, you can draw whatever you like:

columnOD.RendererDelegate = delegate(DrawListViewSubItemEventArgs e,
    Graphics g, Rectangle r, Object rowObject)
{
    g.FillRectangle(new SolidBrush(Color.Red), r);
    g.DrawString(((Person)rowObject).Name, objectListView1.Font,
        new SolidBrush(Color.Black), r.X, r.Y);
}

Installing a delegate works fine, but there are numerous utility methods that are useful within such a delegate. Is the row currently selected? What colour should the background be? The BaseRenderer class encapsulates these utilities. To make your own Renderer class, you must subclass BaseRenderer, override the Render(Graphics g, Rectangle r) method and again draw whatever you like, only this time you have a lot of nice utility methods available to you. There are a couple of subclasses of BaseRenderer already available for use.

Class Purpose
BarRenderer This is a simple-minded horizontal bar. The row's data value is used to proportionally fill a "progress bar."
MultiImageRenderer This renderer draws 0 or more images based on the row's data value. The 5-star "My Rating" column on iTunes is an example of this type of renderer.
MappedImageRenderer This renderer draws an image decided from the row's data value. Each data value has its own image. A simple example would be a Boolean renderer that draws a tick for true and a cross for false. This renderer also works well for enums or domain-specific codes.
ImageRenderer This renderer tries to interpret its row's data value as an image. Most typically, if you have stored Images in your database, you would use this renderer to draw the images from the database.

To use any of these renderers or your own custom subclass, you assign an instance of them to a column's Renderer property, like this:

colCookingSkill.Renderer = new MultiImageRenderer(Resource1.star16, 5, 0, 40);

This means that the cooking skill column will draw up to 5 of the star16 images, depending on the data value. The renderer expects data values in the range 0 to 40. Values of 0 or less will not draw any stars. Values of 40 or more will draw 5 stars. Values in between will draw a proportional number of stars.

Things to Remember About Owner Drawing

Owner drawing only happens when you turn on the OwnerDrawn mode. So, you can only see your custom renderer when the ObjectListView is in owner-drawn mode.

Rows in list views are always of fixed height and calculated from the ListView font and/or the height of the image lists. [v1.5] Row height can now be set using the RowHeight property. Be aware that this feature should be considered highly experimental. Your program, dog and/or spouse may behave erratically when you use this property.

It is obvious, but easily overlooked, that owner drawing is slower than non-owner drawing. The Framework has to do a lot more work for owner drawing than it does for native drawing. Again, for small lists, the difference is not significant. However, it can be noticeable when a large number of redraws is necessary. For example, go to the "Virtual List" tab on the demo and drag the scroll thumb down to the bottom. Now turn on OwnerDraw and do it again. Quite a difference!

IDE Integration

Once we have our nice new UI widget, we still have one more important step: make it work within the IDE. The whole point of this ListView is that it should make the programmer's life easier. That means it has to integrate well with the development environment, which drops us into the scary world of attributes and metadata.

One problem with figuring out how to integrate with the IDE is that it is not well-documented. That is, some pieces are documented, but it is usually not clear what we should do with those pieces. You might read that you can use EditorAttribute to control how a particular property is edited, but it is hard to see how to use that information to put the right sort of editors onto your custom DataSource and DataMember properties.

That is where the quasi-magical Lutz Roeder's .NET Reflector is so useful. It disassembles the .NET libraries themselves, showing all the classes -- not just the public ones -- and all the methods of each class. It then reverse-engineers the source code for the methods. It's an amazing and amazingly useful piece of software. Using the Reflector, it turns out that the right incantation for our DataSource property is the relatively simple, if unintuitive:

[AttributeProvider(typeof(IListSource))]
public Object DataSource { ... }

However, for the DataMember property, we need to invoke this spell:

[Editor("System.Windows.Forms.Design.DataMemberListEditor, System.Design,
    Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a",
    typeof(UITypeEditor))]
public string DataMember { ... }

This has to be considered at least non-obvious given that DataMemberListEditor is not even mentioned in the SDK documentation. Below are some attributes that I found most useful:

Attribute What does it do? Example
Category When properties are categorized, to which category should this property belong? Category("Behavior")
Description What text should be displayed to the user to explain this property? Description ("Should
the list view show
images on subitems?"
)
DefaultValue If the user does not set this property, what value will it have? DefaultValue(false)
Browsable By default, all public properties are presented to the user for modification within the IDE. Set this to false to hide a property from the user. Browsable(false)
DesignerSerializationVisibility Even if the user can't see a public property, the IDE will still generate code to set it to its default value. This attribute hides the property from the serializer, so that no code is generated for this property. DesignerSerializationVisibility(
DesignerSerializationVisibility.
Hidden)
Editor How will this property be edited? Be warned: using this attribute properly is a mix of alchemy and astrology. Editor(typeof(MyCustomEditor),
typeof(UITypeEditor))

[v1.6] Implementing Restricted-width Columns

To limit the widths of columns, we have to find some way to intercept attempts to modify them. There are three UI mechanisms to change the width of a column:

  • Drag the divider with the mouse
  • Double-click the divider to autosize the column
  • Press Ctrl-NumPad+ to autosize all columns

Fortunately, all three of these mechanisms ultimately use the same HDN_ITEMCHANGING message. We only need to catch that message and everything should be fine. The first part of the solution requires the handling of WM_NOTIFY events, like this:

protected override void WndProc(ref Message m) {
    switch (m.Msg) {
        case 0x4E: // WM_NOTIFY
        if (!this.HandleNotify(ref m))
            base.WndProc(ref m);
        break;
    default:
        base.WndProc(ref m);
        break;
    }
} 

Then we can handle the HDN_ITEMCHANGING message. Note that we handle both ANSI and Unicode versions, even though the ANSI version is probably irrelevant. If the change would make the column wider or thinner than it should be, we simply veto the change by returning a result of 1.

private bool HandleNotify(ref Message m) {

    bool isMsgHandled = false;

    const int HDN_FIRST = (0 - 300);
    const int HDN_ITEMCHANGINGA = (HDN_FIRST - 0);
    const int HDN_ITEMCHANGINGW = (HDN_FIRST - 20);

    NMHDR nmhdr = (NMHDR)m.GetLParam(typeof(NMHDR));

    if (nmhdr.code == HDN_ITEMCHANGINGW) {
        NMHEADER nmheader = (NMHEADER)m.GetLParam(typeof(NMHEADER));
        if (nmheader.iItem >= 0 && nmheader.iItem < this.Columns.Count) {
            HDITEM hditem = (HDITEM)Marshal.PtrToStructure(
                nmheader.pHDITEM, typeof(HDITEM));
            OLVColumn column = this.GetColumn(nmheader.iItem);
            if (IsOutsideOfBounds(hditem, column) {
                m.Result = (IntPtr)1; // prevent the change
                isMsgHandled = true;
            }
        }
    }

    return isMsgHandled;
}

private bool IsOutsideOfBounds(HDITEM hditem, OLVColumn col) {
    // Check the mask to see if the width field is valid,
    if ((hditem.mask & 1) != 1)
        return false;

    // Now check that the value is in range
    return (hditem.cxy < col.MinimumWidth ||
           (col.MaximumWidth != -1 && hditem.cxy > col.MaximumWidth));
}

The solution does not appear very complicated. However, reality is rarely so simple. For example, those of you with some knowledge about the header control might be thinking, "Hey! What about HDN_TRACK and its friends? Why don't you do anything about them?"

Well, according to KB article #183258, Microsoft states that when a header control has the HDS_FULLDRAG style, it will receive HDN_ITEMCHANGING messages rather than HDN_TRACK messages. Since version 4.71 of common controls, header controls always receive the HDS_FULLDRAG style. So, still it seems that we only have to handle the HDN_ITEMCHANGING message.

The trouble is that this is not always true. Under XP SP2 (at least), header controls with the HDS_FULLDRAG style do not always send HDN_ITEMCHANGING messages rather than HDN_TRACK messages. This may be why Microsoft has withdrawn that particular KB article. On some machines, header controls send HDN_ITEMCHANGING events as they should, but on others, the header controls send the old sequence of messages: HDN_BEGINTRACK, HDN_TRACK, HDN_ENDTRACK, HDN_ITEMCHANGING, HDN_ITEMCHANGED.

After quite a bit of digging, the crucial setting seems to be the Explorer option "Show Window Contents While Dragging." In an example of "truly bizarre side effects," if this option is turned on, the header will send HDN_ITEMCHANGING messages instead of HDN_TRACK messages (as it should). However, if it is turned off, the header sends lots of HDN_TRACK messages and only one HDN_ITEMCHANGING message at the very end of the process.

Having two possible sequences of events complicates my simple plan. If the "Show Window Contents While Dragging" option is turned on, the current code works perfectly. If it is off, things are uglier.

On the whole, if we receive multiple HDN_TRACK messages and only one HDN_ITEMCHANGING message, it's harder to control the resizing process. The reason is that there is no way to cancel just one HDN_TRACK message. If we return a result of 1 from a HDN_TRACK message, we cancel the whole drag operation, not just that particular track event. From the user's point of view, it would appear that when they dragged the column to its minimum or maximum width, the drag operation would simply stop, even when they hadn't released the mouse. This is clearly not what we want.

If we are willing to compile with unsafe code enabled, we can modify the HDN_TRACK message itself, changing the size of the column in place (see the commented out code in the HandleNotify method). Without unsafe code, the best we can do is allow the user to drag the column to any width and then have it spring back to within bounds once they release the mouse button. UI-wise, it's very ugly.

I'm still looking for suggestions for a better way to build this part of the mouse trap. :-)

[1.6] Strange Bug in ListView

Just in case someone else runs into this problem, there is a strange bug in the ListView code. The effect of this bug is that in between a BeginUpdate() and EndUpdate() pair, iterating over the Items collection and calling Items.Clear() does not work reliably if the ListView handle has not been created. For example, if you call the following method before the handle for listView1 has been created, nothing will be written to the debugging output:

private void InitListView1()
{
    this.listView1.BeginUpdate();
    this.listView1.Items.Add("first one");
    this.listView1.Items.Add("second one");
    foreach (ListViewItem lvi in this.listView1.Items)
        System.Diagnostics.Debug.WriteLine(lvi);
    this.listView1.EndUpdate();
}

If you remove the BeginUpdate() and EndUpdate() pair or call the method after the handle has been created, the method will work as expected.

The source of this bug is that, deep in the bowels of the ListView code, when BeginUpdate() is called, the ListView begins caching list updates. When EndUpdate() is called, the cache is flushed. However, GetEnumerator() does not flush the cache or take it into account. So, iterating over Items between calls to BeginUpdate() and EndUpdate() will only return the items that were present before BeginUpdate(). There are at least two easy ways around this bug:

  1. Don't use a BeginUpdate()/EndUpdate() pair.
  2. Make a call to Items.Count, which does flush the cache, before iterating over the collection.

Thanks to ereigo for helping me track down this bug.

Still to Do

  • Improve the column resize limiting code
  • Allow text tips to be shown for each sub-item, rather than just column 0

Frequently Requested, but Currently Impossible

The most frequently requested feature is collapsible groups. I want it; you want it; your great aunt Mildrid who lives in Wollongong wants it. Unfortunately, with the current ListView it is just not possible: groups cannot be collapsed.

There are tantalizing hints in the Platform SDK that this will not always be the case. The LVGROUP structure has a state member, for which the only current valid value is LVGS_NORMAL which means "Groups are expanded; the group name is displayed and all items in the group are displayed." That just begs for another value, LVGS_COLLAPSED, but alas, no such beast exists. In the future, collapsible groups may be possible, but for now, they are not.

[August 2007 update] Apparently, under Windows Vista those values -- LVGS_COLLAPSED, LVGS_COLLAPSIBLE and their friends -- all exist. It should therefore be possible to make this control have collapsible groups, but only under Vista! They do nothing under previous versions of Windows, including XP. However, I don't have Windows Vista and all current reviews give me no inclination to upgrade any time soon. If I ever get a Windows Vista machine, I'll see if I can make collapsible groups happen... but don't hold your breath. :-)

Conclusion

I have written this control in two other languages -- Smalltalk and Python -- and it is always one of the most useful items in my toolbox. I have used it in every project I have had in those languages and I'm sure it is just as useful here. I hope that the code encourages more "slothfulness" and gives programmers time to improve some other parts of their project, thus encouraging hubris as well.

History

24 July 2008 - Version 1.13

Major changes
  • Allow check boxes on FastObjectListViews. .NET's ListView cannot support checkboxes on virtual lists. We cannot get around this limit for plain VirtualObjectListViews, but we can for FastObjectListViews. This is a significant piece of work and there may well be bugs that I have missed. This implementation does not modify the traditional CheckedIndicies/CheckedItems properties, which will still fail. It uses the new CheckedObjects property as the way to access the checked rows. Once CheckBoxes is set on a FastObjectListView, trying to turn it off again will throw an exception.
  • There is now a CellEditValidating event, which allows a cell editor to be validated before it loses focus. If validation fails, the cell editor will remain. Previous versions could not prevent the cell editor from losing focus. Thanks to Artiom Chilaru for the idea and the initial implementation.
  • Allow selection foreground and background colors to be changed. Windows does not allow these colours to be customised, so we can only do these when the ObjectListView is owner drawn. To see this in action, set the HighlightForegroundColor and HighlightBackgroundColor properties and then call EnableCustomSelectionColors().
  • Added AlwaysGroupByColumn and AlwaysGroupBySortOrder properties, which force the list view to always be grouped by a particular column.
Minor Improvements
  • Added CheckObject() and all its friends, as well as CheckedObject and CheckedObjects properties
  • Added LastSortColumn and LastSortOrder properties.
  • Made SORT_INDICATOR_UP_KEY and SORT_INDICATOR_DOWN_KEY public so they can be used to specify the image used on column headers when sorting.
  • Broke the more generally useful CopyObjectsToClipboard() method out of CopySelectionToClipboard(). CopyObjectsToClipboard() could now be used, for example, to copy all checked objects to the clipboard.
  • Similarly, building the column selection context menu was separated from showing that context menu. This is so external code can use the menu building method, and then make any modification desired before showing the menu. The building of the context menu is now handled by MakeColumnSelectMenu().
  • Added RefreshItem() to VirtualObjectListView so that refreshing an object actually does something.
  • Consistently use copy-on-write semantics with AddObject(s)/RemoveObject(s) methods. Previously, if SetObjects() was given an ArrayList that list was modified directly by the Add/RemoveObject(s) methods. Now, a copy is always taken and modifying, leaving the original collection intact.
Bug Fixes (not a complete list)
  • Fixed a bug with GetItem() on virtual lists where the item returned was not always complete .
  • Fixed a bug/limitation that prevented ObjectListView from responding to right clicks when it was used within a UserControl (thanks to Michael Coffey).
  • Corrected bug where the last object in a list could not be selected via SelectedObject.
  • Fixed bug in GetAspectByName() where chained aspects would crash if one of the middle aspects returned null (thanks to philippe dykmans).

10 May 2008 - Version 1.12

  • Added AddObject/AddObjects/RemoveObject/RemoveObjects methods. These methods allow the programmer to add and remove specific model objects from the ObjectListView. These methods work on ObjectListView and FastObjectListView. They have no effect on DataListView and VirtualObjectListView since the data source of both of these is outside the control of the ObjectListView.
  • Non detail views can now be owner drawn. The renderer installed for primary column is given the chance to render the whole item. See BusinessCardRenderer in the demo for an example. In the demo, go to the Complex tab, turn on Owner Drawn, and switch to Tile view to see this in action.
  • BREAKING CHANGE. The signature of RenderDelegate has changed. It now returns a boolean to indicate if default rendering should be done. This delegate previously returned void. This is only important if your code used RendererDelegate directly. Renderers derived from BaseRenderer are unchanged.
  • The TopItemIndex property now works with virtual lists
  • MappedImageRenderer will now render a collection of values
  • Fixed the required number of bugs:
    • The column select menu will now appear when the header is right clicked even when a context menu is installed on the ObjectListView
    • Tabbing while editing the primary column in a non-details view no longer tries to edit the new column's value
    • When a virtual list that is scrolled vertically is cleared, the underlying ListView becomes confused about the scroll position, and incorrectly renders items after that. ObjectListView now avoids this problem.

1 May 2008 - Version 1.11

  • Added SaveState() and RestoreState(). These methods save and restore the user modifiable state of an ObjectListView. They are useful for saving and restoring the state of your ObjectListView between application runs. See the demo for examples of how to use them.
  • Added ColumnRightClick event
  • Added SelectedIndex property
  • Added TopItemIndex property. Due to problems with the underlying ListView control, this property has several quirks and limitations. See the documentation on the property itself.
  • Calling BuildList(true) will now try to preserve scroll position as well as the selection (unfortunately, the scroll position cannot be preserved while showing groups).
  • ObjectListView is now CLS-compliant
  • Various bug fixes. In particular, ObjectListView should now be fully functional on 64-bit versions of Windows.

18 March 2008 - Version 1.10

  • Added space filling columns. A space filling column fills all (or a portion) of the width unoccupied by other columns.
  • Added some methods suggested by Chris Marlowe: ClearObjects(), GetCheckedObject(), GetCheckedObjects(), a flavour of GetItemAt() that returns the item and column under a point. Thanks for the suggestions, Chris.
  • Added minimal support for Mono. To create a Mono version, compile with conditional compilation symbol "MONO". The Windows.Forms support under Mono is still a work in progress -- the listview still has some serious problems (I'm looking at you, virtual mode). If you do have success with Mono, I'm happy to include any fixes you might make (especially from Linux or Mac coders). Please don't ask me Mono questions.
  • Fixed bug with subitem colors when using owner drawn lists and a RowFormatter.

2 February 2008 - Version 1.9.1

  • Added FastObjectListView for all impatient programmers.
  • Added FlagRenderer to help with drawing bitwise-OR'ed flags (search for FlagRenderer in the demo project to see an example)
  • Fixed the inevitable bugs that managed to appear:
    • Alternate row colouring with groups was slightly off
    • In some circumstances, owner drawn virtual lists would use 100% CPU
    • Made sure that sort indicators are correctly shown after changing which columns are visible

16 January 2008 - Version 1.9

  • Added ability to have hidden columns, i.e. columns that the ObjectListView knows about but that are not visible to the user. This is controlled by OLVColumn.IsVisible. I added ColumnSelectionForm to the demo project to show how it could be used in an application. Also, right clicking on the column header will allow the user to choose which columns are visible. Set SelectColumnsOnRightClick to false to prevent this behaviour.
  • Added CopySelectionToClipboard() which pastes a text and HTML representation of the selected rows onto the Clipboard. By default, this is bound to Ctrl-C.
  • Added support for checkboxes via CheckStateGetter and CheckStatePutter properties. See ColumnSelectionForm for an example of how to use.
  • Added ImagesRenderer to draw more than one image in a column.
  • Made ObjectListView and OLVColumn into partial classes so that others can extend them.
  • Added experimental IncrementalUpdate() method, which operates like SetObjects() but without changing the scrolling position, the selection, or the sort order. And it does this without a single flicker. Good for lists that are updated regularly. [Better to use a FastObjectListView and the Objects property]
  • Fixed the required quota of small bugs.

30 November 2007 - Version 1.8

  • Added cell editing -- so easy to say, so much work to do
  • Added SelectionChanged event, which is triggered once per user action regardless of how many items are selected or deselected. In comparison, SelectedIndexChanged events are triggered for every item that is selected or deselected. So, if 100 items are selected, and the user clicks a different item to select just that item, 101 SelectedIndexChanged events will be triggered, but only one SelectionChanged event. Thanks to lupokehl42 for this suggestion and improvements.
  • Added the ability to have secondary sort column used when the main sort column gives the same sort value for two rows. See SecondarySortColumn and SecondarySortOrder properties for details. There is no user interface for these items -- they have to be set by the programmer.
  • ObjectListView now handles RightToLeftLayout correctly in owner drawn mode, for all you users of Hebrew and Arabic (still working on getting ListViewPrinter to work, though). Thanks for dschilo for his help and input.

13 November 2007 - Version 1.7.1

  • Fixed bug in owner drawn code, where the text background color of selected items was incorrectly calculated.
  • Fixed buggy interaction between ListViewPrinter and owner drawn mode.

7 November 2007 - Version 1.7

  • Added ability to print ObjectListViews using ListViewPrinter.

30 October 2007 - Version 1.6

  • Major changes
    1. Added ability to give each column a minimum and maximum width (set the minimum equal to the maximum to make a fixed-width column). Thanks to Andrew Philips for his suggestions and input.
    2. Complete overhaul of DataListView to now be a fully functional, data-bindable control. This is based on Ian Griffiths' excellent example, which should be available here, but unfortunately seems to have disappeared from the Web. Thanks to ereigo for significant help with debugging this new code.
    3. Added the ability for the listview to display a "this list is empty"-type message when the ListView is empty (obviously). This is controlled by the EmptyListMsg and EmptyListMsgFont properties. Have a look at the "File Explorer" tab in the demo to see what it looks like.
  • Minor changes
    1. Added the ability to preserve the selection when BuildList() is called. This is on by default.
    2. Added the GetNextItem() and GetPreviousItem() methods, which walk sequentially through the ListView items, even when the view is grouped (thanks to eriego for the suggestion).
    3. Allow item count labels on groups to be set per column (thanks to cmarlow for the idea).
    4. Added the SelectedItem property and the GetColumn() and GetItem() methods.
    5. Optimized aspect-to-string conversion. BuildList() is 15% faster.
    6. Corrected the bug with the custom sorter in VirtualObjectListView (thanks to mpgjunky).
    7. Corrected the image scaling bug in DrawAlignedImage() (thanks to krita970).
    8. Uses built-in sort indicators on Windows XP or later (thanks to gravybod for sample implementation).
    9. Plus the requisite number of small bug fixes.

3 August 2007 - Version 1.5

  • ObjectListViews now have a RowFormatter delegate. This delegate is called whenever a ListItem is added or refreshed. This allows the format of the item and its sub-items to be changed to suit the data being displayed, like red colour for negative numbers in an accounting package. The DataView tab in the demo has an example of a RowFormatter in action. Include any of these words in the value for a cell and see what happens: red, blue, green, yellow, bold, italic, underline, bk-red, bk-green. Be aware that using RowFormatter and trying to have alternate coloured backgrounds for rows can give unexpected results. In general, RowFormatter and UseAlternatingBackColors do not play well together.
  • ObjectListView now has a RowHeight property. Set this to an integer value and the rows in the ListView will be that height. Normal ListViews do not allow the height of the rows to be specified; it is calculated from the size of the small image list and the ListView font. The RowHeight property overrules this calculation by shadowing the small image list. This feature should be considered highly experimental. One known problem is that if you change the row height while the vertical scroll bar is not at zero, the control's rendering becomes confused.
  • Animated GIF support: if you give an animated GIF as an Image to a column that has ImageRenderer, the GIF will be animated. Like all renderers, this only works in OwnerDrawn mode. See the DataView tab in the demo for an example.
  • Sort indicators can now be disabled, so you can put your own images on column headers.
  • Better handling of item counts on groups that only have one member: thanks to cmarlow for the suggestion and sample implementation.
  • The obligatory small bug fixes.

30 April 2007 - Version 1.4

  • Owner drawing and renderers.
  • ObjectListView now supports all ListView.View modes, not just Details. The tile view has its own support built in.
  • Column headers now show sort indicators.
  • Aspect names can be chained using a "dot" syntax. For example, Owner.Workgroup.Name is now a valid AspectName. Thanks to OlafD for this suggestion and a sample implementation.
  • ImageGetter delegates can now return ints, strings or Image objects, rather than just ints as in previous versions. ints and strings are used as indices into the image lists. Images are only shown when in OwnerDrawn mode.
  • Added OLVColumn.MakeGroupies() to simplify group partitioning.

5 April 2007 - Version 1.3

  • Added DataListView.
  • Added VirtualObjectListView.
  • Added Freeze()/Unfreeze()/Frozen functionality.
  • Added ability to hand off sorting to a CustomSorter delegate.
  • Fixed bug in alternate line coloring with unsorted lists: thanks to cmarlow for finding this.
  • Handle null conditions better, e.g. SetObjects(null) or having zero columns.
  • Dumbed-down the sorting comparison strategy. Previous strategy was classic overkill: user extensible, handles every possible situation and unintelligible to the uninitiated. The simpler solution handles 98% of cases, is completely obvious and is implemented in 6 lines.

5 January 2007 - Version 1.2

  • Added alternate line colors.
  • Unset sorter before building list. 10x faster! Thanks to aaberg for finding this.
  • Small bug fixes.

26 October 2006 - Version 1.1

  • Added "Data Unaware" and "IDE Integration" article sections.
  • Added model-object-level manipulation methods, e.g. SelectObject() and GetSelectedObjects().
  • Improved IDE integration.
  • Refactored sorting comparisons to remove a nasty if...else cascade.

14 October 2006 - Version 1.0

License

This code is covered by GNU General Public License v3.

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPL)

About the Author

Phillip Piper


Phillip has been playing with computers since the Apple II was the hottest home computer available. He learned the fine art of C programming and Guru meditation on the Amiga.

Python and Smalltalk are his languages of choice. C# is interesting. C++ is to programming what drills are to visits to the dentist.

He has worked for longer than he cares to remember as Lead Programmer and System Architect of the Objective document management system. (www.objective.com)

He is currently on leave from programming, and is living in northern Mozambique, teaching in villages.
Occupation: Web Developer
Location: Mozambique Mozambique

Other popular List Controls articles:

Article Top
Sign Up to vote for this article
You must Sign In to use this message board.
FAQ FAQ Noise ToleranceSearch Search Messages 
 Layout  Per page   
 Msgs 1 to 25 of 677 (Total in Forum: 677) (