Creating Members
In this section, we'll explore the VB.NET syntax for creating methods, properties, fields, events, and delegates, as well as handling constants.
Creating Methods
As in previous versions of VB, you can create both methods that return a value, using the Function statement, and methods that do not, using the Sub statement. Both types can accept arguments passed by value (ByVal, now the default), by reference (ByRef), and an optional array of objects (ParamArray) as arguments.
TIP
Parameter arrays have always been an underutilized feature of VB. However, they can be powerful when you want to pass an undefined number of objects into a method. The reason they are underutilized is that the calling code must explicitly pass the parameters in a comma-delimited list rather than simply as an array. In most cases where it makes sense to use a ParamArray, the calling code does not know at design-time how many objects it will pass, and so simply defining the arguments using an array of objects typically makes more sense. VB.NET's support for overloaded methods also serves to lessen the need for ParamArrays because you can create specific method signatures for each set of arguments.
As in VB 6.0, if the Optional keyword is used with any of the arguments, all subsequent arguments must be declared as optional. However, VB.NET requires a default value to be specified in the argument list for any optional parameters. Alternatively, as will be discussed later, the same method can be declared more than once inside a class to create an overloaded method from which the client can choose during development.
To return a value from a class, VB.NET now supports the use of the Return statement as well as assigning a value to the function name, shown as follows:
Public Function ListCourses(Optional ByVal pVendor As Long = -1) As DataSet Dim myDS As DataSet ' Code to access the database and populate the dataset Return myDS End Function
The method declaration can also include an expanded set of modifiers including Public, Private, Friend, Protected, Protected Friend, and Shared. As you would expect, the first five modifiers in that list serve the same functions as in class modules to restrict access to the method in both derived classes and across projects. The Shared keyword, however, is unique to members and is used to implement static members across all instances of a class, as will be discussed shortly.
Creating Properties
One of the syntactical changes in VB.NET is the consolidation of property declarations into the Property keyword. Basically the Property keyword includes an access modifier, the name of the property, and an optional parameters list in addition to the code to optionally both get and set the value of the property. As in VB 6.0, properties are useful because you can set the value the client is attempting to set the property to and raise exceptions if necessary.
The basics of the Property statement can be seen in the following example, where a LastName property is implemented for the Instructor class:
Private mstrLName As String Property LastName() As String Get Return mstrLName End Get Set(ByVal Value As String) mstrLName = Value End Set End Property
Notice that the code to retrieve the value and to store the value is placed in blocks within the Property statement and that the Value argument is used to catch the incoming value. As noted in Chapter 3, "VB.NET Language Features," any variables declared within either the Get or Set blocks are scoped within the block and are accessible only within the block.
The access modifiers for properties include ReadOnly, WriteOnly, and Default. As the names imply, the ReadOnly and WriteOnly modifiers are used to limit the access to the property. When specified, either the Get or Set block must be omitted from the Property statement as appropriate.
VB.NET also supports parameterized properties by including a parameter list in the declaration of the property. Typically you would use this construct when the property returns an individual array or object instance stored in the class. For example, assume that a class called Vendors is used to store a collection of Vendor objects defined in the Vendor class. To make development simpler, the Vendors class is derived from the System.Collections.CollectionBase class (as will be discussed later in the chapter) that provides all the low-level handling of collections. To provide access to a particular Vendor in the Vendors collection, you could create the read-only property shown in Listing 4.1.
Listing 4.1 Default property. This listing shows how to implement a default property for a class.
Public Class Vendors Inherits CollectionBase ' other properties and methods Default ReadOnly Property Item(ByVal index As Integer) As Vendor Get Return CType(Me.InnerList.Item(index), Vendor) End Get End Property Public Sub Add(ByVal obj As Vendor) Me.InnerList.Add(obj) End Sub End Class
When the Vendor class is instantiated by a client, the client can then add vendors using the public Add method and retrieve one by passing an index value to the Item property, as shown in the following example:
Dim colVendors As New Vendors Dim objVen As New Vendor Dim objVen2 As Vendor colVendors.Add(objVen) ' populate or retrieve the collection objVen2 = colVendors.Item(4) ' Get 5th item in the collection
In the preceding example, you'll also notice that the property is marked as Default. Only properties that are parameterized can be marked as default. This allows a client to use the shortcut syntax
objVen2 = colVendors(4) ' Get 5th item in the collection
rather than accessing the Item property directly. Of course, only one property can be marked as the default within a class.
You can also create collection classes by deriving from the Collection class in the Microsoft.VisualBasic namespace. The difference is that the Collection class contains more default behavior such as the inclusion of the Item and Add methods, whereas CollectionBase contains methods that allow you to provide custom behavior as items are added to and removed from the collection.
NOTE
The Services Framework uses parameterized properties in many scenarios, particularly those where an object hierarchy has been implemented (such as the ADO.NET SqlParameterCollection class).
Like methods, properties can be modified with the Shared, Private, Overridable, Overrides, and Overloads keywords as discussed later. The only point to remember is that if a property is marked as ReadOnly, WriteOnly, or Default, the overriding method in the derived class must also be marked as such.
Creating Fields
Previous versions of VB also had the ability to create fields. Fields are simply variables declared with the Public or Public Dim keyword at the class level. Variables declared with Private or simply Dim are not exposed as fields. They are available to any code within the class and can be accessed by clients of the class in the same fashion as properties. The advantage to using fields is that they require very little syntax and are simple. In addition, fields can also be marked with the ReadOnly keyword so that any code outside the class constructor cannot change the value. The ReadOnly field Count in the following code snippet is public so that it is accessible by code both inside and outside the class, but it can be changed only within the constructor (New) method of the class:
Public Class Instructors Public ReadOnly Count As Integer Public Sub New() Count = 50 ' Legal End Sub Public Sub OtherProc() Count = 2 ' Generates a compile time error End Sub End Class
The primary disadvantage to using fields is that access to them cannot be intercepted by custom code as is the case with properties.
Creating Events and Delegates
The basic event handling syntax has not changed significantly from previous versions of VB, although it has been extended (as you'll see shortly). As in previous versions, events can be declared within a class using the Event keyword. An event can be Public, Private, Protected, Friend, and Protected Friend to the class, can pass arguments either ByVal or ByRef, and cannot return values. Events can then be raised using the RaiseEvent keyword from within the class and passing the required arguments. A partial sample from a class called RegistrationWatch is shown in Listing 4.2.
Listing 4.2 Simple events. This class implements the simple event NewRegistrations using the Event keyword and fires it using RaiseEvent.
Public Class RegistrationWatch Public Event NewRegistrations(ByVal pStudents As DataSet) ' Other methods and properties Public Sub Look() Dim dsStuds As DataSet Dim flNew As Boolean ' Method that fires on a timer to look for new registrations ' since the last invocation of the method ' If one is found then create a DataSet with the new students ' and raise the event flNew = True dsStuds = New DataSet() If flNew Then RaiseEvent NewRegistrations(dsStuds) End If End Sub End Class
Notice that the class defines a NewRegistrations event as Public so that consumers of the class will be able to catch it. The event passes back to the consumer a variable containing an ADO.NET DataSet that stores information on the new registrations found. The event is raised in the Look method using the RaiseEvent statement.
To catch the event, a consumer can declare the RegistrationWatcher class using the WithEvents keyword (termed declarative event handling). Note that, as in VB 6.0, variables declared using WithEvents can be either Public or Private. However, in VB.NET, WithEvents can be declared at the module or class level, rather than only in classes and forms as in VB 6.0. The syntax for using WithEvents is as follows:
Private WithEvents mobjReg As RegistrationWatch
To set up the event handler (or event sink), you can then create a procedure that handles the event within the class or module, as shown in the following example:
Public Sub RegFound(ByVal pStudents As System.Data.DataSet) _ Handles mobjReg.NewRegistrations MsgBox("New Students Found!") End Sub
Note that VB.NET uses the new Handles keyword to indicate precisely which event the procedure handles rather than simply relying on the naming convention used by the event handler as in previous versions. Declarative event handling is certainly the most convenient way to handle events, although as mentioned, it requires the object variable to be scoped correctly and cannot be used to dynamically turn an event handler on or off.
TIP
Although it is true that declarative event handling does not allow you to turn events on and off, if the class raising the event is a custom class, you can implement your own EnableRaisingEvents property. The client can then use this property to stop the raising of events. You would then wrap your RaiseEvent statements in a check of this property.
When dealing with inheritance, keep in mind that if a class acting as a consumer does not implement the event handlers for a particular object, classes derived from it will not be able to implement them later. In addition, as with other methods, the event handlers can be specified with the Overridable, NotOverridable, and MustOverride keywords.
Events can also be marked as Shared, which allows all consumers of a class to receive an event notification when the RaiseEvent method is executed in any instance of the class. This might be useful for notifying several consumers when shared data changes, for example, in a service application that periodically queries a database and exposes the data through shared properties.
Dynamic Event Handling
VB.NET expands the ability to use events by implementing dynamic event handling in addition to the declarative approach discussed earlier. Dynamic event handling can be very useful when you want to turn event handling on or off (called hooking and unhooking an event) at a specific time or when the object variable that you want to hook does not reside at the module or class level. To hook and unhook events at run-time, use the AddHandler and RemoveHandler statements.
As an example, consider the RegistrationWatch class shown in Listing 4.3. In this example, we want to create client code in a class that hooks and unhooks the NewRegistrations event based on the setting of the public property. The client class that does this is shown in Listing 4.3.
Listing 4.3 Dynamic events. This class hooks and unhooks events dynamically using the AddHandler and RemoveHandler statements.
Public Class Registrations Private mRec As Boolean = False Private mRegWatch As RegistrationWatch Public Property ReceiveNotifications() As Boolean Get Return mRec End Get Set mRec = Value If mRec = True Then ' Add the event handler AddHandler mRegWatch.NewRegistrations, _ AddressOf Me.NewRegistrations Else ' Remove the event handler RemoveHandler mRegWatch.NewRegistrations, _ AddressOf Me.NewRegistrations End If End Set End Property Public Sub NewRegistrations(ByVal ds As DataSet) ' New Registrations found MsgBox("New registrations have been added!") End Sub Public Sub New() ' Instantiate the class level object mRegWatch = New RegistrationWatch() End Sub Public Sub TestNotify() ' Test method to simulate repeated queries for new registrations mRegWatch.Look() End Sub End Class
In Listing 4.3, the property ReceiveNotifications is used to determine whether the class is to receive notifications. The Set block of the property then reads the value and calls the AddHandler and RemoveHandler statements accordingly. Both of the statements accept two parameters:
A reference to the event to be hooked (in this case, the NewRegistrations event of the module level mRegWatch variable)
The address of the method to use as the event handler
Note that the AddressOf and Me keywords are used to create a pointer to the method and specify a method internal to the class, respectively.
From the client's perspective, the code differs only in that the ReceiveNotification property can be set to True when notifications are desired (because the mRec class level variable defaults to False), as shown in the following example:
Dim objReg As New Registrations() objReg.TestNotify() ' No notification objReg.ReceiveNotifications = True objReg.TestNotify() ' Notification received
Mapping Events to Delegates
As mentioned in Chapter 1, the infrastructure for events is based on the concept of delegates, and therefore, it is not surprising that the event keywords (such as Event, RaiseEvent, AddHandler, and RemoveHandler) simply abstract the creation and processing of delegates. However, VB.NET developers can also access delegates directly through the Delegate keyword. To understand delegates, let's first explore how delegates are used to implement events in VB.NET.
Remember first and foremost that delegates are simply types that hold references to functions in other objects (type-safe function pointers). There are two basic types of delegates: single-cast and multi-cast. The former allows a single function pointer to be stored and the latter creates a linked-list of pointers (events are implemented as multi-cast delegates). In addition to the function pointer, the delegate stores the arguments that the function will accept. As a result, from the developer's view, the delegate can be thought of simply as a method signature.
The basic idea behind using a delegate is that a program (called A for this example) creates a delegate that points to one of its own functions and then passes the delegate to some other program (B). At some time in the future, B executes A's function, making sure to push the appropriate arguments on the stack, by running the code at the address provided by the delegate. Of course, this is exactly the model used when dealing with events.
In the example in Listing 4.3, what happens when the Event keyword and RaiseEvent statements are added to the class is this. Behind the scenes, the VB compiler creates a Delegate with the same signature as the event and stores it in a field of the class as follows:
Delegate Sub NewRegistrations(ByVal pStudents As DataSet)
The compiler also creates add and remove methods for the delegate that take as an argument a reference to a delegate defined (in this case, NewRegistration). In addition, the RaiseEvent is replaced with a call to the delegate's Invoke method, which accepts the single argument as defined in the Delegate.
The consumer then uses the WithEvents keyword and implements the event handler with the Handles statement. At run-time, the delegate is instantiated (it is actually a class derived from System.Delegate) and a reference to the event handler is passed to the add method in RegistrationWatch. When the delegate is invoked, the function pointer to the event handler is used to call the method. This simple mapping of delegates to events should also explain why it is easy for VB.NET to support the AddHandler and RemoveHandler statements. They simply call the add and remove methods implemented by the compiler at specified times rather than upon instantiation and deallocation.
To make this a little clearer, examine the code in Listing 4.4 that shows the RegistrationWatcher class rewritten with a delegate in place of the event.
Listing 4.4 Simple delegate. This class uses a delegate in place of an event to perform simple notification.
Public Class RegistrationWatch Delegate Sub NewRegistrations(ByVal pStudents As DataSet) ' Other methods and properties Private mfound as NewRegistrations Public Sub RegisterClient(ByVal found As NewRegistrations) Mfound = found End Sub Public Sub Look() Dim dsStuds As DataSet Dim flNew As Boolean ' Method that fires on a timer to look for new registration ' If one is found then create a DataSet with the new students ' and raise the event flNew = True dsStuds = New DataSet() If flNew Then mfound.Invoke(dsStuds) 'invoke the delegate End If End Sub End Class
The differences between Listing 4.4 and Listing 4.3 can be summarized as follows:
The Event statement has been replaced with Delegate.
The RaiseEvent statement has been replaced with Delegate.Invoke.
The RegisterClient method is now used to pass in the reference to the delegate stored in a private class variable, whereas the Look method simply invokes the delegate at the appropriate time.
Notice also you don't have to explicitly create the add and remove methods, even when specifying the delegate yourself; the VB.NET compiler will add these automatically.
The client code also changes in order to instantiate the delegate as it is being passed to the Look method. Note that the address of the event handler is passed as the only argument in the constructor of the delegate, as shown in the following example:
Private mobjReg As RegistrationWatch ' in a class or module mobjReg = New RegistrationWatch() mobjReg.RegisterClient(New RegistrationWatch.NewRegistrations( _ AddressOf NewRegistrations)) mobjReg.Look() Private Sub NewRegistrations(ByVal ds As DataSet) End Sub
Events can also work with delegates as a kind of shortcut for declaring the event signature. For example, rather than declaring an event as
Event NewRegistrations(ByVal ds As DataSet)
you could make the declarations
Delegate Sub NewRegistrations(ByVal ds As DataSet) Event NewReg as NewRegistrations
This allows you to reuse the definition of the delegate inside the event. This can be useful when you have many events that require the same arguments.
Obviously, using delegates in place of events might be construed as overkill because events work quite nicely for scenarios where an event model is required. However, because delegates are function pointers, they also provide other capabilities of which you can take advantage, such as function substitution and asynchronous operation.
Function Substitution with Delegates
The idea of function substitution is exactly as it sounds: A section of client code can call one of several functions depending on the delegate it is passed. This promotes the idea of polymorphism because it allows you to write code that is generic as it doesn't know at compile-time which function it is going to call. As an example, consider the Registration class shown in Figure 4.1. Suppose that it contains a RegisterStudent method that implements the business rules necessary to register a student to take a course. As a small part of that process, the cost of the course must be calculated. However, the algorithm for calculating the cost varies depending on how the student was registered and could include a phone call, Web, and registrations received directly from a vendor or partner through a Web service.
To implement this requirement, the Registration class could declare a delegate called CalcCourseCost as follows:
Delegate Function CalcCourseCost(ByVal CourseID As String) As Decimal
Note that delegates can also return values and therefore be declared as a function. The RegisterStudent method is shown in Listing 4.5.
Listing 4.5 Skeleton code for the RegisterStudent method. This method uses the delegate passed in as the first parameter to invoke the appropriate calculation function.
Public Sub RegisterStudent(ByVal pCalc As CalcCourseCost, _ ByVal pStud As DataSet) ' Implement the business process for registering the student ' 1. make sure the student has provided enough info ' 2. make sure the student has not been blacklisted ' 3. possibly suggest another class that is closer based on ' geographic data: raise exception ' 4. make sure the class is not full ' 5. calculate the cost Dim curCost As Decimal Dim strCourseID As String curCost = pCalc(strCourseID) ' 6. persist the registration (database or queue) ' 7. notify the appropriate internal staff of a new registration ' 8. email verification to student End Sub
The method takes both the delegate and the student information packed in a DataSet as parameters. After completing the preliminary business rules, the course cost can be calculated simply by invoking the delegate pCalc and passing the required CourseID argument. Note that this example illustrates that the Invoke method of the delegate is actually the default method of a delegate object. The invocation of the calculation function could also be written as pCalc.Invoke(strCourseID).
On the client side, the appropriate delegate must be instantiated. The skeleton code in Listing 4.6 shows code residing in a module or class that first collects the registration method (stored in RegType) and uses a Select Case statement to instantiate the correct delegate. The delegate is then passed to the RegisterStudent method.
Listing 4.6 Client code using a delegate. This code determines the appropriate delegate at run-time and passes it to the RegisterStudent method. Note that the procedures used as the delegates are shown later.
Dim objReg As New Registrations() Dim delCalc As Registrations.CalcCourseCost Dim RegType As Integer Dim ds As DataSet ' Determine the registration type ' Create the delegate based on the registration type Select Case RegType Case 1 delCalc = New Registrations.CalcCourseCost(AddressOf CalcWeb) Case 2 delCalc = New Registrations.CalcCourseCost(AddressOf CalcPhone) Case 3 delCalc = New Registrations.CalcCourseCost(AddressOf CalcService) End Select ' Register the student objReg.RegisterStudent(delCalc, ds) ' Other code goes here Public Function CalcWeb(ByVal strCourseID As String) As Decimal ' Calculate cost based on web registration End Function Public Function CalcPhone(ByVal strCourseID As String) As Decimal ' Calculate cost based on phone registration End Function Public Function CalcService(ByVal strCourseID As String) As Decimal ' Calculate cost based on service registration End Function
NOTE
Experienced VB developers might have noticed that this technique is similar to using the CallByName function in VB 6.0. The difference is that CallByName could be used only on internal classes or COM objects, whereas delegates can be used with any procedure (in a module or a class) that fits the signature of the delegate.
What About Interfaces?
Developers who have done interface-based programming in VB 6.0 will note that the polymorphism shown in the CalcCourseCost example could also be implemented simply by creating separate classes for each calculation method and implementing a common interface (ICalc) that exposes a Calculate method. Different classes that implement the ICalc interface could then be passed to RegisterStudent at run-time to call the Calculate method. Although this technique would also work, it might be more complicated and less efficient. For example, by using a delegate, you don't have to create separate classes because any number of functions (as long as they have the same signature) in the calling code can be used to implement the delegate, and you don't have to pass a full object reference to RegisterStudent, only a delegate. As a rule of thumb, create interfaces when there is a collection of related members that you want to implement across classes and when the particular function will be implemented only once. Use delegates when you have only a single function to implement, the class implementing the delegate doesn't require a reference to the object, or you want to create a delegate for a shared method (all methods defined for the interface are always instance methods).
As mentioned in the "What About Interfaces?" sidebar, using delegates for function substitution can also take the place of using interfaces (discussed later in the chapter). This pattern can be approximated by instantiating a delegate inside a class and then using a private function to invoke the delegate. For example, consider the case in which disparate classes each support the ability to persist their state. In this case, client applications could work with each of these classes polymorphically through the use of a delegate rather than requiring them to implement the same interface or belong to the same inheritance hierarchy. The pattern for such a class is shown in Listing 4.7.
Listing 4.7 Delegates as interfaces. This class shows how a delegate can be used to expose functionality to client applications polymorphically. It is functionally equivalent to using an interface.
Delegate Function Persist() As String Public Class Registrations Public ReadOnly myPersist As Persist Public Sub New() myPersist = New Persist(AddressOf Me.SaveToDisk) End Sub Private Function SaveToDisk() As String Dim strFile as String ' save the contents to disk and return the path name Return strFile End Function End Class
Note that the delegate is declared external to the class and that the class then instantiates a read-only variable in the constructor, passing it the address of the private SaveToDisk method (which will actually save the results and return the file name). The client application can then call any class that supports the Persist delegate by implementing a function that accepts the delegate as parameter, as shown here:
Public Function Save(ByVal pPersist As Persist) As String Return pPersist.Invoke End Function
The client then invokes the function to save the contents to a file by passing the delegate of the Registrations class to the Save function, as shown here:
Dim objRegister As New Registrations Dim strPath As String ' ...work with the class strPath = Save(objRegister.myPersist)
In this way, each class can decide internally how to implement the code to save its contents and yet provide a public interface through the Persist delegate that client applications can call. Even though this code is slightly more complex from the client application's perspective, it is more flexible because interfaces and inheritance are not required.
Asynchronous Processing with Delegates
A second major use of delegates in .NET is for handling asynchronous processing. One of the benefits of using delegates for asynchronous processing is that they can be used both in conjunction with .NET Remoting (discussed in Chapter 8, "Building Components,") to facilitate communication between processes and machines and with code that resides in the same application domain. This common programming model allows you to write code that is location transparent as well.
Delegates are appropriate for asynchronous calls because they were designed with BeginInvoke and EndInvoke methods that allow the client to invoke a method asynchronously and then catch its results respectively. You can think of BeginInvoke and EndInvoke as partitioning the execution of a method from a client's perspective. To specify which procedure to call upon completion of the asynchronous method, you use the AsyncCallback object and IAsyncResult interface from the System namespace. When using delegates with the asynchronous classes and interfaces in the System namespace, the CLR provides the underlying services needed to support this programming model including a thread pool and the ability to call objects that reside inside synchronized contexts such as those using COM+ services. (We'll explore more about thread support in the CLR in Chapter 12, "Accessing System Services," as well.) One of the side benefits of this integration is to make it fairly simple to create multi-threaded applications in VB.NET.
NOTE
VB 6.0 developers will recall that because the VB 6.0 runtime was single-threaded, it was very difficult to create any asynchronous code without resorting to using the Win32 API directly (which led to unstable code that was tricky to debug) or tricking the COM library into creating an object in a new single-threaded apartment. Needless to say, neither technique was particularly attractive.
To understand the pattern for asynchronous programming in .NET, consider a method of the Registration class that is used to send reminder e-mail notifications to all students who are to attend a class the following week. This RemindStudent method might take some time to complete because it must read a list of students from the database, construct an e-mail message for each one, send the e-mail, and update the database appropriately. For the client application's thread to avoid being blocked when the method is called, the method can be called using a delegate (in this case often termed an asynchronous delegate).
Because the .NET asynchronous model allows the client to decide whether the called code will run asynchronously, the code for the RemindStudent method does not have to be written explicitly to handle asynchronous calls. You can simply declare it and write the code as if it were synchronous as follows:
Public Function RemindStudent(ByVal pDaysAhead As Integer, _ ByRef pErrors() as String) As Boolean ' Read from the database and send out the email notification End Sub
Note that the method requires an argument to specify how many days in advance the e-mail should be sent out and returns an array of strings containing any error messages. However, the client code must significantly change. The code in Listing 4.8 is used to call the RemindStudent method.
Listing 4.8 Code used to implement asynchronous programming using delegates.
' Async delegate Delegate Function RemStudentCallback(ByVal pDaysAhead As Integer, _ ByRef pErrors() as String) As Boolean ' Client code inside a class or module ' Declare variables Dim temp() As String Dim intDays As Integer = 7 ' Create the Registration class and the delegate Dim objReg As Registration = New Registration() Dim StudCb As RemStudentCallback = New RemStudentCallback(AddressOf _ objReg.RemindStudent) ' Define the AsyncCallback delegate Dim cb As AsyncCallback = New AsyncCallback(AddressOf RemindResult) ' Can create any object as the state object Dim state As Object = New Object() ' Asynchronously invoke the RemindStudent method on objReg StudCb.BeginInvoke(intDays, temp, cb, state) ' Do some other useful work Public Sub RemindResult(ByVal ar As IAsyncResult) Dim strErrors() As String Dim StudCb As RemStudentCallback Dim flResult As Boolean Dim objResult As AsyncResult ' Extract the delegate from the AsyncResult objResult = CType(ar, AsyncResult) StudCb = objResult.AsyncDelegate ' Obtain the result flResult = StudCb.EndInvoke(strErrors, ar) ' Output results End Sub
To begin, notice that the client code first creates a new instance of the Registration class as normal. However, it then creates a new RemStudentCallback delegate and places in it the address of the RemindStudent method of the newly created object instance (objReg). Once again, the delegate must have the same signature as the RemindStudent method.
Now the code must create an address that can be called back upon completion of the RemindStudent method. To do this, a system delegate class called AsyncCallback is instantiated and passed the address of a procedure called RemindResult. You'll notice that RemindResult must accept one argument: an object that supports the IAsyncResult interface.
After the callback is set up, the RemindStudent method is actually invoked using the BeginInvoke method of the delegate that was created earlier (StudCb). The BeginInvoke method accepts the same arguments as the delegate it was instantiated as in addition to the delegate that contains the callback and an object in which you can place additional state information required by the method.
At this point, your code can continue on the current execution path. The delegate will invoke the RemindStudent method (on a separate thread if in the same application) and when finished will call the procedure defined in the delegate passed to BeginInvoke (in this case, the RemindResult). As mentioned previously, this procedure accepts an argument of type IAsyncResult. To retrieve the instance of the delegate RemStudentCallback from it, you first need to convert it to an object of type AsyncResult using the CType function. The AsyncDelegate property of the resulting AsyncResult object contains the reference to the RemStudentCallback delegate. The return value and any arguments passed ByRef are then captured using the return value and arguments passed to the EndInvoke method of the delegate.
Creating Named Constants
There are two ways to expose constants in a class in VB.NET: The first is to use the traditional Const keyword at the class level, and the second is to use an enumerated type.
To create a constant, simply declare it and assign it a value as follows:
Private Const QUILOGY_CODE As Byte = 1
Constants can be defined as any of the simple data types such as Byte, Boolean, Char, Short, Integer, Long, Single, Double, Decimal, Date, and String. As expected, constants cannot be the targets of assignment statements and must be initialized deterministically. In other words, the code used to initialize the constant must be able to be evaluated at compile-time using a constant expression that contains no variables or functions that return varying results.
TIP
If you would like to create a constant but need to initialize it using a non-deterministic algorithm, you should use a read-only field and initialize it in the class constructor. Read-only fields also have the advantage of being able to support additional types such as custom objects.
Finally, constants in a class are always explicitly shared members, as will be discussed later in the chapter. However, you can also modify them with the Public, Private, Protected, Friend, and Protected Friend keywords.
The second technique for creating constants in a class is to use an enumeration. As in VB 6.0, enumerations in VB.NET are declared with the Enum keyword and are used to provide a collection of constants that can be used in structures, variables, parameters, or even return values of procedures. The limitations of enumerated types include the following:
They have a restricted set of data types (Byte, Short, Integer, and Long, which is the default).
They must be initialized at compile-time under the same restrictions as constants. If you don't explicitly initialize them, they will be initialized to 0. VB.NET also supports the syntax where initializing the first constant in the enumeration and leaving the others uninitialized will result in the uninitialized constants being initialized in increments of 1 based on the first constant.
They are not inheritable.
The typical syntax for declaring an enum is as follows:
Public Enum VendorCodes As Byte vcQuilogy = 1 vcMicrosoft = 2 vcOracle = 3 vcRational = 4 vcSybase = 5 End Enum
Note that in this case, the Byte data type is used explicitly for the VendorCodes enumerated type rather than defaulting to a Long. In addition, this syntax implies that all constants within the type are of the same data type unlike a structure.