Creating Classes


Using objects is fairly straightforward and intuitive. It is the kind of thing that even the most novice programmers pick up and accept rapidly. Creating classes and objects is a bit more complex and interesting, and is covered throughout the rest of this chapter.

Basic Classes

As discussed earlier, objects are merely instances of a specific template (a class). The class contains the code that defines the behavior of its objects, as well as defining the instance variables that will contain the object’s individual data.

Classes are created using the Class keyword, and include definitions (declaration) and implementations (code) for the variables, methods, properties, and events that make up the class. Each object created based on this class will have the same methods, properties, and events, and its own set of data defined by the fields in the class.

The Class Keyword

If you want to create a class that represents a person - a Person class - you could use the Class keyword:

  Public Class Person    ' Implementation code goes here End Class 

As you know, Visual Basic projects are composed of a set of files with the .vb extension. It’s possible to have each file contain multiple classes, which means that within a single file you could have something like this:

  Public Class Adult   ' Implementation code goes here. End Class Public Class Senior   ' Implementation code goes here. End Class Public Class Child   ' Implementation code goes here. End Class 

The most common and preferred approach is to have a single class per file. This is because the Visual Studio 2005 Solution Explorer and the code-editing environment are tailored to make it easy to navigate from file to file to find code. For instance, if you create a single class file with all these classes, the Solution Explorer simply displays a single entry, as shown in Figure 3-2.

image from book
Figure 3-2

However, the Visual Studio IDE does provide the Class View window. If you do decide to put multiple classes in each physical .vb file, you can make use of the Class View window to quickly and efficiently navigate through the code, jumping from class to class without having to manually locate those classes in specific code files, as shown in Figure 3-3.

image from book
Figure 3-3

The Class View window is incredibly useful even if you stick with one class per file, as it still provides you with a class-based view of the entire application.

This chapter uses one class per file in its examples, as this is the most common approach. To begin, open the Visual Studio IDE and create a new Windows Application project named ObjectIntro. Choose the Project image from book Add Class menu option to add a new class module to the project. You’ll be presented with the standard Add New Item dialog box. Change the name to Person.vb and click the Open button. The result will be the following code, which defines the Person class:

  Public Class Person End Class 

With the Person class created, you’re ready to start adding code to declare the interface, implement the behaviors, and declare the instance variables.

Fields

Fields are variables declared in the class that will be available to each individual object when the application is run. Each object gets its own set of data - basically, each object gets its own copy of the fields.

Earlier, you learned that a class is simply a template from which you create specific objects. Variables that you define within the class are also simply templates - and each object gets its own copy of those variables in which to store its data.

Declaring member variables is as easy as declaring variables within the Class block structure. Add the following code to the Person class:

 Public Class Person    Private mName As String    Private mBirthDate As Date End Class

You can control the scope of the fields by using the following keywords:

  • Private - Available only to code within the class

  • Friend - Available only to code within the project/component

  • Protected - Available only to classes that inherit from the class (discussed in detail in Chapter 4)

  • Protected Friend - Available to code within your project/component and classes that inherit from the class whether in the project or not (discussed in detail in Chapter 4)

  • Public - Available to code outside the class and to any projects that reference the assembly

Typically, fields are declared using the Private keyword, making them available only to code within each instance of the class. Choosing any other option should be done with great care, because all the other options allow code outside the class to directly interact with the variable, meaning that the value could be changed and your code would never know that a change took place.

Tip 

One common exception to making fields Private is the use of the Protected keyword, as discussed in Chapter 4.

Methods

Objects typically need to provide services (or functions) that can be called when working with the object. Using their own data or data passed as parameters to the method, they manipulate information to yield a result or perform an action.

Methods declared as Public, Friend, or Protected in scope define the interface of the class. Methods that are Private in scope are available to the code only within the class itself and can be used to provide structure and organization to code. As discussed earlier, the actual code within each method is called an implementation, while the declaration of the method itself is what defines the interface.

Methods are simply routines that are coded within the class to implement the services you want to provide to the users of an object. Some methods return values or provide information to the calling code. These are called interrogative methods. Others, called imperative methods, just perform an action and return nothing to the calling code.

In Visual Basic, methods are implemented using Sub (for imperative methods) or Function (for interrogative methods) routines within the class module that defines the object. Sub routines may accept parameters, but they don’t return any result value when they are complete. Function routines can also accept parameters, and they always generate a result value that can be used by the calling code.

A method declared with the Sub keyword is merely one that returns no value. Add the following code to the Person class:

  Public Sub Walk()   ' implementation code goes here End Sub 

