Properties let the application view and modify an object’s data. Methods let the program invoke the object’s behaviors and perform actions. Together, properties and methods let the program send information (data values or commands) to the object.
In a sense, events do the reverse: They let the object send information to the program. When something noteworthy occurs in the object’s code, it can raise an event to tell the main program about it. The main program can then decide what to do about the event.
The following sections describe events. They explain how a class declares events and how other parts of the program can catch events.
A class object can raise events whenever it needs to notify to the program of changing circumstances. Normally, the class declares the event using the Event keyword. The following text shows the Event statement’s syntax:
[attribute_list] [accessibility] [Shadows] _ Event event_name([parameters]) [Implements interface.event]
The following sections describe the pieces of this declaration. Some of these are similar to earlier sections that describe constant, variable, and class declarations. By now, you should notice some familiarity in the use of the attribute_list and accessibility clauses. For more information on constant and variable declarations, see Chapter 4. For more information on class declarations, refer to the section “Classes” earlier in this chapter.
The attribute_list defines attributes that apply to the event. For example, the following declaration defines a description that the code editor should display for the ScoreAdded event:
Imports System.ComponentModel Public Class Student <Description("Occurs when a score is added to the object")> _ Public Event ScoreAdded(ByVal test_number As Integer) ... End Class
The accessibility value can take one of the following values: Public, Protected, Friend, Protected Friend, or Private. These values determine which pieces of code can catch the event. Following is an explanation of these keywords:
Public - Indicates that there are no restrictions on the event. Code inside or outside the class can catch the event.
Protected - Indicates that the event is accessible only to code in the same class or in a derived class.
Friend - Indicates that the event is available to all code inside or outside the class module within the same project. The difference between this and Public is that Public allows code outside of the project to access the subroutine. This is generally only an issue for code libraries (.dll files) and control libraries. For example, suppose that you build a code library containing a class that raises some events. You then write a program that uses the class. On the one hand, if the class declares an event with the Public keyword, then the code in the library and the code in the main program can catch the event. On the other hand, if the class declares an event with the Friend keyword, only the code in the library can catch the event, not the code in the main program.
Protected Friend - Indicates that the event has both Protected and Friend status. The event is available only within the same project and within the same class or a derived class.
Private - Indicates that the event is available only within the class that contains it. An instance of the class can catch this event, but code outside of the class cannot.
The Shadows keyword indicates that this event replaces an event in the parent class that has the same name but not necessarily the same parameters.
The parameters clause gives the parameters that the event will pass to event handlers. The syntax for the parameter list is the same as the syntax for declaring the parameter list for a subroutine or function.
If an event declares a parameter with the ByRef keyword, the code that catches the event can modify that parameter’s value. When the event handler ends, the class code that raised the event can read the new parameter value.
If the class implements an interface and the interface defines an event, this clause identifies this event as the one defined by the interface. For example, the IStudent interface shown in the following code defines the ScoreChanged event handler. The Student class implements the IStudent interface. The declaration of the ScoreChanged event handler uses the Implements keyword to indicate that this event handler provides the event handler defined by the IStudent interface.
Public Interface IStudent Event ScoreChanged() ... End Interface Public Class Student Implements IStudent Public Event ScoreChanged() Implements IStudent.ScoreChanged ... End Class
After it has declared an event, a class raises it with the RaiseEvent keyword. It should pass the event whatever parameters were defined in the Event statement.
For example, the Student class shown in the following code declares a ScoreChange event. Its AddScore method makes room for a new score, adds the score to the Scores array, and then raises the ScoreChanged event, passing the event handler the index of the score in the Scores array.
Public Class Student Private Scores() As Integer ... Public Event ScoreChanged(ByVal test_number As Integer) ... Public Sub AddScore(ByVal new_score As Integer) ReDim Preserve Scores(Scores.Length) Scores(Scores.Length - 1) = new_score RaiseEvent ScoreChanged(Scores.Length - 1) End Sub ... End Class
There are two ways that you can catch an object’s events. First, you can declare the object variable using the WithEvents keyword, as shown in the following code:
Private WithEvents TopStudent As Student
In the code editor, click the left drop-down list and select the variable’s name. In the right drop-down list, select the event. This makes the code editor create an empty event handler similar to the following one. When the object raises its ScoreChanged event, the event handler executes.
Private Sub TopStudent_ScoreChanged(ByVal test_number As Integer) _ Handles TopStudent.ScoreChanged End Sub
The second method for catching events is to use the AddHandler statement to define an event handler for the event. First, write the event handler subroutine. This subroutine must take parameters of the proper type to match those defined by the event’s declaration in the class. The following code shows a subroutine that can handle the ScoreChanged event. Note that the parameter’s name has been changed, but its accessibility (ByRef or ByVal) and data type must match those declared for the ScoreChanged event.
Private Sub HandleScoreChanged(ByVal quiz_num As Integer) End Sub
If the event handler’s parameter list is long and complicated, writing an event handler can be tedious. To make this easier, you can declare an object using the WithEvents keyword and use the drop-down lists to give it an event handler. Then you can edit the event handler to suit your needs (change its name, remove the Handles clause, change parameter names, and so forth).
After you build the event handler routine, use the AddHandler statement to assign the routine to a particular object’s event. The following statement makes the HandleScoreChanged event handler catch the TopStudent object’s ScoreChanged event:
AddHandler TopStudent.ScoreChanged, AddressOf HandleScoreChanged
Using AddHandler is particularly handy when you want to use the same event handler with more than one object. For example, you might write an event handler that validates a TextBox control’s contents to ensure that it contains a valid phone number. By repeatedly using the AddHandler statement, you can make the same event handler validate any number of TextBox controls.
AddHandler is also convenient if you want to work with an array of objects. The following code shows how a program might create an array of Student objects and then use the HandleScoreChanged subroutine to catch the ScoreChanged event for all of them:
' Create an array of Student objects. Const MAX_STUDENT As Integer = 30 Dim students(MAX_STUDENT) As Student For i As Integer = 0 To MAX_STUDENT students(i) = New Student Next i ' Add ScoreChanged event handlers. For i As Integer = 0 To MAX_STUDENT AddHandler students(i).ScoreChanged, AddressOf HandleScoreChanged Next i ...
If you plan to use AddHandler in this way, you may want to ensure that the events provide enough information for the event handler to figure out which object raised the event. For example, you might modify the ScoreChanged event so that it passes a reference to the object raising the event into the event handler. Then the shared event handler can determine which Student object had a score change.
If you add an event handler with AddHandler, you can later remove it with the RemoveHandler statement. The syntax is the same as the syntax for AddHandler, as shown here:
RemoveHandler TopStudent.ScoreChanged, AddressOf HandleScoreChanged
A second form of event declaration provides more control over the event. This version is quite a bit more complicated and at first can seem very confusing. Skim through the syntax and description that follow and then look at the example. Then if you go back and look at the syntax and description again, they should make more sense. This version is also more advanced and you may not need it often (if ever), so you can skip it for now if you get bogged down.
This version enables you to define routines that are executed when the event is bound to an event handler, removed from an event handler, and called. The syntax is as follows:
[attribute_list] [accessibility] [Shadows] _ Custom Event event_name As delegate_name [Implements interface.event] [attribute_list] AddHandler(ByVal value As delegate_name) ... End AddHandler [attribute_list] RemoveHandler(ByVal value As delegate_name) ... End RemoveHandler [attribute_list] RaiseEvent(delegate_signature) ... End RaiseEvent End Event
The attribute_list, accessibility, Shadows, and Implements interface.event parts have the same meaning as in the previous, simpler event declaration. See the section, “Declaring Events,” earlier in this chapter for information on these pieces.
The delegate_name tells Visual Basic the type of event handler that will catch the event. For example, the delegate might indicate a subroutine that takes as a parameter a String variable named new_name. The following code shows a simple delegate for this routine. The delegate’s name is NameChangedDelegate. It takes a String parameter named new_name.
Public Delegate Sub NameChangedDelegate(ByVal new_name As String)
For more information on delegates, see the section “Delegates” in Chapter 4.
The main body of the custom event declaration defines three routines named AddHandler, RemoveHandler, and RaiseEvent. You can use these three routines to keep track of the event handlers assigned to an object (remember that the event declaration is declaring an event for a class) and to call the event handlers when appropriate.
The AddHandler routine executes when the program adds an event handler to the object. It takes as a parameter a delegate variable named value. This is a reference to a routine that matches the delegate defined for the event handler. For example, if the main program uses the AddHandler statement to add the subroutine Employee_NameChanged as an event handler for this object, then the parameter to AddHandler is a reference to the Employee_NameChanged subroutine.
Normally, the AddHandler subroutine saves the delegate in some sort of collection so that the RaiseEvent subroutine described shortly can invoke it.
The RemoveHandler subroutine executes when the program removes an event handler from the object. It takes as a parameter a delegate variable indicating the event handler that should be removed. Normally, the RemoveHandler subroutine deletes the delegate from the collection that AddHandler used to originally store the delegate.
Finally, the RaiseEvent subroutine executes when the object’s code uses the RaiseEvent statement to raise the event. For example, suppose that the Employee class defines the NameChanged event. When the class’s FirstName or LastName property procedure changes an Employee object’s name, it uses the RaiseEvent statement to raise the NameChanged event. At that point, the custom RaiseEvent subroutine executes.
Normally, the RaiseEvent subroutine calls the delegates stored by the AddHandler subroutine in the class’s collection of event delegates.
The following code shows how the Employee class might implement a custom NameChanged event. It begins with the FirstName and LastName property procedures. The Property Set procedures both use the RaiseEvent statement to raise the NameChanged event. This is fairly straightforward and works just as it would if the class used the simpler event declaration. Next the code defines an ArrayList named m_EventDelegates that it will use to store the event handler delegates. It then uses a Delegate statement to define the types of event handlers that this event will call. In this example, the event handler must be a subroutine that takes a String parameter passed by value. Now the code defines the NameChanged custom event. Notice that the Custom Event statement ends with the delegate NameChangedDelegate. If you type this first line and press Enter, Visual Basic creates empty AddHandler, RemoveHandler, and RaiseEvent subroutines for you.
Subroutine AddHandler displays a message and saves the delegate in the m_EventDelegates list. When AddHandler is called, the value parameter refers to an event handler routine that has the proper type.
The subroutine RemoveHandler displays a message and removes a delegate from the m_EventDelegates list. In a real application, this routine would need some error-handling code in case the delegate is not in m_EventDelegates.
When the FirstName and LastName property set procedures use the RaiseEvent statement, the RaiseEvent subroutine executes. This routine’s parameter takes whatever value the class used when it used the RaiseEvent statement. This subroutine displays a message and then loops through all the delegates stored in the m_EventDelegates list, invoking each. It passes each delegate the new_name value it received in its parameter, with spaces replaced by plus signs.
Public Class Employee ' The FirstName property. Private m_FirstName As String Public Property FirstName() As String Get Return m_FirstName End Get Set(ByVal value As String) m_FirstName = value RaiseEvent NameChanged(m_FirstName & " " & m_LastName) End Set End Property ' The LastName property. Private m_LastName As String Public Property LastName() As String Get Return m_LastName End Get Set(ByVal value As String) m_LastName = value RaiseEvent NameChanged(m_FirstName & " " & m_LastName) End Set End Property ' List to hold the event handler delegates. Private m_EventDelegates As New ArrayList ' Defines the event handler signature. Public Delegate Sub NameChangedDelegate(ByVal new_name As String) ' Define the custom NameChanged event. Public Custom Event NameChanged As NameChangedDelegate AddHandler(ByVal value As NameChangedDelegate) Debug.WriteLine("AddHandler") m_EventDelegates.Add(value) End AddHandler RemoveHandler(ByVal value As NameChangedDelegate) Debug.WriteLine("RemoveHandler") m_EventDelegates.Remove(value) End RemoveHandler RaiseEvent(ByVal new_name As String) Debug.WriteLine("RaiseEvent (" & new_name & ")") For Each a_delegate As NameChangedDelegate In m_EventDelegates a_delegate(new_name.Replace(" ", "+")) Next a_delegate End RaiseEvent End Event End Class
The following code demonstrates the NameChanged event handler. It creates a new Employee object and then uses two AddHandler statements to assign the event Employee_NameChanged handler to the object’s NameChanged event. This makes the custom AddHandler subroutine execute twice and save two references to the Employee_NameChanged subroutine in the delegate list. Next, the program sets the Employee object’s FirstName. The FirstName property set procedure raises the NameChanged event so the RaiseEvent subroutine executes. RaiseEvent loops through the delegate list and calls the delegates. In this example, that means the subroutine Employee_NameChanged executes twice. The program then uses a RemoveHandler statement to remove an Employee_NameChanged event handler. The custom RemoveHandler subroutine executes and removes one instance of the Employee_NameChanged subroutine from the delegate list. Next the program sets the Employee object’s LastName. The LastName property set procedure uses the RaiseEvent statement so the RaiseEvent subroutine executes. Now there is only one instance of the Employee_NameChanged subroutine in the delegate list, so it is called once. Finally, the code uses the RemoveHandler statement to remove the remaining instance of Employee_ NameChanged from the delegate list. The RemoveHandler subroutine executes and removes the instance from the delegate list.
Dim emp As New Employee AddHandler emp.NameChanged, AddressOf Employee_NameChanged AddHandler emp.NameChanged, AddressOf Employee_NameChanged emp.FirstName = "Rod" RemoveHandler emp.NameChanged, AddressOf Employee_NameChanged emp.LastName = "Stephens" RemoveHandler emp.NameChanged, AddressOf Employee_NameChanged
The following text shows the result in the Debug window. It shows where the AddHandler, RaiseEvent, and RemoveHandler subroutines execute. You can also see where the Employee_NameChanged event handler executes and displays its name.
AddHandler AddHandler RaiseEvent (Rod ) Employee_NameChanged: Rod+ Employee_NameChanged: Rod+ RemoveHandler RaiseEvent (Rod Stephens) Employee_NameChanged: Rod+Stephens RemoveHandler
If you declare a variable in a class with the Shared keyword, all objects of the class share a single instance of that variable. You can get or set the variable’s value through any instance of the class.
For example, suppose the Student class declares a shared NumStudents variable, as shown in the following code:
Public Class Student Shared NumStudents As Integer ... End Class
In this case, all instances of the Student class share the same NumStudents value. The following code creates two Student objects. It uses one to set the shared NumStudents value and uses the other to display the result.
Dim student1 As New Student Dim student2 As New Student student1.NumStudents = 100 MessageBox.Show(student2.NumStudents)
Because all instances of the class share the same variable, any changes to the value that you make using one object are visible to all the others. Figure 16-4 illustrates this idea. Each Student class instance has its own FirstName, LastName, Scores, and other individual data values, but they all share the same NumStudents value.
Figure 16-4: If a variable in a class is declared Shared, all instances of a class share the same value.
Because a shared variable is associated with the class as a whole and not a specific instance of the class, Visual Basic lets you refer to it using the class’s name in addition to using specific instance variables. The following code defines a new Student object and uses it to set NumStudents to 100. It then uses the class name to display the NumStudents value.
Dim student1 As New Student student1.NumStudents = 100 MessageBox.Show(Student.NumStudents)
Shared methods are a little less intuitive than shared variables. Like shared variables, shared methods are accessible using the class’s name. For example, the NewStudent function shown in the following code is declared with the Shared keyword. This function creates a new Student object, initializes it by adding it to some sort of database, and then returns the new object.
Public Class Student ... ' Return a new Student. Public Shared Function NewStudent() As Student ' Instantiate the Student. Dim new_student As New Student ' Add the new student to the database. '... ' Return the new student. Return new_student End Function ... End Class
The type of function that creates a new instance of a class is sometimes called a factory method. In some cases, you can use an appropriate constructor instead of a factory method. One time when a factory method is useful is when object creation might fail. If data passed to the method is invalid, some resource (such as a database) prohibits the new object (perhaps a new Student has the same name as an existing Student), or the object may come from more than one place (for example, it may either be a new object or one taken from a pool of existing objects). In those cases, a factory method can return Nothing. A constructor could raise an error, but it cannot force an object to not be created.
If you want to force the program to use a factory method rather than creating an instance of the object directly, give the class a private constructor. Code that lies outside of the class cannot use the constructor because it is private. It also cannot use the default constructor associated with the New statement because the class has an explicit constructor. The code must create new objects by using the factory method, which can use the private constructor because it’s inside the class.
As is the case with shared variables, you can access a shared method by using any instance of the class or by using the class’s name. The following code declares the student1 variable and initializes it by calling the NewStudent factory method using the class’s name. Next, the code declares student2 and uses the student1 object’s NewStudent method to initialize it.
Dim student1 As Student = Student.NewStudent() Dim student2 As Student = student1.NewStudent()
By default, Visual Basic warns you if you try to access a shared member by using a specific variable such as student1 rather than the class name.
One oddity of shared methods is that they can use class variables and methods only if they are also shared. If you think about accessing a shared method through the class name rather than an instance of the class, this makes sense. If you don’t use an instance of the class, then there is no instance to give the method data.
In the following code, the Student class declares the variable NumStudents with the Shared keyword so shared methods can use that value. It declares the instance variables FirstName and LastName without the Shared keyword, so shared methods cannot use those values. The shared NewStudent method starts by incrementing the shared NumStudents value. It then creates a new Student object and initializes its FirstName and LastName values. It can initialize those values because it is using a specific instance of the class and that instance has FirstName and LastName values.
Public Class Student Public Shared NumStudents As Integer Public FirstName As String Public LastName As String ... ' Return a new Student. Public Shared Function NewStudent() As Student ' Increment the number of Students loaded. NumStudents += 1 ' Instantiate the Student. Dim new_student As New Student new_student.FirstName = "<unknown>" new_student.LastName = "<unknown>" ' Add the new student to the database. ... ' Return the new student. Return new_student End Function ... End Class
Figure 16-5 illustrates the situation. The shared NewStudent method is contained within the class itself and has access to the NumStudents variable. If it wanted to use a FirstName, LastName, or Scores value, however, it needs to use an instance of the class.
Figure 16-5: A shared method can only access other shared variables and methods.