There are four flavours of ObjectListView:
Stock standard, but still very nice
A normal ObjectListView
is capable of almost anything that a ListView
can do. It is better suited for smaller numbers of rows (<10,000).
If in doubt, start with this flavour. You can always change it to one of the others later on.
Everything is just smooth and easy
A DataListView
is for the ultra-slothful: those who literally do not want to even write one line of code.
A DataListView
can be given a DataSource
from within the IDE, and it will automatically keep itself sync with that DataSource
. Further, if the DataListView
is marked as editable, edits will be automatically written into the DataSource
.
Has a slight bitter taste, but makes you capable of anything
When you want to knock your users out with 10 million search results in a ListView
, a VirtualObjectListView
is for you. It can handle any number of objects -- but at a cost:
IVirtualListDataSource
interface.But for the cost of this "bitterness," you really can do anything.
Also makes you capable of anything, but without the bitterness
When you want speed, speed, and more speed, but you don't want the bitterness of the VirtualObjectListView
, a FastObjectListView
is your weapon of choice.
You still can't use groups or Tile view, and you can't install a custom sorter, but otherwise it operates just like a normal ObjectListView
-- only much faster.
Did I mention the speed?
Adds a distinctive flavour
If you have a tree structured data model, and you want to show common information about the branches and the leaves of the structure, a TreeListView
is at your disposal.
This really needs a page to itself: Cell Editing in an ObjectListView
There are a couple of ways to change the standard sorting behavior:
BeforeSorting
event, do whatever you like, and then set Cancelled
to true
on the event so that no standard processing is done.ObjectListView
or a DataListView
, you can install a CustomSorter
delegate.VirtualObjectListView
(or one of its subclasses FastObjectListView
or TreeListView
), you cannot use the CustomSorter
. You have to implement the SortObjects()
method of the IVirtualListDataSource
. The CustomSorter
delegates needs a little further explanation. This delegate must install a ListViewItemSorter
on the ObjectListView
. This installed item sorter will actually do the work of sorting the ListViewItems
. See ColumnComparer
in the code for an example of what an item sorter has to do. NOTE: This delegate does not sort the model objects themselves. Remember this control knows nothing about your model objects, so sorting them makes no difference to the control. You have to install the ListViewItemSorter
.
So if you were using a normal ObjectListView
to show a collection of medical incidents, and you wanted to sort them so that the emergencies were always shown first, you could install a delegate like this:
this.incidentListView.CustomSorter = delegate(OLVColumn column, SortOrder order) { this.incidentListView.ListViewItemSorter = new ColumnComparer( this.isEmergencyColumn, SortOrder.Descending, column, order); };
This says, sort the items by the value in the "Is Emergency?" column first, and secondarily by the column that the user just clicked on. Remember that the OLVColumn
given to the ColumnComparer
does not have to an active column in the control. You can create one just for the custom sorter. So, even if there was no "Is Emergency?" column, you could still sort by the incidents so that emergencies were shown first:
this.incidentListView.CustomSorter = delegate(OLVColumn column, SortOrder order) { this.incidentListView.ListViewItemSorter = new ColumnComparer( new OLVColumn("ignored", "IsEmergency"), SortOrder.Descending, column, order); };
And if you were using a VirtualObjectListView
to show a collection of 10 million mailing addresses, and you wanted to sort them in some fashion, you would implement the SortObjects()
method of the IVirtualListDataSource
like this:
class MyMailingAddressesDataSource : AbstractVirtualListDataSource { ... override public void SortObjects(OLVColumn column, SortOrder order) { SortMailingAddressBy(column.AspectName, order); }; };
One incompatibility between v1.x and v2.0 is that FastObjectListViews
can no longer have a CustomSorter
. In v1.x it was possible, if tricky, to get a CustomSorter
to work with a FastObjectListView
, but that is no longer possible in v2.0 In v2.0, if you want to custom sort a FastObjectListView
, you will have to subclass FastObjectListDataSource
and override the SortObjects()
method, then install that customized data source into your FastObjectListView
.
class MyCustomSortingDataSource : FastVirtualListDataSource { ... override public void SortObjects(OLVColumn column, SortOrder order) { // do some amazing sorting here this.RebuildIndexMap(); // you must call this otherwise the control will not work properly }; }; ... this.myFastObjectListView.DataSource = new MyCustomSortingDataSource(this.myFastObjectListView);
This needs a page to itself as well: How to owner draw my values?
Columns have both a MinimumWidth
and a MaximumWidth
properties. By default these are -1, which means that no limit is enforced. But if they are set to some other value, the column will be limited to the given minimum or maximum.
For example, setting this.titleColumn.MinimumWidth = 30;
will stop the "Title" column from becoming less than 30 pixels in width. This is useful for preventing users from shrinking columns to 0 width and then not being about to find them again.
There are some columns just don't make sense to be resizable. A column that always shows a 16x16 status icon makes no sense to be resizable. To make a column be fixed width and unresizable by the user, simply set both MinimumWidth
and MaximumWidth
to be the same value.
On most columns, the column's width is static, meaning that it doesn't change by itself. But sometimes it would be useful if a column would resize itself to show more (or less) of itself when the user changed the size of the ListView. For example, the rightmost column of a personnel list might display "Comments" about that person. When the window was made larger, it would be nice if that column automatically expanded to show more of the comments about that person. You can make this happen by setting the IsSpaceFilling
property to True
on that column.
An ObjectListView
can have more than one space filling column, and they generally share the available space equally between them (see the FreeSpaceProportion
property to change this).
You should be aware that as the ObjectListView
becomes smaller, the space filling columns will become smaller too, until they eventually disappear (have zero width). The MinimumWidth
and MaximumWidth
properties still work for space filling columns. So you can use the MinimumWidth
property to make sure that a space filling column doesn't disappear.
You install a RowFormatter
delegate. A RowFormatter
delegate is called after a OLVListItem
has been completely filled in, but before it is added to the ObjectListView
. It can change the formatting of the list item, or any of its other properties.
To show customers in red when they owe money, you could do this:
this.customerListView.RowFormatter = delegate(OLVListItem olvi) { Customer customer = (Customer)olvi.RowObject; if (customer.Credit < 0) olvi.ForeColor = Color.Red; };
You can format sub-items too, but it is a little more involved. There is no separated callback for individual sub-items. So the RowFormatter
can be written to format sub-items too.
So, if in our above example, we only want to make the actual credit balance to be red, not the whole row, the RowFormatter
would look like this:
this.customerListView.RowFormatter = delegate(OLVListItem olvi) { Customer customer = (Customer)olvi.RowObject; if (customer.Credit < 0) { int i = this.customerListView.Columns.IndexOf(this.creditColumn); if (i >= 0) { olvi.UseItemStyleForSubItems = false; olvi.SubItems[i].ForeColor = Color.Red; } } };
This code doesn't assume a position for the credit column but finds it each time. This is conservative and it could be replaced with a constant -- if you can guarantee that no other fool is going to move or remove it :-)
The other important thing to note is the olvi.UseItemStyleForSubItems = false
. By default, all subitems have the same formatting as the listitem itself. To indicate that you want subitems have their own formatting, you must set UseItemStyleForSubItems
to false
.
You make a ListViewReporter
object in your IDE, and you set the ListView
property to be the ObjectListView
you want to print.
There is a whole article available on CodeProject explaining in detail how to do this: Turning a ListView into a nice report. Read the article and play with demo to see how it works.
To uses checkboxes with an ObjectListView
, you must set the CheckBoxes
property to True
. Then you can either use CheckedAspectName
or you can install CheckStateGetter
and CheckStatePutter
delegates.
CheckedAspectName
When your model object already has a property that directly matches whether or not a row should be checked, a CheckedAspectName
is the simplest approach. Simply set the CheckedAspectName
to the name of your property, and the ObjectListView will handle everything else, both the getting and the setting of this property's value. The property must be a boolean property.
If CheckedAspectName
is too simple for your needs, you can install CheckStateGetter
and CheckStatePutter
delegates. The first delegate is used to decide if the checkbox on the row that is showing the given model object should be checked or unchecked. The second is called when the user clicked the check box.
There are two flavour of check state getter/putters. there are CheckStateGetter
and CheckStatePutter
delegates which deal with CheckStates
; and there are BooleanCheckStateGetter
and BooleanCheckStatePutter
delegates which deal only with booleans
. If you are only interested in checkboxes being on or off, the boolean
versions are what you want. However, if you want to deal with indeterminate values too, you must use the CheckState
versions.
this.objectListView1.BooleanCheckStateGetter = delegate(Object rowObject) { return ((Person)rowObject).IsActive; }; this.objectListView1.BooleanCheckStatePutter = delegate(Object rowObject, bool newValue) { ((Person)rowObject).IsActive = newValue; return newValue; // return the value that you want the control to use };
Note that the CheckStatePutter
returns the value that will actually be used. This doesn't have to be the same as the value that was given. So your delegate can refuse to accept the checking of a particular model if it wants.
The .NET ListView
cannot have CheckBoxes
cannot on virtual lists. But, as of v1.13 (July 2008), I have changed VirtualObjectListView
(and thus FastObjectListView
and TreeListView
) so it can support checkboxes. So now all flavours of ObjectListView
support checkboxes equally.
The only caveat for using check boxes on virtual lists is that, when a CheckStateGetter
is installed, the control has to iterate the entire list when the CheckedObjects
property is read. Without a CheckStateGetter
, the control assumes that nothing is checked until the user (or the programmer) explicitly checks it. So it knows which objects have been checked and can simply return them as the value of the CheckedObjects property. But when a CheckStateGetter
is installed, the only way the control can know whether an object is checked is by calling the CheckStateGetter
delegate. So to return the value of CheckedObjects
property, the control must iterate the whole list, asking in turn if this object is checked. This is fine if the list has only 100 or even 1000 objects, but if the list has 10,000,000 objects, your program is going to hang.
The information that is shown when in Tile
view is customisable. The primary column has to appear, but the other "rows" are configurable. In the example below, the Person's name appears, since that is the primary column, but the "Occupation", "Birthdate", and "Hourly Rate" pieces of information are shown as well.
To do this, set IsTileViewColumn
to true
for those columns that you want to appear in the Tile
view. Confusingly, a column in Detail
view becomes a "row" in a Tile
view.
If you really want to change the information in the Tile view, you can custom draw it! To do this, install a Renderer
on the primary column of the list and set OwnerDraw
to true. See BusinessCardRenderer
in the demo project for an example implementation. To see that renderer in action, run the demo, switch to the "Complex" tab, click the "Owner Drawn" checkbox, and switch to Tile view.
The EmptyListMsg
is the property that holds the string that appears when an ObjectListView
is empty. This string is rendered using the EmptyListMsgFont
.
this.objectListView1.EmptyListMsg = "This database has no rows"; this.objectListView1.EmptyListMsgFont = new Font("Tahoma", 24);
No, you can't owner draw it. Don't even think about it.
Normally, images that are shown on rows come from an ImageList
. The ImageGetter
delegate simply returns the index of the image that should be drawn against the cell. However, sometimes, the images that should be drawn are not known at compile time. Or they are generated dynamically from some characteristic of the model object being displayed. In such cases, the ImageList
cannot be pre-populated with the images to be used. But with a little planning, you can still use your ImageList
to manage your images, even when the Images
are dynamically retrieved.
First, give your ObjectListView
an empty SmallImageList
and an empty LargeImageList
.
Secondly, install an ImageGetter
delegate on your primary column that does something like this:
this.mainColumn.ImageGetter = delegate(object row) { String key = this.GetImageKey(row); if (!this.listView.LargeImageList.Images.ContainsKey(key)) { Image smallImage = this.GetSmallImageFromStorage(key); Image largeImage = this.GetLargeImageFromStorage(key); this.listView.SmallImageList.Images.Add(key, smallImage); this.listView.LargeImageList.Images.Add(key, largeImage); } return key; };
This dynamically fetches the images if they haven't been already fetched. You will need to write the GetImageKey()
, GetSmallImageFromStorage()
and GetLargeImageFromStorage()
methods. Their names will probably be different, depending on exactly how you are deciding which image is shown against which model object.
For example, if we were writing a File Explorer look-a-like, we might have something that looks like this:
this.mainColumn.ImageGetter = delegate(object row) { File theFile = (File)row; String extension = this.GetFileExtension(theFile); if (!this.listView.LargeImageList.Images.ContainsKey(extension)) { Image smallImage = this.GetSmallIconForFileType(extension); Image largeImage = this.GetLargeIconForFileType(extension); this.listView.SmallImageList.Images.Add(extension, smallImage); this.listView.LargeImageList.Images.Add(extension, largeImage); } return key; };
If you only use Details
view, you don't need to maintain the LargeImageList
, but if you use any other view, you must keep the SmallImageList
and the LargeImageList
in sync.
Yes. You can use a TypedObjectListView
wrapper.
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, this is not possible, so we have a TypedObjectListView
class instead. 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); };
A side benefit 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".
A TreeListView
shows a tree structure with its nice ability to expand and collapse, but also shows information in columns.
Like all the other ObjectListViews
, TreeListView
relies on delegates. The two essential delegates for using a TreeListView
are:
CanExpandGetter
is used to decide if a given model can be expandedChildrenGetter
is used to gather the children that will appear under a given model after it is expanded. This delegate is only called if CanExpandGetter
has returned true for that model object. 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 says that only directories can be expanded.
this.treeListView.ChildrenGetter = delegate(object x) { DirectoryInfo dir = (DirectoryInfo)x; return new ArrayList(dir.GetFileSystemInfos()); };
This second delegate gets the contents of a directory when that directory is expanded. Remember, ChildrenGetter
delegates are only ever called if CanExpandGetter
returns true, so this delegate knows that the parameter x
must be a DirectoryInfo
instance.
Once you have these two delegates installed, you populate the control by setting its Roots
. Roots are the top level branches of the tree. You can use the Roots
property to set these top branches, or you can call SetObjects()
, which also sets the collection of top level brances. To add or remove these top level branches, you can call AddObjects()
and RemoveObjects()
, since in a tree view, these operate on the top level branches.
The TreeListView
caches the list of children under each branch. This is helpful when the list of children is expensive to calculate. To force the TreeListView
to refetch the list of children, call RefreshObject()
on the parent.
I have a list that shows medical incidents. One of the columns is the doctor reponsible for that incident. I'd like the users to be able to sort by the "Doctor" column and then type the first few characters of the doctors name and find the cases assigned to that doctor. Is there a way to do that?
Surprisingly, yes! If you set IsSearchOnSortColumn
to true
, then characters typed into the list will be matched against the values of the sort column, rather than against the values of column 0. iTunes shows this behavior when you sort by the "Artist" or "Album" columns.
Remember: this searching works on the string representation of the value, rather than on the values themselves.
The ListView's default behavior is to only use tool tips to show truncated cell values (even then only when FullRowSelect
is true). But with an ObjectListView, you are not so limited.
To show a different tooltip when the mouse is over a cell, you need to install a delegate (no prizes for guessing that one). The tool tip for cells is controlled by the CellToolTipGetter
delegate. If this returns null, the tool tip behaves as normal, but if it returns a string, that string will be shown while the mouse hovers over that cell. Inserting "\r\n" sequences into the string gives a multiline tool tip. The example below shows a multiline tool tip only if the Control
key is held down:
// Show a long tooltip over cells only when the control key is down
this.olv.CellToolTipGetter = delegate(OLVColumn col, Object x) {
if (Control.ModifierKeys == Keys.Control) { Song s = (Song)x;
return String.Format("{0}\r\n{1}\r\n{2}", s.Title, s.Artist, s.Album);
} else
return null;
};
Similarly, the column headers can show their own tool tips, this time via the HeaderToolTipGetter
delegate.
Instead of installing a delegate, you can set the ToolTipText
property on a column. This is useful if the tool tips for the column headers don't change. If you install a HeaderToolTipGetter
delegate, it takes precendence over the ToolTipText
properties, which will be ignored.