The Walk() method presumably contains some code that performs some useful work when called but has no result value to return when it is complete. To make use of this method, you might write code such as this:

  Dim myPerson As New Person() myPerson.Walk() 

Once you’ve created an instance of the Person class, you can simply invoke the Walk() method.

Methods That Return Values

If you have a method that does generate some value that should be returned, you need to use the Function keyword:

  Public Function Age() As Integer   Return CInt(DateDiff(DateInterval.Year, mBirthDate, Now())) End Function 

Notice that you must indicate the datatype of the return value when you declare a Function. This example returns the calculated age as a result of the method. You can return any value of the appropriate datatype by using the Return keyword.

You can also return the value without using the Return keyword, by setting the value of the function name itself:

 Public Function Age() As Integer     Age = CInt(DateDiff(DateInterval.Year, mBirthDate, Now())) End Function

This is functionally equivalent to the previous code. Either way, you can use this method with code similar to the following:

  Dim myPerson As New Person() Dim age As Integer age = myPerson.Age() 

The Age() method returns an Integer data value that you can use in the program as required; in this case, you’re just storing it in a variable.

Indicating Method Scope

Adding the appropriate keyword in front of the method declaration indicates the scope:

  Public Sub Walk() 

This indicates that Walk() is a Public method and thus is available to code outside the class and even outside the current project. Any application that references the assembly can use this method. Being Public, this method becomes part of the object’s interface.

Alternately, you might choose to restrict access to the method somewhat:

  Friend Sub Walk() 

By declaring the method with the Friend keyword, you are indicating that it should be part of the object’s interface only for code inside the project; any other applications or projects that make use of the assembly will not be able to call the Walk() method.

The Private keyword indicates that a method is only available to the code within your particular class:

  Private Function Age() As Integer 

Private methods are very useful to help organize complex code within each class. Sometimes the methods contain very lengthy and complex code. In order to make this code more understandable, you may choose to break it up into several smaller routines, having the main method call these routines in the proper order. Moreover, you can use these routines from several places within the class, so by making them separate methods, you enable reuse of the code. These subroutines should never be called by code outside the object, so you make them Private.

Method Parameters

You will often want to pass information into a method as you call it. This information is provided via parameters to the method. For instance, in the Person class, perhaps you want the Walk() method to track the distance the person walks over time. In such a case, the Walk() method would need to know how far the person is to walk each time the method is called. Add the following code to the Person class:

 Public Class Person   Private mName As String   Private mBirthDate As Date   Private mTotalDistance As Integer   Public Sub Walk(ByVal distance As Integer)      mTotalDistance += distance   End Sub   Public Function Age() As Integer     Return CInt(DateDiff(DateInterval.Year, mBirthDate, Now()))   End Function End Class

With this implementation, a Person object will sum all of the distances walked over time. Each time the Walk() method is called, the calling code must pass an Integer value, indicating the distance to be walked. Your code to call this method would be similar to the following code:

  Dim myPerson As New Person() myPerson.Walk(12) 

The parameter is accepted using the ByVal keyword. This indicates that the parameter value is a copy of the original value. This is the default way in which Visual Basic accepts all parameters. Typically, this is desirable because it means that you can work with the parameter inside the code, changing its value with no risk of accidentally changing the original value in the calling code.

If you do want to be able to change the value in the calling code, you can change the declaration to pass the parameter by reference by using the ByRef qualifier:

  Public Sub Walk(ByRef distance As Integer) 

In this case, you’ll get a reference (or pointer) back to the original value, rather than receive a copy. This means that any change you make to the distance parameter is reflected back in the calling code, very similar to the way object references work, as discussed earlier in this chapter.

Tip 

Using this technique can be dangerous, as it is not explicitly clear to the caller of the method that the value will change. Such unintended side effects can be hard to debug and should be avoided.

Properties

The .NET environment provides for a specialized type of method called a property. A property is a method specifically designed for setting and retrieving data values. For instance, you declared a variable in the Person class to contain a name, so the Person class may include code to allow that name to be set and retrieved. This can be done using regular methods:

  Public Sub SetName(ByVal name As String)   mName = name End Sub Public Function GetName() As String   Return mName End Function 

Using methods like these, you write code to interact with the object:

  Dim myPerson As New Person() myPerson.SetName("Jones") Messagebox.Show(myPerson.GetName()) 

