Click here to Skip to main content
Click here to Skip to main content

Extending the ListViewItem

, 14 May 2009
Rate this:
Please Sign up or sign in to vote.
How to extend the ListViewItem to hold an object for a row, and map it to the columns in the ListView control.

ListViewExtendedItem Demo App

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

  1. The custom attribute, ListViewColumnAttribute:
  2. 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")]
  3. ListViewExtendedItem
  4. This is the class that does all the work. It inherits the ListViewItem class and adds the following:

    1. 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.
    2. 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).
    3. 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)
{
    //Clear all the subitems.
    this.SubItems.Clear();
    //Get the type of the data object.
    Type typeOfData = data.GetType();
    //Define this to keep track of whats happening
    bool completed_column = false;
    foreach (ColumnHeader column in listView.Columns)
    {        
        completed_column = false;
        //Get all the properties of the object's type.
        foreach (PropertyInfo pInfo in typeOfData.GetProperties())
        {
            //Get all the custom attributes for the 
            //current property, the use of true here tells
            //the runtime that you wish to check the inherited 
            //class also, this may be usefull if 
            //your objects inherit from a base one.
            foreach (object pAttrib in pInfo.GetCustomAttributes(true))
            {
                //Check to see if the type of the attribute 
                //is that of ListViewColumnAttribute.
                if (pAttrib.GetType() == typeof(ListViewColumnAttribute))
                {
                    //Check to see if the column names coincide.
                    if (pAttrib.ToString() == column.Name)
                    {
                        //Check to see if it has to update it's own 
                        //Text property, or if it has to add subitems.
                        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;
            }
        }
    }
    //Keep the object here so that it can be easyley retrieved 
    //when the user performs some action on the ListView.
    _data = data;
}

First, we clear all the SubItems, 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 ifs 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 ColumnHeaders 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

flavius.stoichescu
Software Developer
Romania Romania
No Biography provided

Comments and Discussions

 
QuestionI have a question.... Pinmemberduybk2838-Mar-12 3:55 
AnswerRe: I have a question.... Pinmemberflavius.stoichescu8-Mar-12 10:18 
GeneralRe: I have a question.... Pinmemberduybk2839-Mar-12 15:02 
GeneralRe: I have a question.... Pinmemberflavius.stoichescu11-Mar-12 15:11 
OK, here's my solution for your problem:
 
Using an attribute on the class, you specify a name of the property that will be used to give the GroupHeader (what you see in the list, there is also a GroupName property for a ListViewGroup but I'm not using that).
 
The attribute code looks like this:
 
class ListViewGroupPropertyAttribute: Attribute
    {
        private string _propName;
 
        public ListViewGroupPropertyAttribute(string propName)
        {
            this._propName = propName;
        }
 
        public override string ToString()
        {
            return _propName;
        }
    }
 
As you can see I am using the same approach I used for the ListViewColumnAttribute.
 
With that, all you have to do is modify the method Update(object, listView) to look like this:
 
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;
                    }
                }
            }
            object[] attrib_array = typeOfData.GetCustomAttributes(typeof(ListViewGroupPropertyAttribute), true);
            string groupName = null;
            bool groupFound = false;
            if (attrib_array.Length > 0)
            {
                string propName = attrib_array[0].ToString();                
                foreach(PropertyInfo pInfo in typeOfData.GetProperties()){
                    if (pInfo.Name == propName)
                    {
                        groupName = pInfo.GetValue(data, null).ToString();
                        break;
                    }
                }
                if (groupName == null)
                {
                    groupName = "Unkown";
                }
            }
            else
            {
                groupName = "Unknown";
            }
            foreach (ListViewGroup lvGroup in listView.Groups)
            {
                if (lvGroup.Header == groupName)
                {
                    this.Group = lvGroup;
                    groupFound = true;
                    break;
                }
            }
            if (!groupFound)
            {
                ListViewGroup lvGroup = new ListViewGroup(groupName);
                listView.Groups.Add(lvGroup);
                this.Group = lvGroup;
            }
            _data = data;
        }
 
I left the code for the columns in there, the code relevant to groups starts at
object[] attrib_array = typeOfData.GetCustomAttributes(typeof(ListViewGroupPropertyAttribute), true);
 
This is what the code does:
1. Looks for a ListViewGroupPropertyAttribute that was given to the object you are displaying.
2. Reads the name of the property and searches the object for it.
3. Gets the content of the property and uses that string to search for an existig group in the ListView.
4a. If it finds the property and finds the group with the same header string as the property contents, the item is assigned to the group.
4b. If it finds the property but no group, it will create the group, put it in the ListView and assign the item to it.
4c. If it doesn't find the property then it creates (if it doesn't exist already) a group called "Unknown" and assigns the item to it.
 
I hope this will help you, please try the code and tell me if you wanted a different behavior.
 
The object you store in the ListView should have something like this:
 
 [ListViewGroupProperty("Name")]
    public class TestClass
    { 
 
        ...
 
        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }
 
        ...
 
 }
 

GeneralRe: I have a question.... Pinmemberduybk28312-Mar-12 7:03 
GeneralVery common idea PinmemberPhillip Piper12-Aug-09 22:34 
GeneralRe: Very common idea Pinmemberflavius.stoichescu6-Jan-10 10:01 
GeneralTake a look at this PinmemberZoran Gelic14-May-09 6:05 
GeneralRe: Take a look at this Pinmemberflavius.stoichescu28-May-09 10:27 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140721.1 | Last Updated 14 May 2009
Article Copyright 2009 by flavius.stoichescu
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid