Introduction
Hello, this is my first article here, so please, if you can, bare with me. Also, my English can be "different", and I apologize for any mistakes.
OK, first of all, the problem: I needed to map an object's properties to a set of ListView
columns and store the object itself within a ListViewItem
. Why? Because one of the greatest things of OOP is that you can create a class that would contain a set of data and methods that operate with that data, and use it in a very elegant way. Because I like working with objects instead of any other method of storing data, I wanted to make the ListViewItem
hold an object and automatically map its properties to a set of columns from a ListView
control.
The solution: The ListViewExtendedItem
class. This class inherits ListViewItem
s, and adds a new property, a different constructor, and a (two if you count the overload) method.
How it Works?
Well, because I make my own classes that hold the data I'm interested in keeping, I have full control over how I build them up. Also, I have complete control over what columns to display and their order. Because of this, I can use custom attributes to mark the properties of the data classes with the corresponding column name. A simple solution, but with a big disadvantage: no other class may be used except those that have been written with this custom attribute.
- The custom attribute,
ListViewColumnAttribute
:
This is the attribute we use to mark the properties with the column name where they should be mapped to. It is a very basic attribute, as you can see below. It only has one constructor that takes a string as a parameter (the column name) and overrides the ToString()
method so that when we later check for the column name, it is a bit easier.
public class ListViewColumnAttribute : Attribute
{
private string _columnName;
public ListViewColumnAttribute(string columnName)
{
_columnName = columnName;
}
public override string ToString()
{
return _columnName;
}
}
So far so good, nothing too fancy going on. We will use this attribute when we write our classes like this:
[ListViewColumn("some column")]
ListViewExtendedItem
This is the class that does all the work. It inherits the ListViewItem
class and adds the following:
- A very different constructor,
ListViewExtendedItem(ListView parentListView, object data)
, where parentListView
is the ListView
control that will hold the item. Its columns have to be setup before you create any extended item, and data
is the object you want to add to display.
- A property,
Data
of type object
that holds the object itself; useful if your objects are able to perform several tasks (for instance, my objects are able to print themselves; all I have to do is add an extended item to the ListView, and a button that calls the Print()
method of the object, for each selected item in the ListView
).
- The
Update
method with two overloads, Update(object data)
and Update(object data, ListView listView)
, that does the actual mapping from the object's properties to the columns. The first overload takes only one argument, a new (updated) object that has to be displayed. It relies on the ListView
property from the base ListViewItem
class to get its column information. If it is no set (the item has not yet been added to a ListView
) it will throw an exception. The second overload asks for an explicit ListView
.
OK, the actual code that does all the work is contained within the body of the second overload of the Update
method. Both the constructor and the first overload calls this one to do the job. Here is the code:
public void Update(object data, ListView listView)
{
this.SubItems.Clear();
Type typeOfData = data.GetType();
bool completed_column = false;
foreach (ColumnHeader column in listView.Columns)
{
completed_column = false;
foreach (PropertyInfo pInfo in typeOfData.GetProperties())
{
foreach (object pAttrib in pInfo.GetCustomAttributes(true))
{
if (pAttrib.GetType() == typeof(ListViewColumnAttribute))
{
if (pAttrib.ToString() == column.Name)
{
if (column.DisplayIndex == 0)
{
this.Text = pInfo.GetValue(data, null).ToString();
completed_column = true;
break;
}
else
{
this.SubItems.Add(pInfo.GetValue(data, null).ToString());
completed_column = true;
break;
}
}
}
}
if (completed_column)
{
break;
}
}
}
_data = data;
}
First, we clear all the SubItem
s, so there won't be any other columns besides the ones needed. Then, we get the Type
of the data object, and we define a bool
variable that will help optimize the method a bit. Then, for each column the provided ListView
control has, we have to check the object's properties if one of them has been marked with a custom attribute of type ListViewColumnAttribute
and if the attribute has the same column name as the column we are currently searching for. The code is not really pleasant, three nested foreach
and some if
s along the way are not pleasant to the eyes. However, it does the job done, and for the time being, I cannot think of another way to check the members of a type. After one member is found that has been marked with a matching attribute and the column name matches the one stored by the attribute, we proceed to check if the column we are populating is the first one (DisplayIndex=0
) because the item's own text property is displayed, instead of the text held by a SubItem
.
Notice that there is an if
statement checking the value of completed_column
. This is to ensure that after a match has been found, the code does not linger and search any more, but goes to the next column. After all the columns are done, the internal _data
object is assigned the one passed to the function.
This is pretty much all there is to it. The ListViewExtendedItem
can be added to the Items
collection of the ListView
control, and it will display the marked properties on the right column.
Limitations/Annoying Things
The first obvious annoying thing is that when you retrieve an extended item, you have to type cast it to ListViewExtenededItem
in order to make use of it, and also, you have to unbox the Data
property to access its members. This can all be resolved with a generic implementation of the class; however, for the time being, several things about generics escape me.
Note that I have not tested if this works while using Virtual Items in the ListView
, but it should work, I don't see what would break it... Also, if the data object properties have some obscure type that does not override the ToString()
method, the results are not going to be very helpful. This can be solved by using attributes again to store the name of a certain field you want to show from that obscure object (maybe add an interface that would provide a method to retrieve a property name so that it can be changed at runtime).
And, the last annoying thing, Visual Studio won't update the Name
property of the ColumnHeader
s used in the ListView
, you have to set them up in code.
Source Code and Demo App
In the download (link above), you will find a demo app that makes use of this class. The usage is pretty straightforward. Use the New button to create a new object instance, modify its properties with the property grid, then click Add to save it in the list view. If you click on an item in the list view, you will see the data object's properties in the property grid. After you modify them, click on Update to save the changes and display it in the list view control.
The ListViewColumnAttribute
can be found in the file with the same name, and the same goes for the ListViewExtendedItem
class.
Thanks for reading this article. I hope it helps. Let me know if you like/don't like something about the extended item class.