While this is perfectly acceptable, it is not as nice as it could be with the use of a property. A Property style method consolidates the setting and retrieving of a value into a single structure, and makes the code within the class smoother overall. You can rewrite these two methods into a single property. Add the following code to the Person class:

  Public Property Name() As String   Get      Return mName   End Get   Set(ByVal Value As String)     mName = Value   End Set End Property 

By using a property method instead, you can make the client code much more readable:

  Dim myPerson As New Person() myPerson.Name = "Jones" Messagebox.Show(myPerson.Name) 

The Property method is declared with both a scope and a datatype:

 Public Property Name() As String

In this example, you’ve declared the property as Public in scope, but it can be declared using the same scope options as any other method - Public, Friend, Private, or Protected.

The return datatype of this property is String. A property can return virtually any datatype appropriate for the nature of the value. In this regard, a property is very similar to a method declared using the Function keyword.

Though a Property method is a single structure, it is divided into two parts: a getter and a setter. The getter is contained within a Get...End Get block and is responsible for returning the value of the property on demand:

 Get   Return mName End Get

Though the code in this example is very simple, it could be more complex, perhaps calculating the value to be returned or applying other business logic to change the value as it is returned. Likewise, the code to change the value is contained within a Set End Set block:

 Set(ByVal Value As String)    mName = Value End Set

The Set statement accepts a single parameter value that stores the new value. The code in the block can then use this value to set the property’s value as appropriate. The datatype of this parameter must match the datatype of the property itself. Having the parameter declared in this manner enables you to change the name of the variable used for the parameter value if needed.

By default, the parameter is named Value, but you can change the parameter name to something else, as shown here:

  Set(ByVal NewName As String)    mName = NewName End Set

In many cases, you can apply business rules or other logic within this routine to ensure that the new value is appropriate before you actually update the data within the object. It is also possible to restrict either the Get or Set block to be narrower in scope than the scope of the property itself. For instance, you may want to allow any code to retrieve the property value, but only allow other code in your project to alter the value. In this case, you can restrict the scope of the Set block to Friend, while the Property itself is scoped as Public:

 Public Property Name() As String   Get     Return mName   End Get   Friend Set(ByVal Value As String)     mName = Value   End Set End Property

The new scope must be more restrictive than the scope of the Property itself, and either the Get or Set block can be restricted, not both. The one you don’t restrict uses the scope of the Property method.

Parameterized Properties

The Name property you created is an example of a single-value property. You can also create property arrays or parameterized properties. These properties reflect a range, or array, of values. For example, people often have several phone numbers. You might implement a PhoneNumber property as a parameterized property, storing not only phone numbers, but also a description of each number. To retrieve a specific phone number you’d write code such as the following:

  Dim myPerson As New Person() Dim homePhone As String homePhone = myPerson.Phone("home") 

Or, to add or change a specific phone number, you’d write the following code:

  myPerson.Phone("work") = "555-9876" 

Not only are you retrieving and updating a phone number property, but you’re also updating a specific phone number. This implies a couple of things. First, you can no longer use a simple variable to hold the phone number, as you are now storing a list of numbers and their associated names. Second, you’ve effectively added a parameter to your property. You’re actually passing the name of the phone number as a parameter on each property call.

To store the list of phone numbers, you can use the Hashtable class. The Hashtable is very similar to the standard VB Collection object, but it is more powerful - allowing you to test for the existence of a specific element. Add the following declaration to the Person class:

 Public Class Person   Private mName As String   Private mBirthDate As Date   Private mTotalDistance As Integer   Private mPhones As New Hashtable 

You can implement the Phone property by adding the following code to the Person class:

  Public Property Phone(ByVal location As String) As String   Get     Return CStr(mPhones.Item(Location))   End Get   Set(ByVal Value As String)     If mPhones.ContainsKey(location) Then       mPhones.Item(location) = Value     Else       mPhones.Add(location, Value)     End If   End Set End Property

The declaration of the Property method itself is a bit different from what you’ve seen:

 Public Property Phone(ByVal location As String) As String

In particular, you’ve added a parameter, location, to the property itself. This parameter will act as the index into the list of phone numbers, and must be provided when either setting or retrieving phone number values.

Because the location parameter is declared at the Property level, it is available to all code within the property, including both the Get and Set blocks. Within your Get block, you use the location parameter to select the appropriate phone number to return from the Hashtable:

 Get   Return mPhones.Item(location) End Get

With this code, if there is no value stored matching the location, then you get a trappable runtime error.

