Previous section   Next section

9.4 Indexers and the Default Property

Some classes contain their own internal collection. For example, you might write your own School class that would contain, as a private member variable, a collection of the Students enrolled in the school. You might then want to access the School class as if it were an array of Students. To do so, you would use the default property, which will allow you to write:

Dim joe As Student = mySchool(5)

accessing the sixth element in mySchool's internal collection!

As another example, suppose you create a listbox control named myListBox that contains a list of strings stored in a one-dimensional array, a private member variable named myStrings. A listbox control contains member properties and methods in addition to its array of strings. However, it would be convenient to be able to access the listbox array with an index, just as if the listbox were an array. For example, such a property would permit statements like the following:

Dim theFirstString As String = myListBox(0)

You implement this with the default property. Each class can have one default property, designated with the Default keyword. It is common to use the property name Item for the default property, but that is not required.

You can retrieve the default property with or without the property name. The following two code lines both retrieve the default property (which in this case, is called Item); the first uses the name, the second doesn't:

Dim theFirstString As String = myListBox.Item(0)
Dim theFirstString As String = myListBox(0)

In either case, the default property is acting as an indexer, a property used to index into the class as if it were a collection.

Example 9-10 declares a listbox control class that contains a simple array (strings) and a default property (Item) that acts as an indexer for accessing its contents. To keep the example simple, you'll strip the listbox control down to a few features.

The listing ignores everything having to do with being a user control and focuses only on the list of strings the listbox maintains and methods for manipulating them. In a real application, of course, these are a small fraction of the total methods of a listbox, whose principal job is to display the strings and enable user choice.

Example 9-10. Using an indexer
Option Strict On
Imports System

Namespace Indexers
    'a simplified ListBox control
    Public Class ListBoxTest
        Private strings(255) As String
        Private ctr As Integer = 0

        'initialize the listbox with strings
        Public Sub New(ByVal ParamArray initialStrings( ) As String)
            Dim s As String

            'copy the strings passed in to the constructor
            For Each s In initialStrings
                strings(ctr) = s
                ctr += 1
            Next
        End Sub

        'add a single string to the end of the listbox
        Public Sub Add(ByVal theString As String)
            If ctr >= Strings.Length Then
                ' handle bad index
            Else
                Strings(ctr) = theString
                ctr += 1
            End If
        End Sub

        'allow array-like access
        Default Public Property Item(ByVal index As Integer) As String
            Get
                If index < 0 Or index >= strings.Length Then
                    'handle bad index
                Else
                    Return strings(index)
                End If
            End Get
            Set(ByVal Value As String)
                If index >= ctr Then
                    'handle error
                Else
                    strings(index) = Value
                End If
            End Set
        End Property

        'publish how many strings you hold
        Public Function Count( ) As Integer
            Return ctr
        End Function
    End Class

    Public Class Tester

        Public Sub Run( )
            'create a new listbox and initialize
            Dim lbt As New ListBoxTest("Hello", "World")
            Dim i As Integer

            Console.WriteLine("After creation...")
            For i = 0 To lbt.Count - 1
                Console.WriteLine("lbt({0}): {1}", i, lbt(i))
            Next

            'add a few strings
            lbt.Add("Who")
            lbt.Add("Is")
            lbt.Add("John")
            lbt.Add("Galt")

            Console.WriteLine("After adding strings...")
            For i = 0 To lbt.Count - 1
                Console.WriteLine("lbt({0}): {1}", i, lbt(i))
            Next

            'test the access
            Dim subst As String = "Universe"
            lbt(1) = subst

            'access all the strings
            Console.WriteLine("After editing strings...")
            For i = 0 To lbt.Count - 1
                Console.WriteLine("lbt({0}): {1}", i, lbt(i))
            Next
        End Sub

        Public Shared Sub Main( )
            Dim t As New Tester( )
            t.Run( )
        End Sub
    End Class

End Namespace

Output:
After creation...
lbt(0): Hello
lbt(1): World

After adding strings...
lbt(0): Hello
lbt(1): World
lbt(2): Who
lbt(3): Is
lbt(4): John
lbt(5): Galt

