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.
TreeListView works best when there is a significant overlap of
information between branches and the leaf nodes. They have to share the
same columns. They also work when the branches have no information apart
from their name and the columns are really only used for the leaf
nodes. They do not work so well when you want to show several bits of
information about branches and other bits of information about leaf
nodes, with little overlap between the two. They look silly because of
all the empty cells.
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 an
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 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 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.
4.3 Reports from ListViews

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:
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.
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:
- 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.
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 cells 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 cells 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 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
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 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 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:
- Can the currently dragged objects be dropped at the current location?
- 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:
- The
ObjectListView
that initiated the drag
- The model objects are being dragged
- The model object is current target of the drop
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 clickable link on the right), and footers. When done well,
this can make your listview look very nice indeed:

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 the EmptyListMsgFont. Both of these properties can be configured within in the IDE.
But if you want to write a little bit of code, you can have much more
interesting messages. The empty list message 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 hearts 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;
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.
For each state, the header format allows the font, font
color, background color and frame to be specified. If you combine these
attributes badly, you can produce some truly dreadful designs, but when well
used, the effect can be pleasant.
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.
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:
This reuses the same ObjectListView control,
but now it is a fully functional ObjectListView showing information
about Forex sales.