Similarly, in the Set block, you use the location to update or add the appropriate element in the Hashtable. In this case, you’re using the ContainsKey method of Hashtable to determine whether the phone number already exists in the list. If it does, then you simply update the value in the list; otherwise, you add a new element to the list for the value:

 Set(ByVal Value As String)   If mPhones.ContainsKey(location) Then     mPhones.Item(location) = Value   Else     mPhones.Add(location, Value)   End If End Set

In this way, you’re able to add or update a specific phone number entry based on the parameter passed by the calling code.

Read-Only Properties

There are times when you may want a property to be read-only, so that it can’t be changed. In the Person class, for instance, you may have a read-write property for BirthDate, but only a read-only property for Age. If so, the BirthDate property is a normal property, as follows:

  Public Property BirthDate() As Date   Get     Return mBirthDate   End Get   Set(ByVal Value As Date)    mBirthDate = Value   End Set End Property 

The Age value, on the other hand, is a derived value based on BirthDate. This is not a value that should ever be directly altered, so it is a perfect candidate for read-only status.

You already have an Age method implemented as a Function. Remove that code from the Person class, because you’ll be replacing it with a Property routine instead. The difference between a Function routine and a ReadOnly Property is quite subtle. Both return a value to the calling code, and either way the object is running a subroutine defined by the class module to return the value.

The difference is less a programmatic one than a design choice. You could create all your objects without any Property routines at all, just using methods for all interactions with the objects. However, Property routines are obviously attributes of an object, while a Function might be an attribute or a method. By carefully implementing all attributes as ReadOnly Property routines, and any interrogative methods as Function routines, you create more readable and understandable code.

To make a property read-only, use the ReadOnly keyword and only implement the Get block:

  Public ReadOnly Property Age() As Integer   Get     Return CInt(DateDiff(DateInterval.Year, mdtBirthDate, Now()))   End Get End Property 

Because the property is read-only, you’ll get a syntax error if you attempt to implement a Set block.

Write-Only Properties

As with read-only properties, there are times when a property should be write-only, whereby the value can be changed but not retrieved.

Many people have allergies, so perhaps the Person object should have some understanding of the ambient allergens in the area. This is not a property that should be read from the Person object, as allergens come from the environment, rather than from the person, but it is data that the Person object needs in order to function properly. Add the following variable declaration to the class:

 Public Class Person   Private mstrName As String   Private mdtBirthDate As Date   Private mintTotalDistance As Integer   Private colPhones As New Hashtable()   Private mAllergens As Integer 

You can implement an AmbientAllergens property as follows:

  Public WriteOnly Property AmbientAllergens() As Integer   Set(ByVal Value As Integer)     mAllergens = Value   End Set End Property 

To create a write-only property, use the WriteOnly keyword and only implement a Set block in the code. The property is write-only, so you’ll get a syntax error if you attempt to implement a Get block.

The Default Property

Objects can implement a default property. A default property can be used to simplify the use of an object at times, by making it appear as if the object has a native value. A good example of this behavior is the Collection object, which has a default property called Item that returns the value of a specific item, allowing you to write the following:

 Dim mData As New HashTable() Return mData(index)

Default properties must be parameterized properties. A property without a parameter cannot be marked as the default. This is a change from previous versions of Visual Basic, where any property could be marked as the default.

Our Person class has a parameterized property - the Phone property you built earlier. You can make this the default property by using the Default keyword:

  Default Public Property Phone(ByVal location As String) As String   Get     Return CStr(mPhones.Item(location))   End Get   Set(ByVal Value As String)     If mPhones.ContainsKey(location) Then       mPhones.Item(location) = Value     Else       mPhones.Add(location, Value)     End If   End Set End Property

Prior to this change, you would have needed code such as the following to use the Phone property:

  Dim myPerson As New Person() MyPerson.Phone("home") = "555-1234" 

Now, with the property marked as Default, you can simplify the code:

  myPerson("home") = "555-1234" 

By picking appropriate default properties, you can potentially make the use of objects more intuitive.

Events

Both methods and properties enable you to write code that interacts with your objects by invoking specific functionality as needed. It is often useful for objects to provide notification as certain activities occur during processing. You see examples of this all the time with controls, where a button indicates that it was clicked via a Click event, or a text box indicates that its contents have been changed via the TextChanged event.

Objects can raise events of their own, providing a powerful and easily implemented mechanism by which objects can notify client code of important activities or events. In Visual Basic, events are provided using the standard .NET mechanism of delegates. Before discussing delegates, let’s explore how to work with events in Visual Basic.

Handling Events

We are all used to seeing code in a form to handle the Click event of a button, such as the following code:

 Private Sub Button1_Click(ByVal sender As System.Object, _   ByVal e As System.EventArgs) Handles Button1.Click End Sub

