he .NET 2.0 Framework now supports a new style of strongly typed collections called generics. This article demonstrates how to use generics in your .NET code.
It would be nice to begin by explaining in simple terms what generics are…but it is hard to explain exactly what generics are. Generics are not really a single thing, but rather a set of techniques with a single purpose: to work with generalized code in a data-type specific way.
This somewhat complex-sounding purpose is why it may appear that generics are a Ferrari-style technique. But getting past the definition to actual examples will demonstrate how you can use generics every day to minimize the amount of code you write and improve the performance of your applications.
The two most fundamental and potentially “everyday” generics techniques involve the built-in generics collections and generic methods.
If you ever use arrays or ArrayLists in your applications, consider using the built-in generics collections instead. The built-in generics collections are not only easier to use than arrays, but they allow you to control the data type of the items that are in the collection, providing built-in type-safety, meaning that you get a compile-time error if the code attempts to put something in the collection that is not of the correct type. Generics can also improve your application’s performance, limiting the need to convert data types.
Generic methods provide a way for you to write code that can work with any data type. You specify the exact data type that you want to use when you call the code. This minimizes repetitive code and maximizes type safety.
For both techniques, generics provide for code (either that you write or that is in the Framework) that will work with any data type, such as strings, integers, business objects, etc. You then specify the data type that the code should use when you call that code. That allows you to reuse that code for any number of data types.
This article demonstrates how to use the built-in generic collections instead of arrays. It then details other uses of generic collections, such as for data binding and sorting. Finally, it describes how to build your own generic methods to take advantage of generics every day.
Using Generics Instead of Arrays
Arrays are a common mechanism for storing data in an application. You may currently use them to store internal data or user-entered values. But working with arrays is somewhat complex. First you need to set up their dimension, and then you need to keep track of how many items you are adding to the array so you don’t exceed their dimension.
For example, the following code takes user-entered names and adds them to an array:
Private NameArray(5) As String Private NameArrayIndex As Integer = 0 ... If NameArrayIndex > 5 Then ReDim Preserve NameArray(NameArray.Length + 5) End If NameArray(NameArrayIndex) = NameTextBox.Text NameArrayIndex += 1
If you instead use one of the built-in generic collections, you can replace the two declarations with one and replace the five lines of code to manage the array with one line of code:
Private NameList As New List(Of String) ... NameList.Add(NameTextBox.Text)
Note: If Visual Studio does not recognize the List keyword, examine your project imports (My Project ( References Tab ( Imported Namespaces section). Ensure that “System.Collections.Generic” is checked. Or prefix the List keyword with “System.Collections.Generic.”
The declaration in this example creates a new instance of the List built-in generic collection. The Of String syntax identifies the type parameter and defines the specific data type to use with the generic collection. In this case, only strings can be added to the collection.
|Author’s Note: You will often see generics documented using the “Of T” syntax, such as List(Of T). The T is the placeholder for the type parameter and you can replace it with any data type when you create an instance of the generics collection or method.
In this case, if the code attempts to put something other than a string into the collection, the compiler generates a compile-time error. For example, consider this code:
The preceding code line will generate the error: “Option Strict On disallows implicit conversions from ‘Integer’ to ‘String'”. (Assuming of course that you have Option Strict On.)
If you want to retain key-value pairs, such as U.S. states along with their abbreviations, you can use the generic Dictionary class. Generic Dictionaries have two type parameters: a unique key and a value.
Note: Do not confuse these generic Dictionaries with the old scripting Dictionaries sometimes used in Visual Basic 6.0 applications.
The following code creates a new generic Dictionary and adds the abbreviation as the key and the long name as the value:
Private StateDictionary As New Dictionary(Of String, String) ... StateDictionary.Add("CA", "California") StateDictionary.Add("NY", "New York") StateDictionary.Add("WI", "Wisconsin") ...
Even though the code examples so far showed Strings as the type parameters, you can use any data type as the key or as the value.
For example, if you had a Person business object with a unique numeric ID, you could define a PersonDictionary as follows:
Private PersonDictionary As New Dictionary(Of Integer, Person)
The .NET Framework provides for several different types of generic collections. The ones that you may use most often include:
- Dictionary?Provides a list of key and value pairs where the key must be unique. For example PersonDictionary(Of Integer, Of Person) represents a collection of Person objects keyed by a unique integer value.
- List?Provides a list of values that can be accessed by index. For example PersonList(Of Person) represents a list of Person objects. The List provides methods to search, sort, and manipulate lists.
- SortedDictionary?Provides a generic Dictionary sorted by a unique key.
- SortedList?Provides a generic List sorted by a unique key.
These generic collections provide both better type safety and better performance than non-generic collections, and they are much easier to use than arrays. With the built-in generic collection classes you may never need to dimension an array again!
Data Binding With Generic Collections
One common programming task is displaying data to users. Generic collections, coupled with the latest databinding features in Visual Studio 2005, provide a quick and easy way to display data.
To try this, create a new form. Add a TextBox named NameTextBox, a button named AddButton, and a ListBox named NamesListBox to the form. Insert the following declaration into the form code:
Private NameList As New List(Of String)
In the Click event for the button, add the following code:
NameList.Add(NameTextBox.Text) NameTextBox.Text = String.Empty NamesListBox.DataSource = Nothing NamesListBox.DataSource = NameList
The first line adds the value entered into the textbox to the List. The second line clears the textbox so users can enter another value. The last two lines set up the binding.
|Author’s Note: The DataSource did not seem to reset to the latest information from the List unless it was first set to Nothing.
Run the resulting application. Type a name and click “Add.” The name should appear in the ListBox. Repeat this step to add each name to the ListBox.
Next you’ll display the data in sorted order using one of these techniques:
- Sort the generic List.
- Use a generic SortedList.
Both ways have pros and cons.
To sort the List, add a button named SortButton to the form and add the following code to the Click event for that button:
NameList.Sort() NamesListBox.DataSource = Nothing NamesListBox.DataSource = NameList
The first line performs the sort and the second two lines rebind the ListBox. This is necessary to ensure the ListBox contents reflect the sorted data.
Run the application. Type a name and click “Add.” Repeat this several times to create a list of names that are in no particular order. Now click “Sort” to view the sorted list.
The Sort method performs an ascending sort on the contents of the List, assuming that the data type of the List has a default sort comparer. A comparer compares two values; sorting algorithms require comparers to determine whether an item should be sorted ahead of or behind another item. Strings and other basic data types have a default sort comparer. After the list is sorted, you can reverse the sort order using the List.Reverse method.
If you then add items to the List after it has been sorted, any newly added items will not appear in sorted order. You will need to resort after adding each item.
To define a list that will remain sorted as you add items, use the generic SortedList instead. To try this out, change the declaration of the List(of T) to be SortedList(Of TKey, TValue):
Private NameList As New SortedList(Of String, String)
The SortedList sorts on a unique key value, so it requires two type parameters. The first type parameter is the type of the unique key. The second parameter is the value.
The unique key requirement is the primary drawback of using the SortedList; you must assign a unique key to every value and you can sort only on that key (not on the value). However, the positive aspect of a SortedList is that it stays sorted. You don’t need to resort after adding each entry.
To continue trying out the SortedList, you need to change the code that adds items to the List to assign a key as well. For example:
NameList.Add(NameTextBox.Text, NameTextBox.Text) NameTextBox.Text = String.Empty NamesListBox.DisplayMember = "Value" NamesListBox.DataSource = New BindingSource(NameList, Nothing)
This example uses the same text as the key and the value. Notice how the data binding code changed in this code. The NameList now has a Key and a Value, so you need to define which will be displayed in the list using the DisplayMember property of the ListBox. And because the binding is more complex, you can no longer simply assign the NameList as the DataSource. You must instead create a new BindingSource, passing the NameList as a parameter.
Because the keys must be unique, the example generates an error if a user types in a duplicate entry, so the example requires additional exception handling to cover for that condition:
If NameList.ContainsKey(NameTextBox.Text) Then MessageBox.Show("The entered item is already on the list") Else NameList.Add(NameTextBox.Text, NameTextBox.Text) NameTextBox.Text = String.Empty NamesListBox.DisplayMember = "Value" NamesListBox.DataSource = New BindingSource(NameList, Nothing) End If
Run the application. Type a name and click “Add.” Repeat this several times and you will notice that the ListBox remains sorted. Type in a name that already exists and you should see your error message.
The default comparer for the SortedList sorts in ascending order. If you want to sort in descending order, you need to write your own generic comparer method as described in the next section.
Use data binding to display the contents of a List to the user as a ListBox or ComboBox. Use the List’s Sort and Reverse methods to sort on possible repeating values. Use the SortedList instead if you:
- Want the list to sort automatically as you add values
- Can assign a unique key to every value
- Want the List sorted by that key and not the value
Writing Generic Methods
Generics provide a way for you to write code that can work with any data type, such as strings, integers, business objects, etc. You then specify the data type that you want to use when you call the method.
Since writing comparers for sorting is not such an everyday activity, I’ll hold that until the end of this section. Instead, I’ll start with more common functionality, such as a Save option on a form.
To really see the benefit of writing generic methods, the sample application will need to have a more complex data type, not just strings or integers (though strings and integers will of course work). Plus it will need more than one type to demonstrate how the generic method works with multiple types. The sample presented here creates Person and Task classes along with forms for entering person and task information.
Start by creating a business object. For this example, build a Person class with properties for LastName and FirstName:
Private _LastName As String Public Property LastName() As String Get Return _LastName End Get Set(ByVal value As String) _LastName = value End Set End Property Private _FirstName As String Public Property FirstName() As String Get Return _FirstName End Get Set(ByVal value As String) _FirstName = value End Set End Property
Add a Save method to the Person class. For purposes of this example, it does not need to access a database and actually save the values, just display a message.
Public Sub Save() ' just to see how this works
MsgBox("Got to the Save: " & LastName) End Sub
|Author’s Note: Because this code is in a class and not a Windows form, you cannot use MessageBox.Show unless you have a reference to System.Windows.Forms. And we all know that business objects should not be displaying messages?this is just a sample..
Create a form to allow users to enter person information with TextBoxes to hold the LastName and FirstName entries. Include a button named AddButton to add the person to the list and a button named SaveButton to perform a save operation. If desired, add a grid to display the contents of the list.
Change the declaration section of the form code to read as follows:
Private PersonList As New List(Of Person)
The code in the Click event for the Add button is as follows:
Dim newPerson As New Person With newPerson .LastName = LastNameTextBox.Text .FirstName = FirstNameTextBox.Text End With ' Add the person to the collection PersonList.Add(newPerson) ' Clear the form for the next entry FirstNameTextBox.Text = String.Empty LastNameTextBox.Text = String.Empty
This code creates a new Person object, populates it from the TextBox values, and adds it to the list. It then clears the TextBox values for entry of another person’s data.
Note: You cannot easily bind the List(Of Person) type to a grid because you need to map individual columns of the grid to specific Person object properties. You can either perform this mapping manually; populating the grid with code, or you can use object binding to bind the grid. Also, if you use object binding, you can automatically assign the properties of the person object to the TextBox values; in this case you could delete lines 2-5 of the above example. See my article, “Object Binding Tips and Tricks” in the March/April 2006 issue of CoDe Magazine for more information.
Repeat the process above for a Task object, creating a Task class with properties such as TaskName and Description and a Save method. Add a form for task information entry with code similar to the data entry form for person data. If you want to save a few steps, you can download the code for this article.
Reusing Generics Code
At this point, think about the things that your application normally needs to do during the save process that are the same for both tasks and persons. For example, before executing the save you probably want to perform all required validation, display a “saving” message, or other pre-processing. Then the code needs to call the business object to perform the unique processing, such as calling the appropriate Save procedure. Finally, you may have some post-processing in the form, such as displaying a “save complete” message or disabling the Save button until something else is changed.
You may be writing this code now in every form. By writing a generic method, you can write such code one time and use it with every form and with any business object.
To easily use the code in every form, create a base form class. If you haven’t spent a lot of time with form classes, see my article, “Give Your Forms a Base” in the March/April 2004 issue of CoDe Magazine.
Ensure every form then inherits from this base form class instead of System.Forms.Form by changing the code in the .Designer.vb file as follows:
Partial Class GenericMethodExample Inherits BaseWin
Then add the generic save method to the base form class:
Protected Sub Save(Of T)(ByVal objectToSave As T) ' Do any pre-processing ' Call the appropriate save method on the ' business object Dim _methodInfo As Reflection.MethodInfo _methodInfo = GetType(T).GetMethod("Save") _methodInfo.Invoke(objectToSave, Nothing) ' Do any post processing End Sub
The first thing that you will notice is that the generic method is declared slightly differently. The (Of T) syntax indicates that the Save method is a generic method. All generic methods require this syntax. You can define any number of type parameters, such as (Of TSource, TDestination). You can then use whatever type parameter variable(s) you use in the method declaration throughout the method to represent that type.
|Author’s Note: Though most standards suggest using T or T with a suffix, such as TKey or TValue, you can actually use any letters for the type parameter placeholder.
The method parameter in this example (objectToSave) is also declared to be As T, indicating that an object of the desired type must be passed to this method.
The first lines of code in this method would be your code for any processing required before actually saving the data. For example, this code could call validation methods or display a status message.
This method then uses reflection to call the appropriate Save method on the business object, using the GetMethod call to find the method on the defined business object and then invokes that method.
Any post-processing code comes last, such as displaying a “save complete” status message or disabling the Save button until the user makes another change.
Calling the method is easy. In the Click event for the Save button on each form, add code similar to the following:
For Each individual As Person In PersonList MyBase.Save(individual) Next
This example calls the Save method in the base form class for each person in the List.
The code for tasks is similar:
For Each item As Task In TaskList MyBase.Save(item) Next
By building generic methods, you can write generalized code that is type safe and works with any business object in your application.
Now that you know how to build a generic method, let’s look at the comparer, which not only requires a generic method, but a generic class as well. Here’s some code for a simple comparer class:
Public Class PropertyComparer(Of T) Implements IComparer(Of T) Private ReadOnly _propertyInfo As Reflection.PropertyInfo Private ReadOnly _sortDirection As SortOrder Public Sub New(ByVal propertyToSort As String, _ ByVal sortDirection As SortOrder) _sortDirection = sortDirection _propertyInfo = GetType(T).GetProperty(propertyToSort, _ Reflection.BindingFlags.Instance Or _ Reflection.BindingFlags.Public Or _ Reflection.BindingFlags.FlattenHierarchy Or _ Reflection.BindingFlags.IgnoreCase) End Sub Public Function Compare(ByVal x As T, ByVal y As T) _ As Integer Implements IComparer(Of T).Compare Dim xValue As Object = _propertyInfo.GetValue(x, Nothing) Dim yValue As Object = _propertyInfo.GetValue(y, Nothing) If _sortDirection = SortOrder.Ascending Then Return System.Collections.Comparer.Default.Compare( _ xValue, yValue) Else Return System.Collections.Comparer.Default.Compare( _ yValue, xValue) End If End Function End Class
This code defines a generic PropertyComparer class. It uses the (Of T) syntax to denote that it is a generic class, and implements the IComparer generic interface to support comparing two values.
The class has two fields: _propertyInfo and _sortDirection. The _propertyInfo field retains information on the property of the object selected for the sort. This allows you to sort on any business object property. The _sortDirection field determines whether the sort should be ascending or descending.
The constructor takes the property name as a string and the sort direction and uses reflection to get information on the property based on the property name.
The generic Compare method is called automatically during a sort operation to compare the values and sort them appropriately. This Compare method calls the default comparer for the defined property type. For example, it will call the String comparer for properties of type String.
After the PropertyComparer code is in place, performing the sort using the PropertyComparer class is easy. To sort the TaskList, for example, you use this code:
TaskList.Sort(New PropertyComparer(Of _ Task)("TaskName", SortOrder.Ascending))
The built-in Sort method of the generic List class takes a comparer as a parameter. In this example, the Sort method creates a new PropertyComparer for the Task business object. It then passes in the property name designating the Task business object property to use for the sort?in this case the TaskName property. It also defines the sort order. The TaskList is then sorted as defined.
Create your own PropertyComparer class whenever you want to sort on a particular business property or when the default sorting features don’t provide all of the power that you need. By creating this class in your base form class or code library, you can easily reuse it for any business object.
Refactor your methods to generic methods when you find similar methods that differ only in the data type that they use. You will greatly minimize the amount of code that you need to write for common functionality.