Data Binding

Give Your Everyday Custom Collections a Design-Time Makeover

Paul Ballard

Parts of this article are based on a prerelease version of the .NET Framework 2.0. Those sections are subject to change.

This article discusses:

  • Creating a type-safe custom collection
  • Implementing design-time support
  • Data binding support implementation
  • Generics in the .NET Framework 2.0
This article uses the following technologies:
Visual Basic .NET, .NET Framework

Code download available at:CollectionsandDataBinding.exe(158 KB)

Contents

Why Use Custom Collections?
Student Business Object
Creating a Type-Safe Custom Collection
Implementing Design-Time Support for the Collection
Adding Data Binding Support
Implementing Editing Support
Adding Sorting Support
Adding Searching Support
Using the StudentCollection
Getting Your Collection Ready for Prime Time
Looking Forward to the .NET Framework 2.0
Conclusion

In the long-running debate over whether to use ADO.NET DataSets or custom business objects to represent data within a .NET-based application, DataSets have always held a couple of strategic advantages over custom objects: data binding and design-time support. Rather than engage in that debate, which would take an entire article in itself, I'm going to show how you can level the playing field by providing data binding support for custom collections to enable sorting, searching, and editing in as simple a manner as possible. I will also show how to make all of these features available in the Windows® and Web Forms Designers, just like an ADO.NET DataSet. Lastly, I'll take a close look at new functionality available in the Microsoft® .NET Framework 2.0 and then show how much easier it is to implement the same functionality using generics.

Why Use Custom Collections?

When working with data in a .NET-based application, it's often useful to create lists of objects. The .NET Framework provides standard list and dictionary types such as ArrayList and Hashtable that allow you to organize objects of any type into collections. You can then bind these collections to Windows Forms or ASP.NET controls at run time. However, because these collections are built to hold any type of object, there is no way to enforce a rule that limits a collection to containing only objects of a specific type. For example, you cannot define an ArrayList that only contains objects of type Order. Because of this, when developers use the list they have to check or cast the types being returned from the collection to ensure the object is of the type they are expecting. If this check isn't done and an object of the wrong type was added to the collection, exceptions can result. Custom collections—specialized classes based on the .NET Framework collections—let you enforce type safety by limiting collections to accepting only objects of a specific type. This in turn makes the collection easier to use.

Unfortunately, none of the .NET Framework collection classes that you can use to build your custom collection provide the level of design-time data binding support that the DataSet provides. By implementing a few interfaces, however, you can provide that level of functionality to your collection.

Student Business Object

For the examples, I've created a business object called Student which has properties for FirstName, LastName, Age, and Grade. Figure 1 shows the source for this class. It includes a default public constructor as well as an overloaded constructor for creating an instance and setting its properties immediately. Also, you can see that there is a PropertyChanged event defined for each property and that this event is raised in the corresponding property's Set block. These are good basic business object design patterns.

Figure 1 The Student Class

Public Class Student Private m_FirstName As String Private m_LastName As String Private m_Grade As Integer Private m_Age As Integer Public Event FirstNameChanged As System.EventHandler Public Event LastNameChanged As System.EventHandler Public Event GradeChanged As System.EventHandler Public Event AgeChanged As System.EventHandler Public Sub New() End Sub Public Sub New(ByVal FirstName As String, ByVal LastName As String, _ Optional ByVal Grade As Integer = 0, _ Optional ByVal Age As Integer = 0) With Me .m_FirstName = FirstName .m_LastName = LastName .m_Grade = Grade .m_Age = Age End With End Sub Public Property FirstName() As String Get Return m_FirstName End Get Set(ByVal Value As String) m_FirstName = Value RaiseEvent FirstNameChanged(Me, EventArgs.Empty) End Set End Property Public Property LastName() As String Get Return m_LastName End Get Set(ByVal Value As String) m_LastName = Value RaiseEvent LastNameChanged(Me, EventArgs.Empty) End Set End Property Public Property Grade() As Integer Get Return m_Grade End Get Set(ByVal Value As Integer) m_Grade = Value RaiseEvent GradeChanged(Me, EventArgs.Empty) End Set End Property Public Property Age() As Integer Get Return m_Age End Get Set(ByVal Value As Integer) m_Age = Value RaiseEvent AgeChanged(Me, EventArgs.Empty) End Set End Property End Class

Creating a Type-Safe Custom Collection

Now, let's create a custom collection that enforces type safety by only allowing objects of type Student to be added to the list. When you create a custom list collection, follow the naming standard of adding the word "Collection" after the object type. In the case of Student, the collection would be called StudentCollection.

