Click here to Skip to main content
Email Password   helpLost your password?
A much easier to use ListView...

Screenshot - ObjectListView.jpg

...that on Vista and later looks even nicer:

Screenshot - ReportModernExample.jpg

With a little work, you can even produce something from the halls of cool like this:

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. If you want to do something with a ListView, this code probably has some code to help you do it. For those in a hurry, this control has the following major features:

This control now has its own website, hosted by SourceForge: ObjectListView - How I Learned To Stop Worrying and Love .NET's ListView (made using the cool Sphinx documentation tool). This is not an empty shell site. It actually has lots of useful information. There you can find a step by step tutorial to help you get started, as well as a cookbook showing you how to accomplish common tasks. This article is only really an introduction.

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.

Features

An ObjectListView provides two groups of functionality. The first group is designed to make a ListView much easier to use. This group ranges from automatically converting a list of model objects into a fully functional ListView, to making drag and drop and cell editing much easier to use. The second group adds new features to a ListView, such as image overlays and customisable tooltips.

1. Basics of using an ObjectListView

1.1 First steps

There are two ways to use an ObjectListView in your project:

1. Use the ObjectListView project

  1. Download the ObjectListView project.
  2. Add the ObjectListView project to your project (right click on your solution; choose "Add...", "Existing Project", then choose ObjectListView.csproj).
  3. In your project, add a reference to the ObjectListView project (right click on your project, choose "Add Reference...", choose the "Projects" tab, then double click on the ObjectListView project).
  4. Build your project.

Once your project has been built, there should now be a new section in your Toolbox, "ObjectListView 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.

2. Use ObjectListView.dll

If you don't want to add the ObjectListView project to your project, you can also just add the ObjectListView.dll file.

  1. Download or build the ObjectListView.dll file.
  2. In your project, add a reference to ObjectListView.dll (right click on your project, choose "Add Reference...", choose "Browse" tab, navigate to the ObjectListView.dll file, and double click on it).

Adding the DLL does not automatically add any new components into your toolbox. You will need to add them manually after you have added the DLL to your project.

1.2 Making it go

"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 ObjectListView will handle drag and drop, alternate line colouring, column-click sorting, data formatting, 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.

2. Giving a little more detail

2.1 Behind SetObjects()

What is actually happening here? When you call SetObjects(), the ObjectListView runs through the given list of model objects, extracts the aspect nominated for each column, converts that aspect to a string, and then puts those strings together to make a row in the ListView. For those who think in pictures, you can visualize the process like this:

2.2 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 needs a different mindset. If you can perform the mind-mangling step of changing your thinking, ObjectListView will be your best friend.

An ObjectListView is much more active than a plain ListView. A normal ListView is essentially passive: it sits there, and you poke and prod it and eventually it looks like you want. With an ObjectListView, you tell it what you want done, and the ObjectListView does it for you. More formally: an ObjectListView is used declaratively. You configure the ObjectListView, give it your collection of model objects, and the ObjectListView builds the ListView for you.

The crucial part of using an ObjectListView is configuring it. Most of this configuration can be done within the IDE by setting properties on the ObjectListView itself or on the columns that are used within the list. Some configuration cannot be done through properties: these more complex configurations are done by installing delegates (more on this later). Once the columns and control are configured, putting it into action is simple, as you have already seen, a single call to SetObjects().

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. It owns the ListViewItems, and destroys, changes, and builds them as required from the information you have given. Resist the temptation to add/edit/sort or otherwise mess with 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 knows 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.

2.3 Adding complexity: images

A single call to SetObjects() is all well and good, but real-world applications need more than just sorting and grouping. They need at least a little image in the first column.

The obvious first enhancement to this simple example is to display images in the ListView. To do that, we need to configure the ObjectListView so it knows what image to show against each row. This normally cannot be done within the IDE. Very often the image to be shown depends on the model object being displayed. To decide on an image, we need a more complex type of configuration: installing a delegate.

A delegate is basically a piece of code that you give to an ObjectListView saying, "When you need to do this, call this piece of code" where this can be any of several tasks. In this case, we install an ImageGetter delegate, which tells the ObjectListView, "When you need to figure out the image for this model object, call this piece of code." [If the word "delegate" worries you, think of them as function pointers where the parameter and return types can be verified. If that makes no sense to you, just keep reading. It will (possibly) become clear with some examples.]

First, you need a method that matches the ImageGetterDelegate signature: it must accept a single object parameter and return an object. The value returned from the ImageGetter delegate is used as an index into the ObjectListView's SmallImageList. As such, the ImageGetter can return either a string or an int. (If the ObjectListView is owner-drawn, the ImageGetter can also return an Image).

A somewhat frivolous example follows:

object 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);

In VB:

this.personColumn.ImageGetter = _
     New ImageGetterDelegate(AddressOf PersonColumnImageGetter)

