Wednesday, April 28, 2004

ListItemWrapper

Here's a little trick that I use all the time. It's simple but handy.


I often find that I want to have a ListBox full of some particular item - to be completely boring, let's say it's a list of Customer objects. We only want to display the customer's name, but it's handy to store the whole Customer object in the ListBox, rather than just the customer's name, because it saves us having to go through the trouble of looking up a Customer based on their name. And of course .NET completely supports this, since you can add any object you want to the ListBox.


If you were to code up a simple implementation of Customer like this one:


public class Customer {
  private string name;
  private int age;

 
public Customer(string name, int age) {
    this.name = name;
    this.age = age;
  }

  public string Name {
    get { return name; }
    set { name = value; }
  }

  public int Age {
    get { return age; }
    set { age = value; }
  }
}


and if you were to then shove a bunch of these into a ListBox, you might be a bit surprised to find the list box displays something like MyNamespace.Customer for every object - hardly what we want.


The reason for this is simple: when confronted with an object, the ListBox displays it by calling ToString on it. And the default implementation of ToString simply returns the name of the type. Not particularly useful in most situations, but what else could they do?


Of course, this is simple to fix if we're the ones that own the Customer object. Simply adding an override of ToString that returns the name would do it. But what if someone else wrote the object and we don't have the source code? Or what if ToString returns something we don't like? I've run into this enough times that I've found the following trick worth including in my standard bag of tricks. I simply write a class that looks like this:


public class ListItemWrapper {
  private string text;
  private object item;

 
public ListItemWrapper(string text, object item) {
    this.text = text;
    this.item = item;
  }

  public string Text {
    get { return text; }
    set { text = value; }
  }

  public object Item {
    get { return item; }
    set { item = value; }
  }

  public override string ToString() {
    return text;
  }

}


Now, when I want to store a Customer in a list, what I do is this instead:


listBox1.Items.Add(new ListItemWrapper(“Craig“, new Customer(“Craig“, 32)));


Which allows me to have any arbitrary text associated with any arbitrary item. Because ToString is overridden to return the text we choose for each object, the ListBox will display what we want it to. And retrieving the item is as simple as casting to ListItemWrapper and then just accessing the Item property.

13 comments:

  1. All I have to say:

    REALLY COOL! :-)

    ReplyDelete
  2. Isn't this much simpler though:

    listBox1.DisplayMember = "Name";

    Of course that only works if you're using Windows Forms - ASP.NET's ListBox doesn't support this. But why wouldn't you use Windows Forms? ;-)

    ReplyDelete
  3. Yep, using DisplayMember works great...if there happens to be a member that you want to display. Probably 90% of the time, that works very well.

    But here's a scenario where this approach really shines: the ability to include null values in the list to indicate, "I don't want to select anything." Since we can assign text to any value, it allows for (IMO) natural use of null, but still gives us a way to display it.

    Similarly for list boxes that contain mixes of different types - Customers and Managers. Allows you to display things like:

    Manager: Chris Sells
    Customer: Ian Griffiths
    Customer: Craig Andera

    in the listbox.

    Again, these cases are rarer than the cases handled by DisplayMember, but something I've run into often enough to be useful.

    ReplyDelete
  4. I used the Tag property (inherited from Control) for a similar purpose. But your way to wrap an object and supply a ToString() looks interesting.

    ReplyDelete
  5. I believe you're thinking of ListView, which has a much more sophisticated programming model. ListBox doesn't have a Tag property for individual list items.

    ReplyDelete
  6. Very nice. Thanks!

    ReplyDelete
  7. Another solution would be to make the wrapper ask for the information from the customer object. That is: public string Text { get { return item.Firstname + " " + item.Lastname; }

    The benefit is that when you want to display "Lastname, Firstname" instead of just lastname, you only need to make the change to the wrapper.

    ReplyDelete
  8. It also has the unfortunate side effect of making the wrapper non-reusable.

    But you're right: the code that says, "The item is named by combining the first name and last name" has to go somewhere. Whether that's in a wrapper or the app that uses the wrapper is going to be determined by the architecture of the application.

    ReplyDelete
  9. Yes, but what if you want to have Chris Sells naked photo display in your listbox?

    ReplyDelete
  10. Seems like a great place for generics when available:

    public class ListItemWrapper< T >
    {
    private string text;
    private T item;

    public ListItemWrapper(string text, T item) {this.text = text; this.item = item;
    }

    public string Text {get { return text; } set { text = value; }

    }

    public T Item {get { return item; } set { item = value; }
    }

    ...

    }

    ReplyDelete
  11. Agreed - any time you see "object", a generic is often a nice improvement. Now, if only it were 2006... :)

    ReplyDelete
  12. How can I retrieve the items from the list?



    Thanks, Ada

    ReplyDelete
  13. foreach (ListItemWrapper item in listBox1.Items)

    {

    // Do something here

    }



    :)

    ReplyDelete