Typically, we write our code in this type of routine without paying a lot of attention to the code created by the Visual Studio IDE. However, let’s take a second look at that code, as there are a couple of important things to note here.

First, notice the use of the Handles keyword. This keyword specifically indicates that this method will be handling the Click event from the Button1 control. Of course, a control is just an object, so what is indicated here is that this method will be handling the Click event from the Button1 object.

Second, notice that the method accepts two parameters. The Button control class defines these parameters. It turns out that any method that accepts two parameters with these datatypes can be used to handle the Click event. For instance, you could create a new method to handle the event:

 Private Sub MyClickMethod(ByVal s As System.Object, _     ByVal args As System.EventArgs) Handles Button1.Click End Sub

Even though you’ve changed the method name and the names of the parameters, you are still accepting parameters of the same datatypes, and you still have the Handles clause to indicate that this method handles the event.

Handling Multiple Events

The Handles keyword offers even more flexibility. Not only can the method name be anything you choose, but a single method can handle multiple events if you desire. Again, the only requirement is that the method and all the events being raised must have the same parameter list.

Tip 

This explains why all the standard events raised by the .NET system class library have exactly two parameters - the sender and an EventArgs object. Being so generic makes it possible to write very generic and powerful event handlers than can accept virtually any event raised by the class library.

One common scenario where this is useful is when you have multiple instances of an object that raises events, such as two buttons on a form:

 Private Sub MyClickMethod(ByVal sender As System.Object, _     ByVal e As System.EventArgs) _     Handles Button1.Click, Button2.Click End Sub

Notice that the Handles clause has been modified so that it has a comma-separated list of events to handle. Either event will cause the method to run, providing a central location in which to handle these events.

The WithEvents Keyword

The WithEvents keyword tells Visual Basic that you want to handle any events raised by the object within the code:

 Friend WithEvents Button1 As System.Windows.Forms.Button

The WithEvents keyword makes any events from an object available for use, while the Handles keyword is used to link specific events to the methods so that you can receive and handle them. This is true not only for controls on forms, but also for any objects that you create.

The WithEvents keyword cannot be used to declare a variable of a type that doesn’t raise events. In other words, if the Button class didn’t contain code to raise events, you’d get a syntax error when you attempted to declare the variable using the WithEvents keyword.

The compiler can tell which classes will and won’t raise events by examining their interface. Any class that will be raising an event has that event declared as part of its interface. In Visual Basic, this means that you will have used the Event keyword to declare at least one event as part of the interface for the class.

Raising Events

Your objects can raise events just like a control, and the code using the object can receive these events by using the WithEvents and Handles keywords. Before you can raise an event from your object, however, you need to declare the event within the class by using the Event keyword.

In the Person class, for instance, you may want to raise an event anytime the Walk method is called. If you call this event Walked, you can add the following declaration to the Person class:

 Public Class Person   Private mstrName As String   Private mdtBirthDate As Date   Private mintTotalDistance As Integer   Private colPhones As New Hashtable()   Private mintAllergens As Integer   Public Event Walked() 

Events can also have parameters, values that are provided to the code receiving the event. A typical button’s Click event receives two parameters, for instance. In the Walked() method, perhaps you want to also indicate the distance that was walked. You can do this by changing the event declaration:

  Public Event Walked(ByVal distance As Integer) 

Now that the event is declared, you can raise that event within the code where appropriate. In this case, you’ll raise it within the Walk() method, so anytime a Person object is instructed to walk, it fires an event indicating the distance walked. Make the following change to the Walk() method:

 Public Sub Walk(ByVal distance As Integer)    mTotalDistance += distance    RaiseEvent Walked(distance) End Sub

The RaiseEvent keyword is used to raise the actual event. Because the event requires a parameter, that value is passed within parentheses and is delivered to any recipient that handles the event.

In fact, the RaiseEvent statement causes the event to be delivered to all code that has the object declared using the WithEvents keyword with a Handles clause for this event, or any code that has used the AddHandler method. The AddHandler method is discussed shortly.

If more than one method will be receiving the event, the event is delivered to each recipient one at a time. By default, the order of delivery isn’t defined - meaning you can’t predict the order in which the recipients receive the event - but the event is delivered to all handlers. Note that this is a serial, synchronous process. The event is delivered to one handler at a time, and it is not delivered to the next handler until the current handler is complete. Once you call the RaiseEvent method, the event is delivered to all listeners one after another until it is complete; there is no way for you to intervene and stop the process in the middle.

