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:
- Create a new Windows application.
- Add a new class to your project and name it
MyLabel
. - Open the code for the class and insert the sentence "
Inherits Label
" as shown in the code snippet below. - Compile the solution.
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.
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:
Public Class CustomListBox
Inherits ListBox
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
Public Sub New()
MyBase.New()
Me.itemsInterface = New CollectionObjectInterface(Me)
End Sub
Public Shadows ReadOnly Property Items() As CollectionObjectInterface
Get
Return Me.itemsInterface
End Get
End Property
Private ReadOnly Property InnerItems() As ListBox.ObjectCollection
Get
Return MyBase.Items
End Get
End Property
Protected Overridable Sub OnItemAdded(ByVal index As Integer)
RaiseEvent ItemAdded(Me, New ListBoxItemEventArgs(index))
End Sub
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:
Public Class ListBoxItemEventArgs
Inherits EventArgs
Private _index As Integer
Public Sub New(ByVal index As Integer)
Me._index = index
End Sub
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.