After editing strings...
lbt(0): Hello
lbt(1): Universe
lbt(2): Who
lbt(3): Is
lbt(4): John
lbt(5): Galt

Example 9-10 begins by creating two private member variables, strings and ctr:

Private strings(255) As String
Private ctr As Integer = 0

In this program, the listbox maintains a simple array of strings, named (appropriately) strings. The member variable ctr keeps track of how many strings are added to the array.

The constructor initializes the array with the strings passed in as parameters. Because you cannot know how many strings will be added, you use the keyword ParamArray, as described earlier in this chapter:

Public Sub New(ByVal ParamArray initialStrings( ) As String)
    Dim s As String

    'copy the strings passed in to the constructor
    For Each s In initialStrings
        strings(ctr) = s
        ctr += 1
    Next
End Sub

Our focus is on the default property, Item, created using the following code:

Default Public Property Item(ByVal index As Integer) As String

In Example 9-10, the Get( ) method endeavors to implement rudimentary bounds checking, and, assuming the index requested is acceptable, it returns the value requested:

Get
    If index < 0 Or index >= strings.Length Then
        'handle bad index
    Else
        Return strings(index)
    End If
End Get

The Set( ) method checks to make sure that the index you are setting already has a value in the listbox. If not, it treats the set as an error; note that new elements can only be added using the Add( ) method with this approach. The Set( ) accessor takes advantage of the implicit parameter value, which represents whatever is assigned to the property.

Set(ByVal Value As String)
    If index >= ctr Then
        'handle error
    Else
        strings(index) = Value
    End If
End Set

Thus, if you write:

lbt(5) = "Hello World"

the compiler will call the default property Item's Set( ) method on your object and pass in the string "Hello World" as an implicit parameter-named value.

9.4.1 Default Properties and Assignment

In Example 9-10, you cannot assign to an index that does not have a value. Thus, if you write:

lbt(10) = "wow!"

you would trigger the error handler in the Set( ) method, which would note that the index you've passed in (10) is larger than the counter (6).

Of course, you can use the Set( ) method for assignment; you simply have to handle the indexes you receive. To do so, you might change the Set( ) method to check the Length property of the buffer rather than the current value of the counter (ctr). If a value was entered for an index that did not yet have a value, you would update ctr:

Set(ByVal Value As String)
    If index >= strings.Length Then
        'handle error
    Else
        strings(index) = Value
        if ctr < index + 1 then
          ctr = index + 1
        end if
    End If
End Set

This allows you to create a "sparse" array in which you can assign to offset 10 without ever having assigned to offset 9. Thus, if you were to write:

lbt(10) = "wow!"

the output would be:

lbt(0): Hello
lbt(1): Universe
lbt(2): Who
lbt(3): Is
lbt(4): John
lbt(5): Galt
lbt(6):
lbt(7):
lbt(8):
lbt(9):
lbt(10): wow!

In the Run( ) method of Example 9-10, you create an instance of the ListBoxTest class named lbt and pass in two strings as parameters:

Dim lbt As New ListBoxTest("Hello", "World")

You then call Add( ) to add four more strings:

lbt.Add("Who")
lbt.Add("Is")
lbt.Add("John")
lbt.Add("Galt")

Finally, you modify the second value (at index 1):

Dim subst As String = "Universe"
lbt(1) = subst

At each step, you display each value in a loop:

For i = 0 To lbt.Count - 1
    Console.WriteLine("lbt({0}): {1}", i, lbt(i))
Next

9.4.2 Indexing on Other Values

VB.NET does not require that you always use an integer value as the index to a collection. When you create a custom collection class and create your indexer, you are free to overload the default property so that a given collection can be indexed�for example, by an integer value or by a string value, depending on the needs of the client.

In the case of your listbox, you might want to be able to index into the listbox based on a string. Example 9-11 illustrates a string index. Example 9-11 is identical to Example 9-10 except for the addition of an overloaded default property, which can match a string, and findString( ), a helper method created to support that index. The indexer calls findString( ) to return a record based on the value of the string provided.

Notice that the overloaded indexer of Example 9-11 and the indexer from Example 9-10 are able to coexist. The complete listing is shown, followed by the output, and then a detailed analysis.

Example 9-11. String indexer
Option Strict On
Imports System