Declaring and Raising Custom Events

As just noted, by default you have no control over how events are raised. You can overcome this limitation by using a more explicit form of declaration for the event itself. Rather than use the simple Event keyword, you can declare a custom event. This is for more advanced scenarios, as it requires that you provide the implementation for the event itself.

The concept of delegates is covered in detail later in this chapter, but it is necessary to look at them briefly here in order to declare a custom event. A delegate is a definition of a method signature. When you declare an event, Visual Basic defines a delegate for the event behind the scenes based on the signature of the event. The Walked event, for instance, has a delegate like the following:

  Public Delegate Sub WalkedEventHandler(ByVal distance As Integer) 

Notice how this code declares a “method” that accepts an Integer and has no return value. This is exactly what you defined for the event. Normally, you don’t write this bit of code, because Visual Basic does it automatically. However, if you are to declare a custom event, then you need to manually declare the event delegate.

You also need to declare within the class a variable where you can keep track of any code that is listening for, or handling, the event. It turns out that you can tap into the prebuilt functionality of delegates for this purpose. By declaring the WalkedEventHandler delegate, you have defined a datatype that automatically tracks event handlers, so you can declare the variable like this:

  Private mWalkedHandlers As WalkedEventHandler 

Then you can use this variable to store and raise the event within the custom event declaration:

  Public Custom Event Walked As WalkedEventHandler   AddHandler(ByVal value As WalkedEventHandler)     mWalkedHandlers = _       CType([Delegate].Combine(mWalkedHandlers, value), WalkedEventHandler)   End AddHandler   RemoveHandler(ByVal value As WalkedEventHandler)     mWalkedHandlers = _       CType([Delegate].Remove(mWalkedHandlers, value), WalkedEventHandler)   End RemoveHandler   RaiseEvent(ByVal distance As Integer)     If mWalkedHandlers IsNot Nothing Then       mWalkedHandlers.Invoke(distance)     End If   End RaiseEvent End Event 

In this case, you’ve used the Custom Event key phrase, rather than just Event to declare the event. A Custom Event declaration is a block structure with three subblocks: AddHandler, RemoveHandler, and RaiseEvent.

TheAddHandlerblock is called anytime a new handler wants to receive the event. The parameter passed to this block is a reference to the method that will be handling the event. It is up to you to store the reference to that method, which you can do however you choose. In this implementation, you’re storing it within the delegate variable just like the default implementation provided by Visual Basic.

The RemoveHandler block is called anytime a handler wants to stop receiving your event. The parameter passed to this block is a reference to the method that was handling the event. It is up to you to remove the reference to the method, which you can do however you choose. In this implementation, you’re replicating the default behavior by having the delegate variable remove the element.

Finally, the RaiseEvent block is called anytime the event is raised. Typically, it is invoked when code within the class uses the RaiseEvent statement. The parameters passed to this block must match the parameters declared by the delegate for the event. It is up to you to go through the list of methods that are handling the event and to call each of those methods. In the example shown here, you’re allowing the delegate variable to do that for you, which is the same behavior you get by default with a normal event.

The value of this syntax is that you could opt to store the list of handler methods in a different type of data structure, such as a Hashtable or collection. You could then invoke them asynchronously, or in a specific order or based on some other behavior required by the application.

Receiving Events with WithEvents

Now that you’ve implemented an event within the Person class, you can write client code to declare an object using the WithEvents keyword. For instance, in the project’s Form1 code module, you can write the following code:

 Public Class Form1   Inherits System.Windows.Forms.Form   Private WithEvents mPerson As Person 

By declaring the variable WithEvents, you are indicating that you want to receive any events raised by this object. You can also choose to declare the variable without the WithEvents keyword, although in that case you would not receive events from the object as described here. Instead, you would use the AddHandler method, which is discussed after WithEvents.

You can then create an instance of the object, as the form is created, by adding the following code:

  Private Sub Form1_Load(ByVal sender As System.Object, _     ByVal e As System.EventArgs) Handles MyBase.Load   mPerson = New Person() End Sub 

At this point, you’ve declared the object variable using WithEvents and have created an instance of the Person class, so you actually have an object with which to work. You can now proceed to write a method to handle the Walked event from the object by adding the following code to the form. You can name this method anything you like; it is the Handles clause that is important because it links the event from the object directly to this method, so it is invoked when the event is raised:

  Private Sub OnWalk(ByVal distance As Integer) Handles mPerson.Walked   MsgBox("Person walked " & distance) End Sub