There are several ways to create a custom collection. You can create a class that derives directly from one of the standard collection classes, such as ArrayList, and then hide the methods that are not type safe and properties that reference items in the list as Object using the Shadows (Visual Basic®) or new (C#) keyword and replace them with your type-specific versions. You can also create a class that simply implements the IList interface, leaving the internal storage of items in the list entirely up to you, but requiring a bit more code. The preferred method, however, is to derive the custom collection from the CollectionBase class.

The CollectionBase class implements many of the methods and properties that a collection might need, such as Clear and Count, but it leaves the specification of the various type-specific methods to the developer. These methods include Add, Remove, and Insert, as well as the indexer property Item. The CollectionBase class includes a protected property called List, which returns a reference to the CollectionBase's IList implementation (this wraps an internal ArrayList that is not type safe). In the implementation of the public type-specific methods for your collection, you simply wrap the methods of the CollectionBase.List property that aren't type safe with the publicly exposed type safe version. As an example, the StudentCollection.Add method would look like this:

Public Sub Add(ByVal Student As Student) Me.List.Add(Student) 'List.Add() takes type Object End Sub

Figure 2 shows the implementation of the StudentCollection class that ensures that the collection only holds objects of type Student. Note that the Item property uses the Default keyword. This makes it easier for developers who use the collection to access items in the list using the array indexing syntax, as demonstrated in the following line of code:

Dim FirstStudent As Student = myStudentCollection(3) ' get fourth item

Figure 2 The StudentCollection Class

Imports System.Collections Public Class StudentCollection Inherits CollectionBase Public Sub New() End Sub Public Sub Add(ByVal Student As Student) Me.List.Add(Student) 'List.Add() takes type Object End Sub Public Function Contains(ByVal Student As Student) As Boolean Return Me.List.Contains(Student) End Function Public Function IndexOf(ByVal student As Student) As Integer Return Me.List.IndexOf(student) End Function Public Sub Insert(ByVal index As Integer, ByVal Student As Student) Me.List.Insert(index, Student) End Sub Default Public Property Item(ByVal index As Integer) As Student Get Return CType(Me.List.Item(index), Student) End Get Set(ByVal Value As Student) Me.List(index) = Value End Set End Property Public Sub Remove(ByVal Student As Student) Me.List.Remove(Student) End Sub End Class

CollectionBase also implements a couple of very important interfaces, the first of which is called IEnumerable. The IEnumerable interface methods allow developers to work with the items in the collection using a For Each loop. It also implements the IList interface. This interface lets the runtime and the designers know that the class contains the standard list methods such as Add, Remove, and Count that are necessary for data binding. Any object that implements the IList interface can be bound to list-using UI controls, such as Datagrids, Listboxes, and Comboboxes. Therefore, the code presented thus far is all that is necessary to provide a type-safe collection that can be one-way bound at run time. But what about design time?

Implementing Design-Time Support for the Collection

The first step in making the StudentCollection just as useful as a DataSet is to make it available to the Forms Designer. The Forms Designer only allows certain objects to be dragged and dropped onto the design surface or the form itself. Those objects must implement the IComponent interface. Most of the controls you see in the toolbox derive from System.Windows.Forms.Control, which in turn derives from System.ComponentModel.Component. The Component class implements the IComponent interface. Rather than force StudentCollection to derive from Control or Component in order to make use of the Component class's implementation of IComponent, it's easier and cleaner to simply implement the interface directly.

IComponent is a simple interface that defines a property called Site, which returns an object that implements the ISite interface. An object that implements ISite contains a reference to a component and the container it is in and is used by the Designer to keep track of object hierarchies. In the case of the StudentCollection, you will be passed a reference to an ISite object and need only store it for use in the Site property. The IComponent interface also exposes a Disposed event and derives from IDisposable, requiring the implementation of a Dispose method. The Dispose method allows the component to free up any resources currently being used when the object is no longer needed. The StudentCollection will clear its internal list and then raise the Disposed event. Figure 3 shows the ICollection implementation for StudentCollection.

Figure 3 ICollection Implementation

Private m_Site As ISite = Nothing Public Event Disposed(ByVal sender As Object, _ ByVal e As System.EventArgs) Implements _ System.ComponentModel.IComponent.Disposed Public Property Site() As System.ComponentModel.ISite _ Implements System.ComponentModel.IComponent.Site Get Return m_Site End Get Set(ByVal Value As System.ComponentModel.ISite) m_Site = Value End Set End Property Public Sub Dispose() Implements System.IDisposable.Dispose Me.List.Clear() If Not m_Site Is Nothing AndAlso Not m_Site.Container Is Nothing Then m_Site.Container.Remove(Me) End If RaiseEvent Disposed(Me, System.EventArgs.Empty) End Sub

Unfortunately, Visual Studio® .NET 2002 and Visual Studio .NET 2003 are not quite clever enough to know to add a component from the project to the toolbox palette automatically. Therefore, you have to add the component to the toolbox manually by right-clicking the toolbox and selecting Add/Remove Items. Using the Add/Remove Items Dialog Box, you can browse to the assembly containing your component and it will be added to the toolbox. You can then drag it from the toolbox and drop it onto your forms. You'll see that it adds the component to the bottom of the designer, and in the source code for the form it adds code to create and store the component, just as if it were a Button or TextBox control.

Adding Data Binding Support

To enable your collection to have design-time data binding, you next need to implement the IBindingList interface. The IBindingList interface defines the methods and properties necessary for a list-based UI control like the DataGrid to be able to add and remove items from the list, sort the list based on the selected column, and search for a value in the list. Figure 4 shows the complete list of properties and methods that define the IBindingList interface.

Figure 4 IBindingList Interface

Public Interface IBindingList Implements IList, ICollection, IEnumerable ' Events Event ListChanged As ListChangedEventHandler ' Methods Sub AddIndex(ByVal [property] As PropertyDescriptor) Function AddNew() As Object Sub ApplySort(ByVal [property] As PropertyDescriptor, _ ByVal direction As ListSortDirection) Function Find(ByVal [property] As PropertyDescriptor, _ ByVal key As Object) As Integer Sub RemoveIndex(ByVal [property] As PropertyDescriptor) Sub RemoveSort() ' Properties ReadOnly Property AllowEdit As Boolean ReadOnly Property AllowNew As Boolean ReadOnly Property AllowRemove As Boolean ReadOnly Property IsSorted As Boolean ReadOnly Property SortDirection As ListSortDirection ReadOnly Property SortProperty As PropertyDescriptor ReadOnly Property SupportsChangeNotification As Boolean ReadOnly Property SupportsSearching As Boolean ReadOnly Property SupportsSorting As Boolean End Interface

The first set of properties to implement is the AllowNew, AllowEdit, and AllowRemove properties. These are simple read-only Boolean values that tell the UI control whether a user can add, edit, or remove items from the list using the UI control at run time.

The next set of properties is SupportsChangeNotification, SupportsSearching, and SupportsSorting. These properties tell the UI control what features of the IBindingList interface this collection supports. Returning True from the SupportsChangeNotification property tells the user interface control that any changes to the collection will cause the ListChanged event to be raised. This event is part of the IBindingList interface, and the user interface control listens for this event to signal that it should redraw the data if it has changed. The last property, IsSorted, will be covered during the Sorting implementation.

Implementing Editing Support

When the collection is bound to a control, such as a DataGrid, and the collection supports editing via the AllowEdit property, a user can change the values of an item's properties directly from the control. The functionality is similar to that of a spreadsheet. To support this requires code in both the StudentCollection and the Student object. To support row-based editing from within a data-bound control, the Student class must implement the IEditableObject interface.

IEditableObject defines three methods: BeginEdit, EndEdit, and CancelEdit. The BeginEdit method is called when the user navigates to that item within the control, but only when editing is allowed by the collection. EndEdit is called when the user navigates away from that item and any changes the user made should be saved. CancelEdit is called when the user navigates away and the changes should be discarded. Supporting edited data will require the Student object to track the state of its properties before and after editing has begun.

The BeginEdit method in Figure 5 shows an implementation that first checks to see that BeginEdit hasn't already been called. This is an important step because BeginEdit is called many times during the initialization of data binding as well as during navigation of the control by the user. The next thing it does is create a new Hashtable to store the original values of its properties. It then uses a helper class in the ComponentModel namespace to create a PropertyDescriptorCollection. This collection will contain a list of PropertyDescriptor objects that describe each property of the Student class. The BeginEdit method then iterates through the list adding the value of each property to the Hashtable using the corresponding PropertyDescriptor as an index. This means of storing the data is very generic and could have been more easily implemented by just copying the values of the properties to an ArrayList. However, this example will continue to work if you add or remove properties from the Student class as well as if you choose to move this code into an EditableObject base class from which you will derive all of your business objects.

Figure 5 Student's IEditableObject Implementation

Private m_OriginalData As Hashtable Private m_Editing As Boolean = False Private m_IsAddNew As Boolean = False Public Event CancelAddNew( _ ByVal sender As Student, ByVal Remove As Boolean) Public Sub New(ByVal IsAddNew As Boolean) m_IsAddNew = IsAddNew End Sub Public Sub BeginEdit() Implements IEditableObject.BeginEdit If Not m_Editing Then m_Editing = True m_OriginalData = New Hashtable Dim Properties As PropertyDescriptorCollection = _ TypeDescriptor.GetProperties(Me) For Each prop As PropertyDescriptor In Properties m_OriginalData.Add(prop, prop.GetValue(Me)) Next End If End Sub Public Sub EndEdit() Implements IEditableObject.EndEdit If m_Editing Then If (m_IsAddNew) Then RaiseEvent CancelAddNew(Me, False) End If m_OriginalData = Nothing m_Editing = False End If End Sub Public Sub CancelEdit() Implements IEditableObject.CancelEdit If m_Editing Then If (m_IsAddNew) Then RaiseEvent CancelAddNew(Me, True) Else Dim prop As PropertyDescriptor For Each entry As DictionaryEntry In m_OriginalData prop = CType(entry.Key, PropertyDescriptor) prop.SetValue(Me, entry.Value) Next m_OriginalData = Nothing m_Editing = False End If End If End Sub

Dealing with items that are added via a user is a bit tricky and requires some coordination between the collection and the object created. If the user adds a Student to the StudentCollection using a DataGrid for example, and then cancels the edit, you need to remove the item from the collection manually. To handle this, you'll need to add a bit more code to the Student class implementation. First, the class needs another overloaded constructor that takes a Boolean value. This constructor will be called when the user chooses to add an item to the collection via the IBindingList.AddNew method. The Boolean value tells the instance of the Student class that it was created at the user's request. The value is stored as m_IsAddNew and then, in the CancelEdit method, if the m_IsAddNew field is true, the method raises an event that you define called CancelAddNew. This event signals to the collection that the item should be removed from the list. Figure 5 shows the Student class implementation of IEditableObject including the CancelAddNew functionality.

The EndEdit method checks to ensure that the object was being edited and, if so, checks to see if this object was added as a result of the UI control. If it was, it raises the CancelAddNew event passing False to tell the collection that the addition of the item is complete and not to remove it from the collection. It then discards the original property values and sets the m_Editing flag to false.

The CancelEdit method replaces the object's current property values with the values stored in the Hashtable. To do this, the method iterates over the items in the Hashtable and uses the PropertyDescriptor to set the properties value back to the values stored in the Hashtable and then sets the Hashtable to Nothing.

In the StudentCollection, you have two methods to implement in order to wrap up editing support. The IBindingList.AddNew method is called when the user creates a new item in the collection via the UI control. As mentioned earlier, you can't simply add and return a new object in case the user changes her mind. The method creates a new Student object using the overloaded constructor passing in the value of True and then adds the Student to its internal list. It then adds an event handler to be called by that Student object's CancelEdit method and finally returns the new Student object. The CancelAddNew method unwires the event handler and based on the Remove argument, will remove the item from the collection. Figure 6 shows the implementation of these two StudentCollection class methods.

Figure 6 IBindingList Editing Methods

Protected Function AddNew() As Object Implements IBindingList.AddNew Dim s As New Student(True) Me.Add(s) AddHandler s.CancelAddNew, AddressOf Me.CancelAddNew Return s End Function Friend Sub CancelAddNew(ByVal student As Student, ByVal Remove As Boolean) RemoveHandler student.CancelAddNew, AddressOf Me.CancelAddNew If (Remove) Then Me.List.Remove(student) End Sub Protected Overrides Sub OnInsertComplete(ByVal index As Integer, _ ByVal value As Object) MyBase.OnInsertComplete(index, value) RaiseEvent ListChanged(Me, _ New ListChangedEventArgs(ListChangedType.ItemAdded, index)) End Sub Protected Overrides Sub OnRemoveComplete(ByVal index As Integer, _ ByVal value As Object) MyBase.OnRemoveComplete(index, value) RaiseEvent ListChanged(Me, _ New ListChangedEventArgs(ListChangedType.ItemDeleted, index)) End Sub

The IBindingList interface defines an event that should be raised whenever a change is made in the list. It's called, appropriately enough, ListChanged. The ListChanged event takes a ListChangedEventArgs parameter that includes an enumeration specifying what type of change caused the event to be raised as well as the index of the item that was inserted, deleted, modified, or moved. Implementing change notification is simple, thanks to the CollectionBase class's OnInsertComplete and OnRemoveComplete overrides. The StudentCollection implementation is shown in Figure 6.

Adding Sorting Support

When sorting a list, the collection needs a way to perform a comparison of two objects that tells the sorting algorithm whether the first value is higher than, lower than, or equal to the second. With primitive types like String and Int32, this is trivial. But sorting business objects can require complex comparisons that involve the application of business rules and validations. In order to implement a more complex comparison for business objects, you should create a new class that implements the IComparer interface. This class can contain specific code for comparing the two business objects. The IComparer interface only defines one method, Compare. The Compare function takes two Objects as parameters and returns an integer value representing the relationship of Object x to Object y. The value should be 1 if x is greater than y, -1 if x is less than y, and 0 if they are the same.

In the implementation of an IComparer, the first thing to do is check to make sure that the parameters are of the type you expect. In the case of StudentComparer, it expects two parameters of type Student. If anything else is received, an ArgumentException is thrown. It then calls the private CompareProperties method, which does the real work of comparing the Students. The CompareProperties method creates an integer used to change the sign of the result based on whether the sort is ascending or descending. The comparison is then done based on the m_SortProperty in an If/Then/ElseIf structure, with the result being multiplied by the direction modifier. For example, if the value of the sort property on Object x is less than the value of the same property on y, the result is -1. If the order of sorting is ascending, that value is multiplied by 1, returning -1. This will cause the sorting algorithm to place y higher in the list than x. However, if the sort direction is descending, then the result of the comparison is multiplied by -1, resulting in a positive 1. This will cause the sorting algorithm to put Object x higher on the list than Object y.

Figure 7 shows the implementation of the StudentComparer class that can be used to compare Students based on any property. It derives from IComparer and implements the Compare method, as mentioned earlier. Notice that it has a constructor that takes the PropertyDescriptor for the property to sort on and the direction of the sort as parameters. These are used in the Compare method to determine the relationship between the two Students. I should also point out (at the risk of being pummeled by e-mails pointing out my dependency on Visual Basic helper methods), that I am intentionally turning off Option Strict for this class. This is to avoid doing type comparison checks when the values of the properties are compared at run time. This was done solely to keep the code as brief as possible and is by no means a recommendation.

Figure 7 Student Comparison Based on Property

Option Strict Off Imports System.Collections Imports System.Collections.Specialized Imports System.ComponentModel Public Class StudentComparer Implements IComparer Private m_SortProperty As PropertyDescriptor Private m_SortDirection As ListSortDirection Public Sub New(ByVal SortProperty As PropertyDescriptor, _ ByVal direction As ListSortDirection) m_SortProperty = SortProperty m_SortDirection = direction End Sub Private Function CompareProperties( _ ByVal x As Student, ByVal y As Student) As Integer Dim result As Integer = 0 Dim directionModifier As Integer If (m_SortDirection = ListSortDirection.Ascending) Then directionModifier = 1 Else directionModifier = -1 End If If (x Is Nothing) Then result = -1 * directionModifier ElseIf (y Is Nothing) Then result = 1 * directionModifier ElseIf (m_SortProperty.GetValue(x) < _ m_SortProperty.GetValue(y)) Then result = -1 * directionModifier ElseIf (m_SortProperty.GetValue(x) > _ m_SortProperty.GetValue(y)) Then result = 1 * directionModifier Else result = 0 End If Return result End Function Public Function Compare(ByVal x As Object, ByVal y As Object) _ As Integer Implements IComparer.Compare If (Not TypeOf x Is Student) Then Throw New ArgumentException( _ "Argument must be of Type Student", "x") If (Not TypeOf y Is Student) Then Throw New ArgumentException( _ "Argument must be of Type Student", "y") Return CompareProperties(CType(x, Student), CType(y, Student)) End Function End Class

To allow sorting of the collection via the UI controls, your collection must implement the IBindingList.ApplySort method. This method receives a PropertyDescriptor describing the property used to sort the list and an enumeration indicating the order of the sort—either ascending or descending. These values are assigned to private fields that are exposed through the IBindingList.SortDirection and IBindingList.SortProperty properties. In order to restore the original order of the items in the list if a sort is removed, the original order needs to be saved. The easiest way to do this is to create a new ArrayList based on the original list before you call the method that actually changes the list's order. Figure 8 shows the ApplySort implementation for the StudentCollection class.

Figure 8 ApplySort Implementation

Private m_SortProperty As PropertyDescriptor Private m_SortDirection As ListSortDirection Private m_OriginalList As ArrayList Protected ReadOnly Property SortDirection() As ListSortDirection _ Implements IBindingList.SortDirection Get Return m_SortDirection End Get End Property Protected ReadOnly Property SortProperty() As PropertyDescriptor _ Implements System.ComponentModel.IBindingList.SortProperty Get Return m_SortProperty End Get End Property Protected ReadOnly Property IsSorted() As Boolean _ Implements IBindingList.IsSorted Get Return m_SortProperty Is Nothing End Get End Property Private Sub SaveList() m_OriginalList = New ArrayList(Me.List) End Sub Private Sub ResetList(ByVal NewList As ArrayList) Me.List.Clear() Me.InnerList.AddRange(NewList) End Sub Private Sub DoSort() Me.InnerList.Sort( _ New StudentComparer(m_SortProperty, m_SortDirection)) End Sub Protected Sub ApplySort(ByVal [property] As PropertyDescriptor, _ ByVal direction As ListSortDirection) _ Implements IBindingList.ApplySort m_SortProperty = [property] m_SortDirection = direction If (m_OriginalList Is Nothing) Then SaveList() DoSort() RaiseEvent ListChanged(Me, _ New ListChangedEventArgs(ListChangedType.Reset, 0)) End Sub Protected Sub RemoveSort() Implements IBindingList.RemoveSort ResetList(m_OriginalList) m_SortDirection = Nothing m_SortProperty = Nothing RaiseEvent ListChanged(Me, _ New ListChangedEventArgs(ListChangedType.Reset, 0)) End Sub

There are lots of ways to implement the actual sorting algorithm. Rather than reinvent the wheel, I've chosen to take advantage of ArrayList's built-in sorting implementation. If you remember, the CollectionBase.List property exposes an IList implementation that wraps an internal ArrayList. That internal ArrayList is exposed from another property on CollectionBase, InnerList. As such, I simply call the InnerList's Sort method, passing in an instance of a StudentComparer. The IBindingList.RemoveSort method restores the list to its original order, which was saved before the list was sorted for the first time. It's important to remember to raise the ListChanged event when the list order is changed to keep the UI controls in sync. The last property to implement for sorting is IBindingList.IsSorted. This property is largely unused but easy enough to implement by returning False if the IBindingList.SortProperty is Nothing and True if it has been set.

Adding Searching Support

User interaction isn't the only thing that affects the binding of UI controls to a collection. If a developer using your collection sets the SelectedValue or SelectedIndex of a UI control that is bound to the collection, the control must be able to find that value in the collection and make it the current item. To provide this feature, implement the IBindingList.Find method. This method receives a PropertyDescriptor defining which property is being used by the control for its SelectedValue. This is usually defined with the DataMember property on the control. The IBindingList.Find method also receives the value to find in the collection. It then iterates through the list and checks the value of the specified property to see if it matches the value passed in the parameter. When you have a match, you return the index of the item in the collection. Figure 9 shows this process implemented for the StudentCollection class, which includes some specific type-checking of the value to ensure that you properly match the value to the key.

Figure 9 Find Property

Protected Function Find(ByVal [property] As PropertyDescriptor, _ ByVal key As Object) As Integer Implements IBindingList.Find Dim idx As Integer Dim result As Integer = -1 For idx = 0 To Me.List.Count - 1 Dim value As Object = [property].GetValue(Me.List(idx)) Select Case [property].Name Case "FirstName", "LastName" If (value Is key) Then result = idx Case "Age", "Grade" If (CType(value, Integer) = CType(key, Integer)) _ Then result = idx Case Else If (value Is key) Then result = idx End Select If (result <> -1) Then Return result Next End Function Protected Sub AddIndex(ByVal [property] As PropertyDescriptor) _ Implements IBindingList.AddIndex 'Not implemented End Sub Protected Sub RemoveIndex(ByVal [property] As PropertyDescriptor) _ Implements IBindingList.RemoveIndex 'Not Implemented End Sub

There are two other methods of the IBindingList I haven't covered here: AddIndex and RemoveIndex. These methods are used specifically with ADO.NET DataSets and are not relevant for a collection. Therefore, I'll leave the implementation of AddIndex and RemoveIndex blank.

Using the StudentCollection

Figure 10 shows a Windows Form with a DataGrid and several fields for displaying and editing a Student object. All of these controls can be bound to a StudentCollection class within the designer. First, you'll drag a StudentCollection from the toolbox and drop it onto the form, which will move to the bottom of the Designer. If you look at the properties of the collection you can see that it doesn't actually expose much; most of its magic is in what it does to the other controls on the form. To bind the collection to the DataGrid, you'll set the DataGrid's Data source property to the collection. Notice that when you drop down the list for this property, the collection is displayed as an option. This is because the collection, via CollectionBase, implements the IList interface. When you select the collection, you'll see the columns added to the DataGrid for each property. To bind the individual UI controls to properties of the currently selected Student item, expand the control's DataBindings property in the Properties window. For the TextBox controls, you should see the Text property in the list. Drop down the list and you'll see a hierarchical view of the Student class as shown in the properties window in Figure 10. Select the property of the Student class you want to bind to. Other UI controls have different properties in their list for you to bind to, but the collection should always appear as an option. Execute the application and you'll be adding and editing items in the collection without writing any additional code in your forms.

Figure 10 Student Object

Figure 10** Student Object **

Getting Your Collection Ready for Prime Time

While all of the necessary steps to provide design-time data-binding support have been implemented, if you compile the source code as defined thus far you'll find one particularly annoying problem. The properties listed for the Student class aren't displayed in the DataGrid UI control in the same order that you defined them. While this is generally acceptable for controls such as a ListBox or ComboBox that don't display multiple properties of an item from the collection at the same time, it is frustrating on a DataGrid.

I'm going to go one step beyond what is necessary and implement the ITypedList interface. This interface is used to allow a type to explicitly specify the properties it exposes as opposed to letting the TypeDescriptor class figure it out using reflection. The interface defines two methods: GetItemProperties and GetListName. The GetItemProperties method has no parameters and returns a PropertyDescriptorCollection. In this method, you can call TypeDescriptor.GetProperties on the Student class to load all or some of the properties from your type. Once you have the collection created, you can add and remove items or sort the list. The Sort method for this collection has an overload that takes an array of strings, where each element in the array is a name of a property in the collection. The order in which the names appear in the array will be the order that the properties will display in the resulting collection. Figure 11 shows the StudentCollection's implementation of ITypedList.GetItemProperties returning the properties in the order in which they are defined. The GetListName returns a string value with a name for the list.

Figure 11 ITypedList.GetItemProperties

Protected Function GetItemProperties(ByVal listAccessors() _ As PropertyDescriptor) As PropertyDescriptorCollection _ Implements ITypedList.GetItemProperties Dim m_OriginalList As PropertyDescriptorCollection = TypeDescriptor.GetProperties(GetType(Student)) Dim m_SortedList As PropertyDescriptorCollection = _ m_OriginalList.Sort(New String() { _ "FirstName", "LastName", "Age", "Grade"}) Return m_SortedList End Function Protected Function GetListName(ByVal listAccessors() As PropertyDescriptor) As String _ Implements System.ComponentModel.ITypedList.GetListName Return "StudentCollection" End Function

Another nice feature to add to the collection is an icon to be displayed in the toolbox palette. This will help developers distinguish the collection control from other objects. To add an icon for your collection, you should add a 16?16 bitmap to the assembly containing the collection class and using the same file name as the collection but with a .bmp extension. For example, the icon for the StudentCollection class is contained in the StudentCollection.bmp file. In the properties window for the bitmap file, make sure that the Build Action is set to "Embedded Resource". This will put the image into the assembly's resource list. After adding the icon to the assembly, when you add the collection component to the toolbox it will display the icon you defined next to the name of the collection.

Looking Forward to the .NET Framework 2.0

With the final release of the .NET Framework 2.0 just around the corner, it makes sense to consider what changes in the .NET Framework will have in store for building custom collections and data binding. The first thing you should know is that no changes are necessary to migrate your collections from the .NET Framework version 1.x to version 2.0. The IComponent, IBindingList, and ITypedList interfaces haven't changed. There have been quite a few new features added, though, both for creating type-safe collections and for data binding.

There have been significant improvements made to data binding in the designers for the new version. In fact, a developer who is creating an application that uses a custom business object like the Student class shown in previous examples can bind that object directly to the new DataGridView control without creating a collection at all.

From the DataGridView's Tasks window, click "Add a Data Source". This will start the Data Source Configuration Wizard, which allows you to add a data source to your project from a database, a Web service, or another object. Select "Object" and the wizard displays a screen listing the classes in your project and any classes that your project references. When you select the Student class, a new BindingSource object is added to the designer. You should also see that the DataGridView displays the columns matching the properties of the Student class.

Behind the scenes of BindingSource is a new type of collection called a generic collection. A generic collection is a collection in which the type of the items that the collection contains is defined when the collection is instantiated. You will no longer need to implement type-safe methods and properties to add and retrieve items from the collection because the .NET Framework takes care of that for you. There are several types of generic collections ranging from simple lists, such as List(Of T), to specialized generics like Stack(Of T) and Queue(Of T). One of these specialized collections is the BindingList(Of T). This is a generic collection that implements the IBindingList interface. The BindingSource control automatically creates a BindingList(Of Student) when you specify the Student class as the data source in the Data Source Wizard. The DataGridView is then bound to the BindingSource, taking most of the work out of creating a type-safe collection that can be bound to controls at run time.

In order to get true parity with the custom collection built in the first part of this article, you have to do a bit more coding. While BindingList(Of T) implements the IBindingList interface, it does not automatically implement the sorting features. It sets the IBindingList.SupportsSorting property to False and if the IBindingList.ApplySort method is somehow called, it throws a NotSupportedException. You can change that by deriving a custom collection from BindableList(Of T) and then overriding the ApplySortCore method as well as the SupportsSortingCore, SortPropertyCore, and SortDirectionCore properties. You can use exactly the same sorting logic as in the .NET Framework 1.x implementation, including the StudentComparer class. Figure 12 shows a type-safe custom collection for the .NET Framework 2.0 that overrides the BindableList's sorting methods to enable sorting of the list. You'll notice that you have to write considerably less code than you wrote before.

Figure 12 Sorting the List

Imports Students.Student Imports System.ComponentModel Public Class StudentCollection Inherits BindingList(Of Student) Implements IComponent #Region "IComponent Implementation" Private m_Site As ISite = Nothing Public Event Disposed(ByVal sender As Object, _ ByVal e As System.EventArgs) _ Implements System.ComponentModel.IComponent.Disposed Protected Property Site() As System.ComponentModel.ISite Implements _ System.ComponentModel.IComponent.Site Get Return m_Site End Get Set(ByVal Value As System.ComponentModel.ISite) m_Site = Value End Set End Property Public Sub Dispose() Implements System.IDisposable.Dispose Me.Items.Clear() RaiseEvent Disposed(Me, System.EventArgs.Empty) End Sub #End Region #Region "IBindingList Sorting Features" Private m_SupportsSorting As Boolean = True Private m_SortProperty As PropertyDescriptor Private m_SortDirection As ListSortDirection Private m_OriginalList As ArrayList Protected Overrides ReadOnly Property SupportsSortingCore() As Boolean Get Return m_SupportsSorting End Get End Property Protected Overrides ReadOnly Property SortDirectionCore() As _ System.ComponentModel.ListSortDirection Get Return m_SortDirection End Get End Property Protected Overrides ReadOnly Property SortPropertyCore() As _ System.ComponentModel.PropertyDescriptor Get Return m_SortProperty End Get End Property Protected Overrides ReadOnly Property IsSortedCore() As Boolean Get Return m_SortProperty Is Nothing End Get End Property Private Sub SaveList() m_OriginalList = New ArrayList(Me.Items) End Sub Private Sub ResetList(ByVal NewList As ArrayList) Me.Items.Clear() Me.InnerList.AddRange(NewList) End Sub Private Sub DoSort() Me.InnerList.Sort( _ New StudentComparer(m_SortProperty, m_SortDirection) End Sub Protected Overrides Sub ApplySortCore( _ ByVal prop As PropertyDescriptor, _ ByVal direction As ListSortDirection) m_SortProperty = prop m_SortDirection = direction If (m_OriginalList Is Nothing) Then SaveList() End If DoSort() End Sub Protected Overrides Sub RemoveSortCore() ResetList(m_OriginalList) m_SortDirection = Nothing m_SortProperty = Nothing End Sub #End Region End Class

Once again, the order of the columns in the DataGridView doesn't match the order in which the Student class defines the properties. You can implement the ITypedList interface if you like, but one nice feature of the DataGridView control is that it will allow you to change the order of the columns from within the designer. This makes the implementation of ITypedList not strictly necessary. In order to view the collection in the designer, you do have to implement the IComponent interface again as well and you can reuse the code from the .NET Framework 1.x version verbatim. Unlike Visual Studio .NET 2003, however, Visual Studio 2005 is clever enough to find the component and put it into the toolbox palette automatically.

You can now set the BindingSource.DataSource property to point to an instance of the custom collection and you'll be able to sort the data just as before.

Conclusion

In this article I've shown that all of the powerful data binding features currently available for DataSets can also be made available to custom collections by implementing a couple of interfaces. While it's not necessarily simple, type-safe collections that can be used from within the designer and which allow sorting, editing, and searching are a valuable tool to have in your development arsenal. The .NET Framework 2.0 and Visual Studio 2005 make the use of business objects even easier than before. While this won't eliminate the use of DataSets, it's always good to have more options.

Paul Ballard is a Visual Basic MVP, MCSD, MCAD, and MCSE certified consultant and the President of The Rochester Consulting Partnership Inc. He has spent more than 15 years designing and building client/server and Web-based distributed applications and is currently specializing in Microsoft .NET technologies as a consultant, speaker, and trainer.