.NET 2.0 added the convenience of anonymous delegates (to C# at least - VB programmers still have to write separate functions). 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.

[v2.3] If your model class has a property which can return the name or the index of the image that should be shown, you don't need to install a delegate. You can set the ImageAspectName property to the name of that property.

But this is crossing the line between model and view, so I'm not encouraging this practice, just pointing out that it's possible.

2.4 Other customizations

ObjectListView uses a combination of events and delegates to allow further, more complex customizations. All of the following can be customized:

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.

2.5 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.

[v2.1] As of this version, you would not have to define AspectGetters in this manner. It would be enough to set columnName.AspectName to "Name" and the ObjectListView would be able to extract the indexed property from the DataRow. But the example still works as it stands.

3. Flavours of ObjectListView

3.1 A little more data aware — DataListView

In response to intense public demand — OK, a couple of people asked for it — there is now a DataListView class. This is a data-bindable version of ObjectListView which 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! Edit a value in the DataListView and the change automatically appears in the DataTable!

The DataListView can accept several types of objects as data sources:

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.

3.2 You want how many rows in that ListView?! — VirtualObjectListView

If you've ever wanted to thoroughly overwhelm your users with 10 million rows of data, then go ahead and knock them out with VirtualObjectListView.

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 VirtualObjectListView is your answer.

Normally, ObjectListView keeps a list of model objects that it can read, sort, or group at will. A VirtualObjectListView does not keep such a list. Instead, it only fetches 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 VirtualObjectListView will never ask for it, so the program will never have to create it.

To use a VirtualObjectListView, you must implement an IVirtualListDataSource and give that data source to the virtual list (via the DataSource property). Using that interface, the virtual list can then function just like a full-fledged ObjectListView. The only things a virtual list still can't do are: it can't show groups and it can't use tile view. But otherwise, it should operate in the same way as a normal ObjectListView, including sorting, check boxes, and searching through typing.

If you don't want to implement all the methods of IVirtualListDataSource, you can subclass AbstractVirtualListDataSource which is a “do-nothing” implementation of that interface. At the very least, you must implement GetObjectCount() and GetNthObject(), otherwise nothing will appear in the list.

3.3 A much faster ListView — 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.

[New in v2.3] On Vista, FastObjectListViews can now show groups! Simply set ShowGroups to true, and the control will handle groups in the same fashion as a normal ObjectListView.

3.4 Tree hugging — TreeListView

From time to time, there are situations when you want to show a tree structure (like a TreeView), but you also want to show more information about the items than just their name (like a ListView). Enter the TreeListView. It shows a tree structure with its nice ability to expand and collapse, but also shows information in columns:

Screenshot - TreeListView in action

Like all the other ObjectListViews, TreeListView relies on delegates. The two essential delegates for using a TreeListView are: one to know if a given model can be expanded (CanExpandGetter delegate), and the second to get the children of a model when it is expanded (ChildrenGetter delegate).

In the demo, there is an Explorer like example, which navigates the disks on the local computer. The tree list view in that demo is configured like this:

this.treeListView.CanExpandGetter = delegate(object x) {
    return (x is DirectoryInfo);
};
 
 
this.treeListView.ChildrenGetter = delegate(object x) {
    DirectoryInfo dir = (DirectoryInfo)x;
    return new ArrayList(dir.GetFileSystemInfos());
};

In this example, the CanExpandGetter delegate ensures that only directories can be expanded.

The ChildrenGetter delegate returns the contents of a directory when that directory is expanded. ChildrenGetter delegates are only ever called if CanExpandGetter returns true. So in this case, the ChildrenGetter delegate knows that the parameter x must be a DirectoryInfo instance.

To make it work, you must add some "roots" (top level objects). You can do this either by setting the Roots property to a collection of model objects, or you can just call SetObjects() like normal. On a TreeListView, SetObjects(), AddObject(), and RemoveObject() all apply to the collection of roots.

To refresh the list of children under a model, you call RefreshObject() on the parent.

3.5 Casting out the casts — TypedObjectListView

One annoyance with ObjectListView is all the casting that is needed. Because the ObjectListView makes no assumptions about what sort of model objects you will be using, it handles all models as objects and it's up to you to cast them to the right type when you need to. This leads to many delegates starting with a cast like this:

this.objectListView1.SomeDelegate = delegate(object x) {
    MyModelObject model = (MyModelObject)x;
    ...
}

which becomes tiresome after a while. It would be nice if you could tell the ObjectListView that it would always be displaying, say, Person objects. Something like:

this.objectListView1 = new ObjectListView<Person>();
this.objectListView1.SomeDelegate = delegate(Person model) {
    ...
}

Unfortunately, the designer in Visual Studio cannot handle parameterized controls like that. [I remember reading that in a Microsoft blog somewhere, but I can't find it again. There are a couple of knowledgeable people who says that it can't - here for example. If someone knows if this is a documented decision, could you please let me know?] There are a couple of tricks to get around some of the most obvious problems, but they all hit a wall with the code generation.

So, in the meantime, we now have a TypedObjectListView class. This is not another ObjectListView subclass, but rather it's a typed wrapper around an existing ObjectListView. To use one, you create an ObjectListView within the IDE as normal. When it is time to implement your delegates, you create a TypedObjectListView wrapper around your list view, and declare your delegates against that wrapper. It's easier to use than it is to explain, so look at this example:

TypedObjectListView<Person> tlist = new TypedObjectListView<Person>(this.listViewSimple);
tlist.BooleanCheckStateGetter = delegate(Person x) {
    return x.IsActive;
};
tlist.BooleanCheckStatePutter = delegate(Person x, bool newValue) {
    x.IsActive = newValue;
    return newValue;
};

Look ma! No casts! The delegates are declared against the typed wrapper, which does know what model objects are being used.

You can also use the TypedObjectListView for typed access to the delegates on your columns:

tlist.GetColumn(0).AspectGetter = delegate(Person x) { return x.Name; };
tlist.GetColumn(1).AspectGetter = delegate(Person x) { return x.Occupation; }; 

If you don't like referring to columns by their index, you can create TypedColumn objects around a given ColumnHeader object:

TypedColumn<Person> tcol = new TypedColumn<Person>(this.columnHeader16);
tcol.AspectGetter = delegate(Person x) { return x.GetRate(); };
tcol.AspectPutter = delegate(Person x, object newValue) { x.SetRate((double)newValue); }; 

The final feature of a TypedObjectListView is that it can automatically generate an AspectGetter for a column from its AspectName. So, rather than hand-coding AspectGetters like we have done above, you simply configure the AspectName in the IDE, and then call tlist.GenerateAspectGetters(). This can (should?) handle aspects of arbitrary complexity, like "Parent.HomeAddress.Phone.AreaCode".

4. Other features

4.1 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, columns can 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 the potential limitations of my implementation.

4.2 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 totaled. The difference between that total and the width of the control is then shared between the free space filling columns. If you only have one such column, it is given all the space; if you have two, each is given half 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 rightmost 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.

4.3 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.

This is a logically separate piece of code, so it lives in its own project. If you want to use it, you will need to add to your project either the ListViewPrinter project itself or the ListViewPrinter.dll file. The procedure is the same as for the ObjectListView project given in the First steps section above.

4.4 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. 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:

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:

4.4.1 How cells are edited and how you can 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.

You can prevent the cell edit operation from finishing (e.g., if the value the user has entered isn't acceptable) by listening for the CellEditValidating event. If the handler for this event sets Cancel to true, the edit operation will not finish and the editor will remain on the screen. Please make sure you have made it clear to the user why the edit operation hasn't finished.

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

4.4.2 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:

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 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.

All aspects of cell editing are described in further details on this page.

4.5 (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 Example
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 or as a collection of images. Most typically, if you have stored images in your database, you would use this renderer to draw those images. If the cell's data value is an ICollection that contains strings, ints, or Images, then all those images will be drawn.

FlagsRenderer This renderer draws 0 or more images within its cell. The cell's data value should be a collection of bitwise flags that indicate which images should be drawn. See the demo for an example of how to use it.

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.

As of v2.0, Renderers are now Components, which means they can be created and manipulated within the IDE. So, to use a MultiImageRenderer like the above, you would create one within the IDE, configure its properties to be as you can, and then assign it to the column's Renderer property.

4.5.1 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. Row height can be set using the RowHeight property. You cannot have rows of differing heights — it simply cannot be done with a ListView.

It is obvious, but easily overlooked, that owner drawing is slower than non-owner drawing. Owner drawing requires a lot more work than 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!

4.6 Taking the drag out of drag and drop [v2.2]

As of v2.2, ObjectListView now has reasonably sophisticated support for drag and drop operations.

4.6.1 Using an ObjectListView as a drag source

If you want the user to be able to drag rows out of an ObjectListView, you set the DragSource property. This property accepts an object that implements the IDragSource interface. It will often be enough to use an instance of SimpleDragSource:

this.objectListView1.DragSource = new SimpleDragSource();

This drag source remembers the currently selected rows, and equips the drag data object with text and HTML versions of those rows. With that simple drag source, you can, for example, select 10 rows from an ObjectListView and drag them onto Microsoft Word to create a formatted table of those rows. You can also drag rows onto other ObjectListViews, which will normally be what you want.

From within the IDE, you can set IsSimpleDragSource to true to make your ObjectListView into a drag source using a SimpleDragSource.

4.6.2 Using an ObjectListView as a drop sink

Accepting drops from other sources is a little more complicated, but is handled in a similar manner. If you want the user to be able to drop stuff onto an ObjectListView, you set the DropSink property. This property accepts an object that implements the IDropSink interface. In many cases, you will use an instance of SimpleDropSink.

this.objectListView1.DropSink = new SimpleDropSink();

From within the IDE, you can set IsSimpleDropSink to true to do make your ObjectListView into a drop sink. A DragSource needs no further information, but a DropSink needs to know at least two other things:

  1. Can the currently dragged objects be dropped at the current location?
  2. If the objects are dropped, what should happen next?

If you use a SimpleDropSink, the ObjectListView will trigger two events to handle these situations: a CanDrop event and a Dropped event. To actually be useful, you need to handle these events. You can set up handlers for these events within the IDE, like normal.

You can alternatively listen for the ModelCanDrop and ModelDropped events. This second pair of events are triggered when the source of the drag is another ObjectListView. These events work the same as the CanDrop and Dropped events except that the argument block includes more information:

A SimpleDropSink is actually quite sophisticated. It can be configured in several ways. Have a look at the code to see more options.

DropSink configuration options

Allow drops on the background:

myDropSink.CanDropBetween
   = true;

Allow drops between items:

myDropSink.CanDropBetween
   = true;

You can even drop on subitems:

myDropSink.CanDropOnSubItems
   = true;

And change highlight colour just to be different:

myDropSink.FeedbackColor =
    Color.IndianRed;

You can learn more about drag and drop, including how to write your own drop sink from scratch, on this page.

4.7 Collapsible groups

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 — on XP. But on Vista, this most commonly requested feature is a reality. It is enabled by default, so under Vista, groups are automatically collapsible. If you don't want your groups to be collapsible, set HasCollapsibleGroups to false. Thanks to Crustyapplesniffer who implemented this feature.

4.8 Groups on steroids

In v2.3, groups received a major overhaul. No longer content with just being collapsible, groups can now have a title image, a subtitle, a task (that has a clickable link on the right), and footers. When done well, this can make your listview look very nice indeed:

Groups with images, tasks and subtitles

This extended formatting can be setup during the AboutToCreateGroup event. Alternatively, you can use the extended version of the MakeGroupies() method, which allows all these new properties to be configured. The above screenshot was configured with one MakeGroupies() call:

this.columnCookingSkill.MakeGroupies(
    new object[]{10, 20, 30, 40},
    new string[] {"Pay to eat out", "Suggest take-away", "Passable", 
                  "Seek dinner invitation", "Hire as chef"},
    new string[] { "emptytoast", "hamburger", "toast", "dinnerplate", "chef" },
    new string[] { 
        "Pay good money -- or flee the house -- rather than eat their homecooked food", 
        "Offer to buy takeaway rather than risk what may appear on your plate", 
        "Neither spectacular nor dangerous", 
        "Try to visit at dinner time to wrangle an invitation to dinner", 
        "Do whatever is necessary to procure their services" },
    new string[] { "Call 911", "Phone PizzaHut", "", "Open calendar", "Check bank balance" }
);

These group formatting facilities are only available on Vista and later. On XP, groups can only have a header.

4.9 Customisable "List is empty" message

When an ObjectListView is empty, it can display a "this list is empty" type message. The EmptyListMsg property holds the string that appears when an ObjectListView is empty. This string is rendered using EmptyListMsgFont. Both of these properties can be configured within the IDE.

But if you want to write a little bit of code, you can have much more interesting messages. The empty message list is actually implemented as an overlay. You can access that overlay though the EmptyListMsgOverlay property. By default, this is a TextOverlay that you can customise to your heart's content:

TextOverlay textOverlay = this.objectListView1.EmptyListMsgOverlay as TextOverlay;
textOverlay.TextColor = Color.Firebrick;
textOverlay.BackColor = Color.AntiqueWhite;
textOverlay.BorderColor = Color.DarkRed;
textOverlay.BorderWidth = 4.0f;
textOverlay.Font = new Font("Chiller", 36);
textOverlay.Rotation = -5;

Doing this gives a message like this:

Fancy empty list message

4.10 Hyperlinks

ObjectListViews can now treat cells as hyperlinks. To do this, set UseHyperlinks to true of the ObjectListView, and then set the Hyperlink property of OLVColumn to true to make all the cells in that column behave as hyperlinks.

If you don't want all cells to be hyperlinks, you can listen for the IsHyperlink event (in the above shot, occupations that start with "s" are not hyperlinks). In this event, you can specify what URL will be attached to that cell. By default, the URL is the text of the cell. If you set the URL to null, that cell will not be treated as a hyperlink. If you are already listening for FormatCell, you could set up the URL in that event too.

The formatting of the hyperlinks is controlled by the HyperlinkStyle property of ObjectListView. You can create and configure a HyperLinkStyle within the IDE, and then assign it to your ObjectListView. The same style can be assigned to multiple ObjectListViews. In 95% of cases, the default styling will suffice.

When a hyperlink is clicked, a HyperlinkClicked event is triggered. If you handle this yourself, set Handled to true to prevent the default processing from occurring. If you don’t handle it, the default processing is to try and open the associated URL.

Be careful about making column 0 to be a hyperlink. If it is, every time a user clicks a row trying to select it, it will open a browser window, which would become annoying very quickly.

4.11 Header formatting

The font and text color of the ObjectListView header can now be changed. Set the HeaderFont or HeaderForeColor properties on the ObjectListView to change the font and color for all columns, or set HeaderFont or HeaderForeColor properties on one OLVColumn to change just that column. There is also HeaderWordWrap on the ObjectListView which allows the text within a header to be word wrapped. So, if you are feeling sadistic, you can inflict something like this on your users:

Word wrapped headers with different colors and fonts

As far as possible, the header formatting tries to keep the same style as whatever theme is currently active. For this reason, you cannot set the background color of the header. If you want to do this, you can always owner draw the header yourself.

4.12. Generating an ObjectListView without the IDE

One fundamental of good design is separation of presentation from model. Model classes should not know how they are being presented to the user.

But there are development situations when speed of development is everything (merchant banks and stock brokers often seem to be in this camp). In such cases, placing some sort of user interface into the model classes themselves is an acceptable trade off.

It is with a nod to such development that ObjectListView now has a Generator class and OLVColumn attributes. The idea with these classes is that, in your model classes, you decide which properties you want to appear in the ObjectListView, and then you give those properties an OLVColumn attribute. In these attribute, you specify some of the characteristics that you would normal give through the IDE (e.g., column title, alignment, image getter, format string). Then, when you are ready to show your list of models, you generate the columns from the model and then show the models:

List<ForexPurchase> purchases = this.GetForexPurchasesToShow();
Generator.GenerateColumns(this.olv, purchases);
this.olv.Objects = purchases;

In this example, this.olv is a completely unconfigured ObjectListView. Nothing was done to it in the IDE, except for placing it on the form: no columns were created or configured. The Generator uses the information that was given in the OLVColumn attributes to build a fully functional ObjectListView.

When the user later wants to see the foreign exchange sales that were made today, she clicks the “Sales” button, and some code like this might be executed:

List<ForexSale> sales = this.GetForexSalesToShow();
Generator.GenerateColumns(this.olv, sales);
this.olv.Objects = sales;

This reuses the same ObjectListView control, but now it is a fully functional ObjectListView showing information about Forex sales.

[Thanks to John Kohler for this idea and the original implementation.]

5. Interesting bits of code

5.1 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.

5.1.1 Getting information dynamically

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:

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.

If you look at the code in OLVColumn, you will find that the code is actually slightly more complicated, since it supports using a dot notation to access sub-properties. It is 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 :-)

For example, 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.

5.1.2 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 control's 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.

5.2 Showing images on ListView subitems

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.

5.3 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 [this has been bought by RedGate, but is still free]. 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.

5.4 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:

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. 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.

[v2.0] As of v2.0, ObjectListView modifies the HDN_TRACK message itself, changing the size of the column in place, which is the best solution.

5.5 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.

5.6 Implementing an image watermark [v2.2]

I like pictures. I think it's neat that in Explorer you can put a little graphic in the bottom right of the listview. I wanted to do the same thing with an ObjectListView. Surely, it can't be that difficult. But it was.

What did I specifically want from this feature? I wanted a background image on the ObjectListView. It had to stay fixed in place, not scrolling when the ListView scrolls. It had to work on XP and Vista. It had to be easy to custom, ideally just setting an image within the IDE. If the image could be positioned in whatever corner, or have a varying level of transparency, those would be bonuses. And obviously, I wanted it to work flawlessly — though I would be content with working spectacularly well.

5.6.1 WM_ERASEBKGROUND

The classic solution is to intercept the WM_ERASEBKGROUND message, erase the ClientRectangle, draw whatever you want, and the rest of the control then draws over what you've already drawn. Easy.

But it doesn't work. Actually, it works, so long as you don't double buffer the ListView. While the ListView is unbuffered, the image drawn in the WM_ERASEBKGROUND handler appears fine. But, when the control is double buffered, it doesn't work. When DoubleBuffered is set to true, it also sets the AllPaintingInWmPaint style, which means: don't use WM_ERASEBKGROUND, the paint handler will do everything, including erase the background. So, for a double-buffered ListView (which is what I want), drawing in the WM_ERASEBKGROUND handler doesn't work.

5.6.2 LVM_SETBKIMAGE

The second try was to use LVM_SETBKIMAGE. This WinSDK message tells a ListView to draw an image under the control. Exactly what I wanted. But life is rarely that easy.

The first difficulty was actually making it work. TortoiseSVN sometimes has a listview background image, and Stefan had kindly documented some of his troubles in getting it to work. Using the information there, I managed to put an image under the control! Excellent... well, not really. It did put an image under the ListView, but with a number of unpleasant side-effects:

The show stopper was with the Details view. Column 0 always erased the image. I could live with the other problems, but what's the good of an underlay image when column 0 wipes it out? I checked to see if Stefan had found a solution for this one, but he hadn't.

5.6.3 Layered Window API

I eventually decided on using the Layered Window API, which .NET exposes through the Opacity and TransparencyKey properties of Form.

The idea was to place a completely transparent form over the top of ObjectListView, and then draw onto that form (Mathieu Jacques did the same thing with his LoadingCurtain idea). From the user's point of view, the image appears to be draw onto the ObjectListView, but from the ObjectListView point of view, the overlays are not there.

This idea was a good one, but there were many other complications before it actually worked. See here for the overly-detailed version of the story.

5.6.4 Just do it

But it did eventually work. So, as of v2.2, ObjectListViews support overlays, which are non-scrolling, translucent images, or text drawn over the top of the list contents.

The overlays scheme is extensible, but the two most common overlays are image overlays and text overlays.

These two overlays are so common that ObjectListView comes with one of each predefined: OverlayImage and OverlayText properties. These predefined overlays are exposed to the IDE, and can be configured directly from there. So, for the majority of cases, this is the only understanding of overlays that you will need.

6. 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.

7. Still to do

8. History

12 October 2009 - Version 2.3

This release focused on formatting – giving programmers more opportunity to play with the appearance of the ObjectListView.

Decorations

Decorations allow you to put pretty images, text, and effects over the top of your ObjectListView

Group header formatting

Groups have been overhauled for this release. Groups under XP remain unchanged, but under Vista and Windows 7, many more formatting options are now available.

Hyperlinks

ObjectListViews can now have cells that are hyperlinks.

Header formatting

The font and text color of the ObjectListView header can now be changed. You can also word wrap the header text.

FormatRow and FormatCell events

In the previous version, RowFormatter was the approved way to change the formatting (font/text color/background color) of a row or cell. But it had some limitations:

  1. It did not play well with the AlternateBackgroundColors property.
  2. It was called before the OLVListItem had been added to the ObjectListView, so many of its properties were not yet initialized.
  3. It was painful to use it to format only one cell.
  4. Perhaps, most importantly, the programmer did not know where in the ObjectListView the row was going to appear, so they could not implement more sophisticated versions of the row alternate background colors scheme.

To get around all these problems, there is now a FormatRow event. This is called after the OLVListItem has been added to the control. Plus, it has a DisplayIndex property specifying exactly where the row appears in the list (this is correct even when showing groups).

There is also a FormatCell event. This allows the programmer to easily format just one cell.

Generator

By using compiler attributes, ObjectListViews can now be generated directly from model classes. [Thanks to John Kohler for this idea and the original implementation.]

Groups on virtual lists

When running on Vista and later, virtual lists can now be grouped! FastObjectListView supports grouping out of the box. For your own VirtualObjectListView, you must do some more work yourself.

[This was more of a technical challenge for myself than something I thought would be wildly useful. If you do actually use groups on virtual lists, please let me know.]

Small changes

4 August - Version 2.2.1

This is primarily a bug fix release.

New features
Bug fixes

15 May 2009 - Version 2.2

The two big features in this version are drag and drop support and image overlays.

Drag and drop support

ObjectListViews now have sophisticated support for drag and drop operations. An ObjectListView can be made a source for drag operations by setting the DragSource property. Similarly, it can be made a sink for drop actions by setting the DropSink property. The dragging is based on the IDragSource interface, and the drop handling revolves around the IDropSink interface. SimpleDragSource and SimpleDropSink provide reasonable default implementations for these interfaces.

Since the whole goal of ObjectListView is to encourage slothfulness, for most simple cases, you can ignore these details and just set the IsSimpleDragSource and IsSimpleDropSink properties to true, and then listen for CanDrop and Dropped events.

Rearrangable lists are supported through the RearrangeableDropSink class.

Image and text overlays

This version added the ability to draw translucent images and text over the top over the ObjectListView contents. These overlays do not scroll when the list contents scroll. These overlays works in all Views. You can set an overlay image within the IDE using the OverlayImage and OverlayText properties. The overlay design is extensible, and you can add arbitrary overlays through the AddOverlay() method.

Other new features
Bug fixes (not a complete list)

23 February 2009 - Version 2.1

Complete overhaul of owner drawing

In the same way that 2.0 overhauled the virtual list processing, this version completely reworks the owner drawn rendering process. However, this overhaul was done to be transparently backwards compatible.

The only breaking change is for owner drawn non-details views (which I doubt that anyone except me ever used). Previously, the renderer on column 0 was double tasked for both rendering cell 0 and for rendering the entire item in non-detail view. This second responsibility now belongs explicitly to the ItemRenderer property.

Other significant changes
Minor Changes
Bug fixes

10 January 2009 - Version 2.0.1

This version adds some small features and fixes some bugs in 2.0 release.

New or changed features
Bug fixes

30 November 2008 - Version 2.0

Version 2.0 is a major change to ObjectListView.

Major changes
Minor changes
Breaking changes

24 July 2008 - Version 1.13

Major changes
Minor improvements
Bug fixes (not a complete list)

10 May 2008 - Version 1.12

1 May 2008 - Version 1.11

18 March 2008 - Version 1.10

2 February 2008 - Version 1.9.1

16 January 2008 - Version 1.9

30 November 2007 - Version 1.8

13 November 2007 - Version 1.7.1

7 November 2007 - Version 1.7

30 October 2007 - Version 1.6

3 August 2007 - Version 1.5

30 April 2007 - Version 1.4

5 April 2007 - Version 1.3

5 January 2007 - Version 1.2

26 October 2006 - Version 1.1

14 October 2006 - Version 1.0

License Note

This code is covered by GNU General Public License v3.

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
GeneralThe font size ugly in outlook add-in
Member 155319
16:43 29 Jun '08  
Hi Phillip,

I like your work very much. Recently, I developed an outlook Add-in in which I used ObjectListView as the list view to display a list. But I found that the font size if much bigger than outlook default size and system default size. Every line just displays about 2/3 from top. The bottom can't be displayed. Why?
Please help.
GeneralRe: The font size ugly in outlook add-in
Phillip Piper
11:16 8 Jul '08  
Hi there.

I haven't been able to duplicate this problem To try, I gave each list on the demo a font much bigger than normal, at least 24 points. They all displayed correctly, both in virtual mode and in owner drawn.

Could you give me some more information:
- what sort of ObjectListView are you using?
- Is it owner drawn?
- How are you setting the font?
- Is there something unusual about the font you are using?
- Can you repeat the problem in a small stand alone application?
- Does the problem happen all the time or only some times?

With this information, we can probably track down what is wrong.

Regards,
Phillip
GeneralDatagrid Whisperer
Jonathan C Dickinson
5:03 12 Jun '08  
One of the guys over at the MSDN magazine is known as the Datagrid (ASP.Net) whisperer, can remember which. You must be the Listview whisperer. DAMN! This is a sexy bunch of controls!
GeneralRe: Datagrid Whisperer [modified]
Phillip Piper
8:30 12 Jun '08  
I think I'm going to blush Shucks

Regards,
Phillip
The ListView Whisperer Smile
GeneralObjectsAsList issue
NicolasG
0:53 10 Jun '08  
ObjectAsList returns a copy of the Objects collection known as IEnumerable.
This copy can not be used to remove from or add objects to the Objets collection as it is actually done in AddObjects and RemoveObjects.

I suggest to simply use
Objects as IList
(not as ArrayList...) and eventually throw an InvalidOperationException if Objects is not an IList.
ObjectAsList could even be removed.
GeneralRe: ObjectsAsList issue
Phillip Piper
2:20 10 Jun '08  
Hi Nicholas,

Thanks for your comments and suggestions.

I'm sorry but I didn't quite understand everything you are trying to say. Is the ObjectsAsList property not behaving as you expect? Or were you offering suggestions about how to improve the code?

The ObjectsAsList is a protected property, and is not really intended for public use. It was added solely to factor out common code between the AddObjects and RemoveObjects methods. For those methods to work, they need a collection that can be modified. The Objects property is a IEnumerable, which doesn't support modifications. So we need to convert our Objects collection into a modifiable list. ObjectsAsList does this for us.

You are quite right that ObjectsAsList could return an IList rather than always being an ArrayList.

Regards,
Phillip
GeneralRe: ObjectsAsList issue
NicolasG
2:56 10 Jun '08  
Hi,
i just want to say that add or remove must apply to the public "Objets" collection itself.
Actually, it works if "Objects" is an ArrayList, otherwise OLVItems are remove from or added to the ListView but model objets remains in "Objects" collection.

In other words, adding or removing is only correct if "Objets" is an ArrayList.
GeneralRe: ObjectsAsList issue
Phillip Piper
12:01 10 Jun '08  
Hi again,
NicolasG wrote:
i just want to say that add or remove must apply to the public "Objets" collection itself.

I don't think this is true. Add/Remove can't apply (in general) to the Objects collection itself, since the Objects collection is only an IEnumerable.
NicolasG wrote:
but model objets remains in "Objects" collection.

Again, I'm not sure if this is the case. The Objects property always returns the objects that are currently shown in the list -- even after Add/Remove operations. What will be unchanged is the original collection. So, after this code:
myOlv.SetObjects(myEnumerable);
myOlv.RemoveObject(someObject);
the contents of myEnumerable are unchanged, but myOlv.Objects returns an ArrayList that does not contain someObject.

In general, Add/Remove are not supposed to modify the original collection -- only on the "internal" collection of objects used to populate the list. The docs are (fairly) clear that after using Add/Remove, the internal collection is divorced from the original list. Maybe I should make this point even clearer. After using Add/Remove, the only way to get the modified collection is through the Objects property.

It is a quirk of implementation that when the original collection is an ArrayList, the collection is modified by Add/Remove. Maybe I should remove that quirk? I could make the semanitics "copy-on-write"? Do you think that would be OK?

Regards,
Phillip
GeneralRe: ObjectsAsList issue
NicolasG
4:15 11 Jun '08  
Thanks for your response.

I think now it's better to keep untouched the original collection : I agree with the "copy-on-write" semantics.

That should be the case even for ArrayList.
ArrayList should be replaceable by List<Object> or some strong typed IEnumerable collection without fonctional change.

Nicolas
GeneralAbout [CorrectSubItemColors] function
badtoto
0:35 7 Jun '08  
When i need to customize fields color, I'd like to override CorrectSubItemColors function...
and then find it can't be overrided...
AnswerRe: About [CorrectSubItemColors] function
Phillip Piper
5:14 8 Jun '08  
Hi there,

This one thing I don't like about C#: the need to mark methods as being overridable in subclasses. I think all public/protected methods should be overridable in subclasses.

The only real option you have is simply to modify your copy and make that method virtual.

Just a question: can't you use a RowFormatter to customise your field colours?

Regards,
Phillip
GeneralFastObjectListView Crash
bu_bruiser
12:53 6 Jun '08  
I am using the FastObjectListView for a rapidly updating list and have been really impressed. Everything works fine about 90% of the time, but I occasionally get a crash in the SetObjects method on the line:

this.VirtualListSize = this.objectList.Count;

After a lot of experimentation I believe this to be tied to the issue documented in the past release with the underlying list view getting confused about the top item after a list is scrolled and then cleared. I can reliably duplicate the issue in my application by scrolling the list, clearing the list, and then minimizing the application prior to the next round of updates coming in. This produces a crash every time. Has anyone else had this issue? I also get random crashes from time to time on the same issue during normal execution. So far, the only workaround I have found is to add a try catch block around this line of code and ignore the exception, but that is obviously a cheap hack!

Any advice would be greatly appreciated.
GeneralRe: FastObjectListView Crash
Phillip Piper
5:26 8 Jun '08  
Hi there,

Sorry that you're having these problems. I've tried to make the FastObjectListView crash in the way you've described, but haven't yet been able to do that.

Can you tell what exception is thrown? Is your application minimizes when the listview is updated? What delegates are installed in the list?

Regards,
Phillip
GeneralRe: FastObjectListView Crash
bu_bruiser
4:50 9 Jun '08  
Thanks for the quick response! I am seeing null reference exceptions. They are being thrown by the attempt to set the VirtualListSize in the SetObjects method of the FastObjectListView. It appears that the actual issue occurs down in the underlying implementation when an attempt to set the top item is tried.

The application is maximized and scrolled when the list is cleared and then minimized when the crash occurs.

The only delegates I have installed are the column aspect getters.

I have included a stack trace below:

[2008-06-05 14:36:17,745] [MainApp] [App:ERROR] An unhandled exception has occurred:
Exception = System.NullReferenceException
Message = Object reference not set to an instance of an object.
FullText = System.NullReferenceException: Object reference not set to an instance of an object.
at System.Windows.Forms.ListView.set_TopItem(ListViewItem value)
at System.Windows.Forms.ListView.set_VirtualListSize(Int32 value)
at BrightIdeasSoftware.FastObjectListView.SetObjects(IEnumerable collection) in C:\source\env\ObjectListView\ObjectListView.cs:line 4702
AnswerRe: FastObjectListView Crash
Phillip Piper
9:45 9 Jun '08  
Thanks for the info. I should be able to track down the problem from that -- or at least stop it happening Smile

Regards,
Phillip
GeneralApplication won't close
philippe dykmans
11:20 4 Jun '08  
Hi,

Maybe the answer to my question has already been answered. But then, the list is getting so long it's almost impossible to weed through it.

I've experienced that an application that hosts a ObjectListView sometimes refuses to close. Anyone experienced the same? Any ideas?

Thx,
Phil

Philippe Dykmans
Software developpement
Advanced Bionics Corp.

GeneralRe: Application won't close
Phillip Piper
5:06 8 Jun '08  
Hi Phil,

I have noticed this once in a while too! The really annoying thing is that I have never been able to duplicate it.

For me, it only seems to happen in the OLV demo, not in the other applications that use the ObjectListView.

How are you using the OLV when the application refuses to quit? What sort of OLV (plain vanilla, Virtual, Fast)? What callbacks are you using?

Maybe I can combine your experience with mine to track this down.

Regards,
Phillip
GeneralRe: Application won't close
philippe dykmans
7:01 8 Jun '08  
Good idea.

I use ObjectListView plain vanilla. Both with and without callbacks (so, i think the problem is not in the callbacks). After some testing i think i can say that it only happens when i do in-cell editing. Come to think of it, the problem resembles those cases where databinding refuses to focus away from a control because some property did not validate. I think in those cases the app can also not be closed until the problem is solved. Isn't it?

Grtz,
Philippe

Philippe Dykmans
Software developpement
Advanced Bionics Corp.

GeneralCheckboxes and RowFormatter
rewind187
4:44 4 Jun '08  
I have implemented the following things:

this.objectListView1.CheckStateGetter
this.objectListView1.CheckStatePutter
this.objectListView1.RowFormatter

The problem is that the color of the row depends on the checkbox. So onload everything gets drawn perfectly but if I click on a checkbox i want the rowformatter to do it's work again. How do i accomplish this? I've tried to refresh/setobject the objectlistview in the CheckStatePutter but that makes the app crashFrown

Code:
this.objectListView1.CheckStateGetter = delegate(Object row) {
return ((Task)row).completed;
};

this.objectListView1.CheckStatePutter = delegate(Object row, CheckState newValue) {
if ((newValue == CheckState.Checked))
{
((Task)row).completed = true;
((Task)row).Save();
}
else {
((Task)row).completed = false;
((Task)row).Save();
}
return newValue; // return the value that you want the control to use
};

this.objectListView1.RowFormatter = delegate(OLVListItem olvi)
{
Task task = (Task)olvi.RowObject;

if (task.completed.Equals(true))
{
olvi.Font = new Font("Microsoft Sans Serif",8.25f, FontStyle.Strikeout);
}
else if (task.startDate < DateTime.Now)
{
olvi.ForeColor = Color.Red;
}

};

AnswerRe: Checkboxes and RowFormatter
Phillip Piper
5:00 8 Jun '08  
Hi there,

You should be able to called RefreshObject() within your CheckStatePutter. Obviously another bug I've missed Smile Can you tell what happens when you do that? How does it crash?

In the meantime, you could try invoking the update so it runs slightly after the CheckStatePutter, like this:
this.BeginInvoke((MethodInvoker)delegate { this.RefreshObject(row); });
Hope this helps,
Phillip
GeneralRe: Checkboxes and RowFormatter
rewind187
23:27 8 Jun '08  
Well i found some other strange thing:

Every time the listview refreshes or when the object get set the CheckStatePutter is calles for all fields that have a checked checkbox. I think this is also the reason i can't call refreshobject because it comes in a infinite loop! I manage to avoid this by adding a paramater OVLListItem in the CheckStatePutter delegate, but thats not the most efficient way :P

I'll check if I can find the problem.
GeneralBug in GetAspectByName
philippe dykmans
16:46 3 Jun '08  
First of all... stunning piece of work! Congratulations and thanks for sharing!

Now, in GetAspectByName(...) you navigate the property path to get to the value that must be displayed. But if one property on the path returns null, and if this is not the final property on the path, the source.GetType() call throws a nullreference exception. This does not happen if the property is the very last on the path because GetType() doesn't get called anymore.

This can be corrected like this:

try {
source = source.GetType().InvokeMember( property, flags, null, source, null );
if (source == null)
return null; // or break;
}

And a suggestion: in ValueToString you convert a null value into an empty string. Which is a pretty good solution. But you might consider exposing a NullString property on the OLVColumn object. So that users can choose what they want to display in case of null.

Grtz,
Phil

Philippe Dykmans
Software developpement
Advanced Bionics Corp.

GeneralRe: Bug in GetAspectByName
Phillip Piper
4:58 8 Jun '08  
Hi Phil,

Thanks for this bug report. I'll correct the code in the next version.

Regards,
Phillip
QuestionGetNextItem() help
c_manboy
11:01 2 Jun '08  
Ok, I feel a little slow, but I'm not understanding how to navigate using the objectlistview control. I'm trying to use: Me.ObjectListView1.GetNextItem(), but I need to provide the itemtofind parameter. How do I determine the item to find?

Thanks.
AnswerRe: GetNextItem() help
c_manboy
8:04 3 Jun '08  
Ok, I think I figured it.

Me.ObjectListView1.SelectedItem = Me.ObjectListView1.GetNextItem(Me.ObjectListView1.SelectedItem)


Last Updated 13 Oct 2009 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010