You’re using the Handles keyword to indicate which event should be handled by this method. You’re also receiving an Integer parameter. If the parameter list of the method doesn’t match the list for the event, then you’ll get a compiler error indicating the mismatch.

Finally, you need to call the Walk() method on the Person object. Add a button to the form and write the following code for its Click event:

  Private Sub Button1_Click(ByVal sender As System.Object, _     ByVal e As System.EventArgs) Handles button1.Click   mPerson.Walk(42) End Sub 

When the button is clicked, you simply call the Walk method, passing an Integer value. This causes the code in your class to be run, including the RaiseEvent statement. The result is an event firing back into the form, because you declared the mPerson variable using the WithEvents keyword. The OnWalk() method will be run to handle the event, as it has the Handles clause linking it to the event.

The diagram in Figure 3-4 illustrates the flow of control, showing how the code in the button’s Click event calls the Walk() method, causing it to add to the total distance walked and then to raise its event. The RaiseEvent causes the OnWalk() method in the form to be invoked; and once it is done, control returns to the Walk() method in the object. Because you have no code in the Walk method after you call RaiseEvent, the control returns to the Click event back in the form, and then you’re done.

image from book
Figure 3-4

Tip 

Many people have the misconception that events use multiple threads to do their work. This is not the case. Only one thread is involved in this process. Raising an event is much like making a method call, in that the existing thread is used to run the code in the event handler. This means that the application’s processing is suspended until the event processing is complete.

Receiving Events with AddHandler

Now that you’ve seen how to receive and handle events using the WithEvents and Handles keywords, let’s take a look at an alternative approach. You can use the AddHandler method to dynamically add event handlers through your code, and RemoveHandler to dynamically remove them.

WithEvents and the Handles clause require that you declare both the object variable and event handler as you build the code, effectively creating a linkage that is compiled right into the code. AddHandler, on the other hand, creates this linkage at runtime, which can provide you with more flexibility. However, before getting too deeply into that, let’s see how AddHandler works.

In Form1, you can change the way the code interacts with the Person object, first by eliminating the WithEvents keyword, as shown here:

  Private mPerson As Person  

And then by also eliminating the Handles clause:

  Private Sub OnWalk(ByVal distance As Integer)   MsgBox("Person walked " & distance) End Sub 

With these changes, you’ve eliminated all event handling for the object, and the form will no longer receive the event, even though the Person object raises it.

Now you can change the code to dynamically add an event handler at runtime by using the AddHandler method. This method simply links an object’s event to a method that should be called to handle that event. Anytime after you’ve created the object, you can call AddHandler to set up the linkage:

 Private Sub Form1_Load(ByVal sender As System.Object, _     ByVal e As System.EventArgs) Handles MyBase.Load    mPerson = New Person()    AddHandler mPerson.Walked, AddressOf OnWalk  End Sub 

This single line of code does the same thing as the earlier use of WithEvents and the Handles clause, causing the OnWalk() method to be invoked when the Walked event is raised from the Person object.

However, this linkage is performed at runtime, so you have more control over the process than you would have otherwise. For instance, you could have extra code to decide which event handler to link up. Suppose that you have another possible method to handle the event in the case that a message box is not desirable. Add this code to Form1:

  Private Sub LogOnWalk(ByVal distance As Integer)   System.Diagnostics.Debug.WriteLine("Person walked " & distance) End Sub 

Rather than pop up a message box, this version of the handler logs the event to the output window in the IDE.

Now you can enhance the AddHandler code to determine which handler should be used dynamically at runtime:

 Private Sub Form1_Load(ByVal sender As System.Object, _     ByVal e As System.EventArgs) Handles MyBase.Load        mPerson = New Person()   If Microsoft.VisualBasic.Command = "nodisplay" Then     AddHandler mPerson.Walked, AddressOf LogOnWalk   Else     AddHandler mPerson.Walked, AddressOf OnWalk   End If End Sub 

If the word nodisplay is on the command line when the application is run, then the new version of the event handler is used; otherwise, you continue to use the message box handler.

The counterpart to AddHandler is RemoveHandler. RemoveHandler is used to detach an event handler from an event. One example of when this is useful is if you ever want to set the mPerson variable to Nothing or to a new Person object. The existing Person object has its events attached to handlers, and before you get rid of the reference to the object, you must release those references:

  If Microsoft.VisualBasic.Command = "nodisplay" Then   RemoveHandler mPerson.Walked, AddressOf LogOnWalk Else   RemoveHandler mPerson.Walked, AddressOf OnWalk End If mPerson = New Person 

