Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

Customized ListBox Control: Part 1 - Collection Events

4.25/5 (6 votes)
11 Jul 2008CPOL7 min read 1   441  
Create a listbox control that provides events for when adding or removing items.

CustomListBox_Part1

Introduction

In this small article series, I will explain how to write a customized listbox control with some extended features that I believe most developers will find useful. Apart from the specific features being explained, these articles will also present an idea of how you can go about making your own custom components, providing you with your very own additions to the WinForms control toolbox. In this first article, I will address the collection events issue, and more specifically, how to add event notification when adding or removing items.

Many of the .NET WinForms controls that keep a collection of items are missing the vital events for when the collection changes. Normally, you would have to work this out using a data binding or a custom data source, but sometimes, it would just be nice to have the events wired to the control itself. So, we will be looking at this along with the basics for deriving your custom component. A note on the code; I decided to use VB.NET (even though I am more of a C# guy) because I've seen more questions concerning this subject on VB.NET forums.

Creating a derived component

The classic ListBox control has its implementation, and we are not really able to change it much. However, we can create our own control and inherit all the functionality from the classic ListBox control. We could take the long way by creating a User Control and implementing all the features from scratch, but that would take countless hours of developing, and that's not really the point here. What we want is to keep the basic functionality and only change the behavior for some selected features. If you are familiar with the concepts of inheritance, you will have no trouble understanding this. What you may not have thought about, however, is that class inheritance works the same way with controls. After all, a control such as the Label, is a class like any other class in which you would write your code.

Let's try this out with a little example:

  1. Create a new Windows application.
  2. Add a new class to your project and name it MyLabel.
  3. Open the code for the class and insert the sentence "Inherits Label" as shown in the code snippet below.
  4. Compile the solution.
VB
Public Class MyLabel
    Inherits Label
End Class 

If you open up the Form1 designer and check out the toolbox, you will find a new tool at the top called MyLabel. Now, this control is just like the classic Label control. You can put it on the Form1 surface and set the properties for it. It's really no different from the Label control. Why is that? It's because we're inheriting the Label control and all of its functionality, or put another way, we are deriving from Label.

So, now that you are familiar with the concepts of making your own control, let's get started with our CustomListBox control.

New items property interface

Our first goal with implementing the CustomListBox control is to be able to generate events for when items are added or removed. Looking at our base class, the ListBox control, we can see that all items are stored in a ListBox.ObjectCollection class that is accessed through the Items property. What we are going to do in our CustomListBox is to create a new class that will serve as the new Items interface. This class will be what the users of our CustomListBox control will be accessing when they work on the collection. Let's have a look at the code for this class.

VB
Public Class CollectionObjectInterface
       Implements IEnumerable

    Private owner As CustomListBox

    Friend Sub New(ByVal owner As CustomListBox)
        Me.owner = owner
    End Sub

    Public Sub AddRange(ByVal items() As Object)
        For Each item As Object In items
            Me.Add(item)
        Next
    End Sub

    Public Sub AddRange(ByVal value As ListBox.ObjectCollection)
        For Each item As Object In value
            Me.Add(item)
        Next
    End Sub

    Public Sub Add(ByVal item As Object)
        Me.Insert(Me.Count, item)
    End Sub

    Public Sub Insert(ByVal index As Integer, ByVal item As Object)
        Me.owner.InnerItems.Insert(index, item)
        Me.owner.OnItemAdded(index)
    End Sub

    Public Sub Remove(ByVal item As Object)
        Dim index As Integer = Me.IndexOf(item)
        If (index > -1) Then
            Me.RemoveAt(index)
        End If
    End Sub

    Public Sub Clear()
        For i As Integer = (Me.Count - 1) To 0 Step -1
            Me.RemoveAt(i)
        Next
    End Sub

    Public Sub RemoveAt(ByVal index As Integer)
        Me.owner.InnerItems.RemoveAt(index)
        Me.owner.OnItemRemoved(index)
    End Sub

    Public Function Contains(ByVal item As Object) As Boolean
        Return Me.owner.InnerItems.Contains(item)
    End Function

    Public Sub CopyTo(ByVal destination() As Object, ByVal arrayIndex As Integer)
        Me.owner.InnerItems.CopyTo(destination, arrayIndex)
    End Sub

    Public Function GetEnumerator() As System.Collections.IEnumerator _
                    Implements IEnumerable.GetEnumerator
        Return Me.owner.InnerItems.GetEnumerator()
    End Function

    Public Function IndexOf(ByVal value As Object) As Integer
        Return Me.owner.InnerItems.IndexOf(value)
    End Function

    Public ReadOnly Property Count() As Integer
        Get
            Return Me.owner.InnerItems.Count
        End Get
    End Property

    Public Property Item(ByVal index As Integer) As Object
        Get
            Return Me.owner.InnerItems(index)
        End Get
        Set(ByVal value As Object)
            Me.owner.InnerItems(index) = value
        End Set
    End Property

End Class

The ObjectCollectionInterface has all the public members of the ListBox.ObjectCollection class. You could say it is a copy of the ListBox.ObjectCollection class, although the implementation is not the same. This is because we want the user to be able to use the Items property in the same way as with the ListBox control. For certain reasons, which are beyond the scope of this article to address, we do not inherit the ListBox.ObjectCollection. Therefore, we have to fully implement all the members.

The idea of this class is to provide a layer between the user and the base class collection of items, so that we can raise the ItemAdded and ItemRemoved events. It doesn't store any items in itself, but simply calls the methods of the original collection. The CustomListBox will own this class, so in the constructor, we pass a reference to the parent CustomListBox. The CustomListBox will expose the base class (ListBox) Items collection via a property called InnerItems. The ObjectCollectionInterface will use this property to delegate all work being done on the Items collection. As you can see, all the method calls that have to do with adding or removing items are routed to the Insert and RemoveAt methods in which we will perform the appropriate action on the original collection, and raise the events.

The CustomListBox control

The CustomListBox control should be responsible for providing helper methods to raise the ItemAdded and ItemRemoved events, implement a private property to expose the base class items collection to the ObjectCollectionInterface class, and provide a new implementation of the Items property that uses the ObjectCollectionInterface. Let's have a look at these three objectives in code:

VB
''' <summary>
''' Represents a ListBox with events for when adding and removing items. 
''' </summary>
Public Class CustomListBox
    Inherits ListBox

    ' Omitted the code for the internal classes ObjectCollectionInterface and 
    ' ListBoxItemEventArgs...


    Public Event ItemAdded(ByVal sender As Object, ByVal e As ListBoxItemEventArgs)
    Public Event ItemRemoved(ByVal sender As Object, ByVal e As ListBoxItemEventArgs)

    Private itemsInterface As CollectionObjectInterface

    ''' <summary>
    ''' Constructor. Creates a new CollectionObjectInterface that references
    ''' this CustomListBox instance.
    ''' </summary>
    Public Sub New()
        MyBase.New()

        Me.itemsInterface = New CollectionObjectInterface(Me)
    End Sub

    ''' <summary>
    ''' Gets the items of the System.Windows.Forms.CustomListBox.
    ''' </summary>
    ''' <returns>An ObjectCollectionInterface representing the items in 
    ''' the CustomListBox.</returns>
    ''' <remarks>Hides the ListBox Items interface in favor
    '''    of our own interface.</remarks>
    Public Shadows ReadOnly Property Items() As CollectionObjectInterface
        Get
            Return Me.itemsInterface
        End Get
    End Property

    ''' <summary>
    ''' Gets the base Items collection. This property exposes the ListBox's 
    ''' original Items collection to the ObjectCollectionInterface.
    ''' </summary>
    Private ReadOnly Property InnerItems() As ListBox.ObjectCollection
        Get
            Return MyBase.Items
        End Get
    End Property

    ''' <summary>
    ''' Fires the ItemAdded event.
    ''' </summary>
    ''' <param name="index">Specifies the zero-based index for the item 
    ''' that was added.</param>
    Protected Overridable Sub OnItemAdded(ByVal index As Integer)
        RaiseEvent ItemAdded(Me, New ListBoxItemEventArgs(index))
    End Sub

    ''' <summary>
    ''' Fires the ItemRemoved event.
    ''' </summary>
    ''' <param name="index">Specifies the zero-based index for the item 
    ''' that was removed.</param>
    Protected Overridable Sub OnItemRemoved(ByVal index As Integer)
        RaiseEvent ItemRemoved(Me, New ListBoxItemEventArgs(index))
    End Sub

End Class

Apart from the fact that we are inheriting the ListBox control, the most interesting thing about the CustomListBox class is the declaration of the Items property. Remember that we are inheriting an Items property from our base class, ListBox, and we would have no use for our ObjectCollectionInterface class if the user would access that property. So, in order to take advantage of our own interface, we must hide the base class property. To hide a base class member, we have to use the Shadows modifier. The new Items property returns an instance of the ObjectCollectionInterface. The instance is held by a private variable that is assigned in the constructor.

ListBoxItemEventArgs class

You may have noticed that in the event declaration, there is a parameter e that is declared as ListBoxItemEventArgs. This class derives from the EventArgs class, and stores an integer that represents the zero-based index for the item that was added or removed. The code for the ListBoxItemEventArgs class looks like this:

VB
''' <summary>
''' Represents the data for the ItemAdded and ItemRemoved events.
''' </summary>
Public Class ListBoxItemEventArgs
    Inherits EventArgs

    Private _index As Integer

    Public Sub New(ByVal index As Integer)
        Me._index = index
    End Sub

    ''' <summary>
    ''' Gets the zero-based index for the item.
    ''' </summary>
    Public ReadOnly Property Index() As Integer
        Get
            Return Me._index
        End Get
    End Property

End Class

There are, of course, ways in which you could extend the functionality of the events. For instance, you could add the possibility for event subscribers to prevent items from being removed, by adding a cancel flag to the event data. Or, you could add a reference to the removed object in the event arguments for the ItemRemoved event.

A note on overridable members

When doing some background work for this article, I found an interview with Microsoft C# lead architect Anders Hejlsberg, discussing the pros and cons of declaring base class members as overridable. According to Anders Hejlsberg, there are two main reasons why the majority of class members in the .NET framework are not overridable. First, it puts a lot of responsibility on the developer that overrides a base member. He or she has to know what other methods can be called from within the override member, and in what order they should be called not to cause an invalid state. Secondly, it introduces an overhead to call a class member that has been overridden. Personally, I find it limiting not to be able to override members at certain times. Depending on context complexity, I believe a lot more base class members of the .NET framework could be marked as overridable without introducing an overall decreased performance or program stability. In the CustomListBox case, things could have been made a lot nicer if all the members of the ListBox.ObjectCollection were marked as overridable.

You can find the whole interview with Anders Hejlsberg here.

Conclusion

In this first article, we have looked at how we can extend or change the functionality of an existing UI control by deriving a new control from it. We implemented a CustomListBox control that can generate events for when items stored in the base class collection are either added or removed. In the next article, we will be looking at how to implement methods for moving selected items up or down the list.

License

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