Namespace Indexers
    'a simplified ListBox control
    Public Class ListBoxTest
        Private strings(255) As String
        Private ctr As Integer = 0

        'initialize the listbox with strings
        Public Sub New(ByVal ParamArray initialStrings( ) As String)
            Dim s As String

            'copy the strings passed in to the constructor
            For Each s In initialStrings
                strings(ctr) = s
                ctr += 1
            Next
        End Sub

        'add a single string to the end of the listbox
        Public Sub Add(ByVal theString As String)
            If ctr >= strings.Length Then
                ' handle bad index
            Else
                strings(ctr) = theString
                ctr += 1
            End If
        End Sub

        'allow array-like access
        Default Public Property Item( _
           ByVal index As Integer) As String
            Get
                If index < 0 Or index >= strings.Length Then
                    'handle bad index
                Else
                    Return strings(index)
                End If
            End Get
            Set(ByVal Value As String)
                If index >= ctr Then
                    'handle error
                Else
                    strings(index) = Value
                End If
            End Set
        End Property

        'index on string
        Default Public Property Item( _
           ByVal index As String) As String
            Get
                If index.Length = 0 Then
                    'handle bad index
                Else
                    Return strings(findString(index))
                End If
            End Get
            Set(ByVal Value As String)
                strings(findString(index)) = Value
            End Set
        End Property

        'helper method, given a string find
        'first matching record that starts with the target
        Private Function findString( _
           ByVal searchString As String) As Integer
            Dim i As Integer
            For i = 0 To strings.Length - 1
                If strings(i).StartsWith(searchString) Then
                    Return i
                End If
            Next
            Return -1
        End Function

        'publish how many strings you hold
        Public Function Count( ) As Integer
            Return ctr
        End Function
    End Class

    Public Class Tester

        Public Sub Run( )
            'create a new listbox and initialize
            Dim lbt As New ListBoxTest("Hello", "World")
            Dim i As Integer

            Console.WriteLine("After creation...")
            For i = 0 To lbt.Count - 1
                Console.WriteLine("lbt({0}): {1}", i, lbt(i))
            Next

            'add a few strings
            lbt.Add("Who")
            lbt.Add("Is")
            lbt.Add("John")
            lbt.Add("Galt")

            Console.WriteLine(vbCrLf & "After adding strings...")
            For i = 0 To lbt.Count - 1
                Console.WriteLine("lbt({0}): {1}", i, lbt(i))
            Next

            'test the access
            Dim subst As String = "Universe"
            lbt(1) = subst
            lbt("Hel") = "GoodBye"

            'access all the strings
            Console.WriteLine(vbCrLf & "After editing strings...")
            For i = 0 To lbt.Count - 1
                Console.WriteLine("lbt({0}): {1}", i, lbt(i))
            Next
        End Sub

        Public Shared Sub Main( )
            Dim t As New Tester( )
            t.Run( )
        End Sub
    End Class
End Namespace

Output:
lbt[0]: GoodBye
lbt[1]: Universe
lbt[2]: Who
lbt[3]: Is
lbt[4]: John
lbt[5]: Galt

In Example 9-11, the findString( ) method simply iterates through the strings held in strings until it finds a string that starts with the target string used in the index. If found, it returns the index of that string; otherwise it returns the value -1.

You can see in Main( ) that the user passes in a string segment to the index, just as was done with an integer:

lbt("Hel") = "GoodBye"

This calls the overloaded default property, which does some rudimentary error checking (in this case, making sure the string passed in has at least one letter) and then passes the value (Hel) to findString( ). It gets back an index and uses that index to index into the strings array:

Return strings(findString(index))

The Set( ) accessor works in the same way:

Set(ByVal Value As String)
    strings(findString(index)) = Value
End Set

If the string does not match, a value of -1 is returned, which is then used as an index into strings. This action then generates an exception (System.NullReferenceException), as you can see by un-commenting the following line in Main( ):

lbt["xyz"] = "oops"

The proper handling of not finding a string is, as they say, left as an exercise for the reader. You might consider displaying an error message or otherwise allowing the user to recover from the error. Exceptions are discussed in Chapter 11.


  Previous section   Next section
Top