If you don’t detach the event handlers, the old Person object remains in memory because each event handler still maintains a reference to the object even after mPerson no longer points to the object.

This illustrates one key reason why the WithEvents keyword and Handles clause are preferable in most cases. AddHandler and RemoveHandler must be used in pairs, and failure to do so can cause memory leaks in the application, whereas the WithEvents keyword handles these details for you automatically.

Constructor Methods

In Visual Basic, classes can implement a special method that is always invoked as an object is being created. This method is called the constructor, and it is always named New().

The constructor method is an ideal location for such initialization code, as it is always run before any other methods are ever invoked, and it is only run once for an object. Of course, you can create many objects based on a class, and the constructor method will be run for each object that is created.

You can implement a constructor in your classes as well, using it to initialize objects as needed. This is as easy as implementing a Public method named New. Add the following code to the Person class:

  Public Sub New()   Phone("home") = "555-1234"   Phone("work") = "555-5678" End Sub 

In this example, you’re simply using the constructor method to initialize the home and work phone numbers for any new Person object that is created.

Parameterized Constructors

You can also use constructors to allow parameters to be passed to the object as it is being created. This is done by simply adding parameters to the New() method. For example, you can change the Person class as follows:

 Public Sub New(ByVal name As String, ByVal birthDate As Date)   mName = name   mBirthDate = birthDate   Phone("home") = "555-1234"   Phone("work") = "555-5678" End Sub

With this change, anytime a Person object is created, you’ll be provided with values for both the name and birth date. However, this changes how you can create a new Person object. Where you used to have code such as

 Dim myPerson As New Person()

now you will have code such as

 Dim myPerson As New Person("Peter", "1/1/1960")

In fact, because the constructor expects these values, they are mandatory - any code that needs to create an instance of the Person class must provide these values. Fortunately, there are alternatives in the form of optional parameters and method overloading (which enables you to create multiple versions of the same method, each accepting a different parameter list). These topics are discussed later in the chapter.

Termination and Cleanup

In the .NET environment, an object is destroyed and the memory and resources it consumes are reclaimed when there are no references remaining for the object. As discussed earlier in the chapter, when you are using objects, the variables actually hold a reference or pointer to the object itself. If you have code such as

 Dim myPerson As New Person()

you know that the myPerson variable is just a reference to the Person object you created. If you also have code like

 Dim anotherPerson As Person anotherPerson = myPerson

you know that the anotherPerson variable is also a reference to the same object. This means that this specific Person object is being referenced by two variables.

When there are no variables left to reference an object, it can be terminated by the .NET runtime environment. In particular, it is terminated and reclaimed by a mechanism called garbage collection, or the garbage collector, covered in detail in Chapter 5.

Tip 

Unlike COM (and thus VB6), the .NET runtime does not use reference counting to determine when an object should be terminated. Instead, it uses a scheme known as garbage collection to terminate objects. This means that, in Visual Basic, you do not have deterministic finalization, so it is not possible to predict exactly when an object will be destroyed.

Let’s review how you can eliminate references to an object. You can explicitly remove a reference by setting the variable equal to Nothing, with the following code:

 myPerson = Nothing

You can also remove a reference to an object by changing the variable to reference a different object. Because a variable can only point to one object at a time, it follows naturally that changing a variable to point at another object must cause it to no longer point to the first one. This means that you can have code such as the following:

 myPerson = New Person()

This causes the variable to point to a brand-new object, thus releasing this reference to the prior object. These are examples of explicit dereferencing.

Visual Basic also provides facilities for implicit dereferencing of objects when a variable goes out of scope. For instance, if you have a variable declared within a method, then when that method completes, the variable is automatically destroyed, thus dereferencing any object to which it may have pointed. In fact, anytime a variable referencing an object goes out of scope, the reference to that object is automatically eliminated. This is illustrated by the following code:

 Private Sub DoSomething()   Dim myPerson As Person   myPerson = New Person() End Sub

Even though the preceding doesn’t explicitly set the value of myPerson to Nothing, you know that the myPerson variable will be destroyed when the method is complete because it will fall out of scope. This process implicitly removes the reference to the Person object created within the routine.

Of course, another scenario in which objects become dereferenced is when the application itself completes and is terminated. At that point, all variables are destroyed, so, by definition, all object references go away as well.




Professional VB 2005 with. NET 3. 0
Professional VB 2005 with .NET 3.0 (Programmer to Programmer)
ISBN: 0470124709
EAN: 2147483647
Year: 2004
Pages: 267

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net