One of the most exciting features added to VBScript version 5 is the ability to write classes, bringing VBScript one step closer to the object-oriented paradigm, and giving VBScript programmers a powerful programming tool. Granted, classes defined in VBScript do not have the more properly object-oriented capability of Java, C++, or even Visual Basic classes, but they do let the VBScript programmer take advantage of a few of the benefits available when programming in these other languages.
The ability to create objects from classes defined in VBScript gives programmers increased power and flexibility for creating complex scripts, more reusable code, and even Script-based components . This chapter will explain in detail how to unlock these capabilities.
If you've skipped previous chapters and are not familiar with how to use COM objects from VBScript, then you might benefit from reading the first sections of Chapter 7 before tackling this chapter. This chapter will assume that you are familiar with the basics of instantiating objects and calling their properties and methods .
Before we get too far into how to write your own classes in VBScript, and where you can make use of them, we'll start by covering some terminology. Few technical terms in recent years have been misused, obscured, morphed, and confused more than class , object , and component . Often the terms are used interchangeably, even though they do have distinct meaning. This lack of clarity drives object-oriented purists crazy, but it also makes these waters difficult for beginners to navigate. Let's clear the fog a little bit.
In the strict sense, an object is an in-memory representation of a complex data and programming structure that can exist only while a program is running. A good analogy is an array, which is also a complex data structure that exists only at runtime. When in a programming context we refer to an array, it is clear to most people that we mean the in-memory data structure. Unfortunately, when someone uses the word object, it is not always clear that they are using the strict definition of the term , referring to a construct in memory at runtime.
An object is different from an array in several ways, but most importantly, an object not only stores data (in the form of properties ), but also has 'behavior'-that is, 'things it knows how do when asked'-which are exposed as methods . Properties can store any kind of data, and methods can be either procedures or functions. The idea of bringing the data and the behavior together in an object is that you can design your program such that the data that needs to be manipulated and the code that manipulates it can be close to each other.
A class is a template for an object. While an object exists only in memory at runtime, a class is a block of code that you work with at design time. A class is the code, and an object is the use of that code while a program is running. If you want to be able to use an object at runtime, you have to first define a class at design time. Objects are created at runtime based on templates provided by classes. For example, you could write a class called Customer . Once the class definition is saved, you could then use other code to create a thousand Customer objects in memory. This concept is illustrated in Figure 8-1.
Many people, however, use the terms class and object interchangeably, like so: 'I coded the Customer object, then I created a thousand Customer objects and sorted them by total sales.' As we mentioned, this can create some confusion for beginners, but over time you can learn to use the context to figure out what is meant .
A component is nothing more than a packaging mechanism, a way of compiling one or more related classes into a binary file that can be distributed to one or more computers. You can see that the class used to create the object in Figure 8-1 is stored inside of a component. In the Windows operating system, a component usually takes the form of a .DLL or .OCX file. The scripting runtime library that we introduced in Chapter 7 is a perfect example of a component.
When a programmer writes some classes that are related to each other in some way, and he or she wants people to be able to use those classes to create objects at runtime, she would probably package and distribute the classes as a component. A single program or script might make use of dozens of different classes from different components.
Components are not the only way to make use of classes, however. Figure 8-1 shows only one possible scenario (albeit a very common one). In a Visual Basic application, for example, you can write classes that are compiled within the application itself, and are never exposed to the outside world. The classes exist only inside that application, and the sole purpose is to serve the needs of that application. In that case, we would not consider those classes to be part of a component.
However, people are finding that it is often much more productive and forward thinking to package their classes into a component that can exist outside of the application. The thinking is that you might find a use for one or more of those classes later, and having them in a more portable component makes them much easier to reuse. In VBScript, you can use both techniques: you can create classes within a script that can only be used by that script (which we will cover in this chapter), or you can package your classes as a Windows Script Component (see Chapter 13).
The key to creating VBScript classes is the Class statement. Similar to the way the Function End Function or Sub End Sub statement pairs are used to block off the boundaries of a procedure, the Class and End Class statements are used to block off the boundaries of a class. You can use multiple blocks of Class End Class blocks in a single script file to define multiple classes (however, classes cannot be nested).
If you are coming to VBScript from another language, such as Visual Basic, you are probably accustomed to classes being stored in their own separate files. However, this is not the case with VBScript classes. In general, a VBScript class must be defined in the same script file as the script code that creates an instance of it.
This may seem like a pretty big limitation-since part of the purpose of creating a class is easy code portability and centralized reuse-but there are two other options. First, you can package one or more VBScript classes in a Windows Script Component, which we discuss in detail in Chapter 13. Second, you can use the Active Server Pages (ASP) # INCLUDE directive to include VBScript classes in your ASP scripts, which we discuss in Chapter 14. In this chapter, however, we are going to limit ourselves to the discussion of classes that are defined within the same script file as the code that makes use of the class.
Other than this same-script-file difference, Visual Basic programmers will not have any trouble adjusting to VBScript classes. Except for the differences between the Visual Basic and VBScript languages, the structure and techniques for VBScript classes are pretty much the same as for Visual Basic. Here is the fundamental syntax for the Class statement.
Class MyClass End Class
You would, of course, replace MyClass with the name of the class you are defining. This class name must be unique within the script file, as well as within any classes that are brought into the same scope through 'include directives.' The class name must also not be the same as any of the VBScript reserved words (such as Function or While ).
When a script creates an object based on a class, properties are the mechanisms through which data is stored and accessed. Through properties, data can be either stored in the object or retrieved from the object.
The best way to store the value of a property is in a private property variable. This is a variable that is defined at the class level (at the beginning of the class). This variable is private (that is, it is not directly accessible to code outside of the class) and holds the actual value of the property. Code that is using a class will use Property Let , Set , and Get procedures to interact with the property, but these procedures are merely gatekeepers for the private property variable.
You define a private property variable like so:
Class Customer Private mstrName End Class
In order for the variable to have private, class-level scope, it must be declared with the Private statement. The 'm' prefix is the 'Hungarian' notation to indicate that the scope of the variable is ' m odule level,' which is another way of saying 'class level.' Some texts will advocate the use of the 'c' prefix (as in cstrName ) to indicate c lass-level scope. However, we do not recommend this approach as it is easily confused with the prefix that Visual Basic programmers often use for the Currency data type.
A Property Let procedure is a special kind of procedure that allows code outside of a class to place a value in a private property variable. A Property Let procedure is similar to a VBScript Sub procedure in that it does not return a value. Here is the syntax.
Class Customer Private mstrName Public Property Let CustomerName(strName) mstrName = strName End Property End Class
Notice that instead of using the Sub or Function statements to define the procedure, Property Let is used. A Property Let procedure must accept at least one argument. Omitting this argument would defeat the whole purpose of the Property Let procedure, which is to allow outside code to store a value in the private property variable. Notice how the code inside the property procedure saves that strName value passed into the procedure in the private property variable mstrName . You are not required to have any code at all inside the procedure, but not storing the value passed into the procedure in some sort of class-level variable or object would tend to, once again, defeat the whole purpose of the Property Let procedure.
Conversely, you can have as much additional code in the procedure as you like. In some cases, you might wish to do some sort of validation before actually assigning the passed-in value in the private property variable. For example, if the length of the customer name value was not allowed to exceed 50 characters , you could verify that the strName argument value does not exceed 50 characters, and, if it did, use the Err.Raise method (see Chapter 6) to inform the calling code of this violation.
Finally, a property procedure must end with the End Property statement (just as a Function procedure ends with End Function , and a Sub procedure ends with End Sub) . If you wished to break out of a property procedure, you would use the Exit Property statement (just as you would use Exit Function to break out of a Function , and Exit Sub to break out of a Sub) .
A Property Get procedure is the inverse of a Property Let procedure. While a Property Let procedure allows code outside of your class to write a value to a private property variable, a Property Get procedure allows code outside of your class to read the value of a private property variable. A Property Get procedure is similar to a VBScript Function procedure in that it returns a value. Here is the syntax.
Class Customer Private mstrName Public Property Let CustomerName(strName) mstrName = strName End Property Public Property Get CustomerName() CustomerName = mstrName End Property End Class
Like a VBScript Function procedure, a Property Get procedure returns a value to the calling code. This value will typically be the value of a private property variable. Notice how the name of the Property Get procedure is the same as the corresponding Property Let procedure. The Property Let procedure stores a value in the private property variable, and the Property Get procedure reads it back out.
The Property Get procedure does not accept any arguments. VBScript will allow you to add an argument, but if you are tempted to do this, then you will also have to add an additional argument to the property's corresponding Property Let or Property Set procedure (if there is one). This is because a Property Let/Set procedure must always have exactly one more argument than its corresponding Property Get procedure.
Adding an extra argument to a Property Let/Set procedure is extremely awkward , and asking the code that uses your class to accommodate more than one argument in a Property Let procedure is very bad form. If you feel you have a need for a Property Get procedure to accept an argument, you are much better off adding an extra property to fulfill whatever need the Property Get argument would have fulfilled.
If your Property Get procedure returns a reference to an object variable, then you may wish to use the Set statement to return the value. For example:
Class FileHelper 'Private FileSystemObject object Private mobjFSO Public Property Get FSO() Set FSO = mobjFSO End Property End Class
However, since all VBScript variables are Variant variables, the Set syntax is not strictly required. This syntax would work just as well.
Class FileHelper 'Private FileSystemObject object Private mobjFSO Public Property Get FSO() FSO = mobjFSO End Property End Class
It's a good idea to use the Set syntax, though, since it makes it clearer that the corresponding Property Get procedure is returning a reference to an object variable.
A Property Set procedure is very similar to a Property Let procedure, but the Property Set procedure is used exclusively for object-based properties. When the property needs to store an object (as opposed to a variable with a numeric, Date , Boolean , or String subtype), you can provide a Property Set procedure instead of a Property Let procedure. Here is the syntax for a Property Set procedure.
Class FileHelper 'Private FileSystemObject object Private mobjFSO Public Property Set FSO(objFSO) Set mobjFSO = objFSO End Property End Class
Functionally, Property Let and Property Set procedures do the same thing. However, the Property Set procedure has two differences:
For example, here is what code that is using an object based on the above class might look like.
Dim objFileHelper Dim objFSO Set objFSO = _ WScript.CreateObject("Scripting.FileSystemObject") Set objFileHelper = New FileHelper Set objFileHelper.FSO = objFSO
Notice that when the last line writes to the FSO property, it uses the Set statement. This is required because the FileHelper class used a Property Set procedure for the FSO property. Without the Set statement at the beginning of the last line, VBScript would produce an error. When a property on a class is object based, it is typical to use a Property Set procedure. Most programmers using your class will expect this.
That said, since all VBScript variables are Variant variables, it is perfectly legal to use a Property Let procedure instead. However, if you provide a Property Let procedure instead of a Property Set procedure, code that is using your class will not be able to use the Set statement to write to the property (VBScript will produce an error if they do), and this will be a trip-up for programmers who are accustomed to using the Set syntax. If you want to be very thorough, and cover both bases, you can provide both a Property Let and a Property Set for the same property, like so:
Class FileHelper 'Private FileSystemObject object Private mobjFSO Public Property Set FSO(objFSO) Set mobjFSO = objFSO End Property Public Property Let FSO(objFSO) Set mobjFSO = objFSO End Property End Class
The Set syntax inside of the Property Set and Let is optional. Since you are writing directly to the Variant private property variable, you can use either. This example is the functional equivalent of the previous example.
Class FileHelper 'Private FileSystemObject object Private mobjFSO Public Property Set FSO(objFSO) mobjFSO = objFSO End Property Public Property Let FSO(objFSO) mobjFSO = objFSO End Property End Class
You can make a property on a class read-only in one of two ways:
Here is the first method.
Class Customer Private mstrName Public Property Get CustomerName() CustomerName = mstrName End Property End Class
Notice the absence of a Property Let procedure. Since we have not provided a Property Let procedure, code outside of the class cannot write to the CustomerName property.
Here is the second method.
Class Customer Private mstrName Private Property Let CustomerName(strName) mstrName = strName End Property Public Property Get CustomerName() CustomerName = mstrName End Property End Class
The Property Get procedure is declared with the Public statement, and the Property Let procedure is declared with the Private statement. By declaring the Property Let as Private , we have effectively hidden it from code outside of the class. Code inside of the class can still write to the property through the Property Let procedure, but in our simple example, this is of limited usefulness . This is because code inside of the class can write directly to the private property variable, so there is little need for the private Property Let procedure.
The exception to this would be when there is code inside of the Property Let procedure that is performing validations and/or transformations on the value being placed in the property. If this were the case, then there might be a benefit in code inside the class using the private Property Let procedure rather than writing directly to the private property variable.
The first method (providing only a Property Get ) is the more typical method of creating a read-only property.
The two techniques for making a property write-only are the exact reverse of the two techniques for making a property read-only (see previous section):
You can provide properties for your class without using Property Let , Set , and Get procedures at all. This is accomplished through the use of public class-level variables. For example, this code.
Class Customer Private mstrName Public Property Let CustomerName(strName) mstrName = strName End Property Public Property Get CustomerName() CustomerName = mstrName End Property End Class
is the functional equivalent of this:
Class Customer Public Name End Class
The second option looks a lot more attractive. It has a lot less code. From a functionality and syntax standpoint, the second option is perfectly legal. However, many VBScript programmers strongly prefer using private property variables in combination with Property Let , Set , and Get procedures, as we have discussed in the previous sections.
Other programmers prefer to use public class-level variables instead of Property Let , Set , and Get procedures. The main advantage of using public class-level variables to create class properties is that this method takes a lot less code. However, not using Property Let , Set , and Get procedures also has some serious disadvantages that you should consider.
Unless you want the code that uses your class to use awkward syntax like objCustomer.mstrName = "ACME Inc." , you cannot use Hungarian scope or subtype prefixes on your class-level variables. If you agree with the theory that Hungarian prefixes add value to your code, this tends to make the code less readable and understandable.
That said, if you can live with these disadvantages, you certainly can declare your properties as public class-level variables and change them to use Property Let , Set , and Get procedures later if the need arises. However, one could make an argument that it's better to do it the 'right' way from the start. This is one of those issues where good programmers will simply have a difference of opinion, but we think you'll find more programmers who prefer Property Let , Set , and Get procedures over public class-level variables.
Often, however, you are creating a simple class for use within a single script. In such situations, it may be more acceptable to take some shortcuts so that the code is simpler and easier to write. In these cases, you may decide to forgo Property Let , Set , and Get procedures and just use public variables.
A method is a different name for functions and procedures, which we have been discussing throughout this book. When a function or procedure is part of a class, we call it a method instead. If you know how to write Function and Sub procedures (see Chapter 4), then you know how to write methods for a class. There is no special syntax for methods, as there is for properties. Your primary consideration is whether to declare a Function or Sub in a class as Public or Private .
Simply put, a class method that is declared with the Public statement will be available to code outside or inside the class, and a method that is declared with the Private statement will be available only to code inside the class.
The example script SHOW_GREETING.VBS contains a class called Greeting , which can be used to greet the user with different types of messages. The class uses both public and private methods. As you can see in the code for the Greeting class, methods defined in a class use the same syntax as any other VBScript procedure or function. The only new consideration with class methods is whether to make them public or private-that is, visible to outside code, or not visible to outside code.
Class Greeting Private mstrName Public Property Let Name(strName) mstrName = strName End Property Public Sub ShowGreeting(strType) MsgBox MakeGreeting(strType) & mstrName & "." End Sub Private Function MakeGreeting(strType) Select Case strType Case "Formal" MakeGreeting = "Greetings, " Case "Informal" MakeGreeting = "Hello there, " Case "Casual" MakeGreeting = "Hey, " End Select End Function End Class
Code that is outside of this class can call the ShowGreeting method, which is public, but cannot call the MakeGreeting method, which is private and for internal use only. The code at the top of the SHOW_GREETING.VBS example script makes use of the class.
Dim objGreet Set objGreet = New Greeting With objGreet .Name = "Dan" .ShowGreeting "Informal" .ShowGreeting "Formal" .ShowGreeting "Casual" End With Set objGreet = Nothing
Running this script results in the dialog boxes shown in Figures 8-2 through 8-4.
Note to Visual Basic programmers: VBScript does not support the
Friend keyword for defining properties and methods.
An event is a special kind of method that is called automatically. In any given context, the objects you are working with may support one or more events. When an event is supported in a given context, you can choose to write an event handler , which is a special kind of method that will be called whenever the event 'fires.'
Any VBScript class that you write automatically supports two events: Class_Initialize and Class_Terminate . As with most events, providing event handler methods in your class is optional. If you include event handler methods in your class, then they will be called automatically; if you don't, then nothing will happen when these events fire-which is not a problem at all if you had no good reason to provide handler methods .
The Class_ Initialize event 'fires' in your class when some code instantiates an object that is based on your class. It will always fire when an object based on your class is instantiated , but whether your class contains any code to respond to it is up to you. If you do not wish to respond to this event, then you can simply choose to omit the event handler method for the event. An event handler is a subprocedure that is called automatically whenever the event that it is tied to fires. Here is an example class that contains a Class_ Initialize event handler.
Class FileHelper 'Private FileSystemObject object Private mobjFSO Private Sub Class_Initialize Set mobjFSO = _ WScript.CreateObject( _ "Scripting.FileSystemObject") End Sub '<> End Class
As in this example, initializing class-level variables is a fairly typical use for a Class_ Initialize handler. If you have a variable that you want to make sure has a certain value when your class first starts, you can initialize it in the Class_ Initialize event handler. You might also use the Class_Initialize event to do other preliminary things such as opening a database connection, or opening a file.
The syntax for blocking off the beginning and ending of the Class_Initialize event handler must be exactly as you see it in this example. Your code can do just about whatever you please inside the event handler, but you do not have the flexibility of giving the procedure a different name . The first line of the handler must be Private Sub Class_Initialize , and the last line must be End Sub . Really, the event handler is a normal VBScript subprocedure, but with a special name.
Technically, the event handler could also be declared with the Public statement (as opposed to Private , but event handlers are generally private. If you were to make it public, then code outside of the class could call it like any other method any time it liked , which would not generally be desirable.
There can only be exactly one Class_Initialize event handler in a given class. You can omit it if you don't need it, but you can't have more than one.
The Class_Terminate event is the inverse of the Class_Initialize event (see previous section). Whereas the Class_Initialize event fires whenever an object based on your class is instantiated, the Class_Terminate event fires whenever an object based on your class is destroyed. An object can be destroyed in either of two ways:
When either of these things occurs, the Class_Terminate event will fire immediately before the object is actually destroyed. (For more information about object lifetime and references, please see Chapter 7.)
Here is the example FileHelper class that we saw in the previous section, this time with a Class_Terminate event handler added.
Class FileHelper 'Private FileSystemObject object Private mobjFSO Private Sub Class_Initialize Set mobjFSO = _ WScript.CreateObject("Scripting.FileSystemObject") End Sub Private Sub Class_Terminate Set mobjFSO = Nothing End Sub ' End Class
In this example, we are using the Class_Terminate event handler to destroy the object that we instantiated in the Class_Initialize event. This is not strictly necessary, since when the FileHelper object is destroyed, the private mobjFSO variable will go out of scope and the script engine will destroy it for us. However, some programmers prefer to explicitly destroy all objects that they instantiate.
You might also use the Class_Terminate event to close a database connection, close a file, or save some information in the class to a database or file. The same syntactical restrictions that apply to Class_Initialize event handlers apply to Class_Terminate event handlers.
For reasons that are unclear, VBScript does not support named constants declared at the class level. That is, you cannot use the Const statement within a class such that the constant variable would be available throughout the class, or from outside the class. For example, this code will produce a compile error.
Option Explicit Dim objTest Set objTest = new ConstTest objTest.SayHello Set objTest = Nothing Class ConstTest Private Const TEST_CONST = "Hello there." Public Sub SayHello MsgBox TEST_CONST End Sub End Class
The compile error will occur on this line.
Private Const TEST_CONST = "Hello there."
The reason is that this statement is scoped at the class-level, which means that it is declared within the class, but not within any of the properties or methods of the class. (The Const statement is legal within a property or method, but it will of course have only local scope within that property or method.) There is a workaround, however, as shown in this example.
Option Explicit Dim objTest set objTest = new ConstTest objTest.SayHello Class ConstTest Private TEST_CONST Private Sub Class_Initialize TEST_CONST = "Hello there." End Sub Public Sub SayHello MsgBox TEST_CONST End Sub End Class
This workaround creates a pseudo-constant. Instead of declaring TEST_CONST with the Const statement, we declare it as a normal, private class-level variable (we could have made it public as well). Then in the Class_Initialize event handler, we give the TEST_CONST variable the 'constant' value that we want. There is a danger in this, however, because code inside your class can still change the value of the TEST_CONST variable. However, using the all-caps naming convention might help prevent this from happening (most programmers are accustomed to equating all-caps with a named constant). You'll just have to make sure the code inside the class behaves itself.
Please note that in earlier versions of VBScript, class-level constants were also not supported. However, strangely, they would not cause a compile error; their values would simply be ignored. If you are using a version of VBScript that does not produce the compile error, you essentially still have the same problem, and the same workaround will do the trick.
A previous version of VBScript had a bug with class-level arrays; you could declare a class-level array, but it would be ignored by the VBScript engine as an array . The array variable itself was not ignored, but the fact that you had declared it as an array was ignored. This occurred with variables declared as fixed or dynamic arrays. This bug appears to have been fixed with the latest versions of VBScript, so you only need to concern yourself with this if for some reason you are not able to upgrade your scripting engine to the latest version. If you are on a pre-XP version of Windows and want to ensure that you have the latest scripting version, you can download the newest version from http://www.microsoft.com/scripting .
If you are for some reason stuck with an older version that has this bug, then there is a workaround: declare the class-level variable as a normal variable (not as an array), and then use the ReDim statement in the Class_Initialize event handler to transform it into an array of the desired type and size . Then you can use the variable throughout the rest of your class as if it had been declared as an array all along.
Note that local constants and arrays (that is, those declared inside of class methods or property procedures) work fine. It's only class-level arrays and constants that will cause these problems for you.
In Chapter 3, we showed how an array could be used to store a list of names and phone numbers . Later, in Chapter 7 we showed how we could store a phone list in a series of one-element arrays in the scripting runtime's Dictionary object. Now, for the remainder of this chapter, we will further adapt the phone list example to use VBScript classes. In much the same way as the example code from Chapter 7, the code we will develop now will accomplish the following:
The example script we will develop will have the following elements:
You might remember from the Chapter 7 example that all of this same functionality was provided, but it was all done without classes. With that in mind, the purpose of this chapter's evolution of the Chapter 7 code is to illustrate how classes can be used to create more generic and reusable code that is more tolerant to future changes. You can read, execute, and experiment with this script in the file PHONE_LIST_CLASS .VBS , which, along with all of the rest of the code for this book, can be downloaded from the wrox.com Web site.
First, let's take a look at the ListEntry class.
Class ListEntry Private mstrLast Private mstrFirst Private mstrPhone Public Property Let LastName(strLastName) If IsNumeric(strLastName) or _ IsDate(strLastName) Then Err.Raise 32003, "ListEntry", _ "The LastName property may not " & _ "be a number or date." End If mstrLast = strLastName End Property Public Property Get LastName LastName = mstrLast End Property Public Property Let FirstName(strFirstName) If IsNumeric(strFirstName) or _ IsDate(strFirstName) Then Err.Raise 32004, "ListEntry", _ "The FirstName property may not " & _ "be a number or date." End If mstrFirst = strFirstName End Property Public Property Get FirstName FirstName = mstrFirst End Property Public Property Let PhoneNumber(strPhoneNumber) mstrPhone = strPhoneNumber End Property Public Property Get PhoneNumber PhoneNumber = mstrPhone End Property Public Sub DisplayEntry MsgBox "Phone list entry:" & vbNewLine & _ vbNewLine & _ "Last: " & mstrLast & vbNewLine & _ "First: " & mstrFirst & vbNewLine & _ "Phone: " & mstrPhone End Sub End Class
This class has three properties: LastName , FirstName , and PhoneNumber . Each property is implemented using a private property variable, along with Property Let and Get procedures. Since we have provided both Let and Get procedures for each property, they can both read from and written to. Notice also that in the Property Let procedures for LastName and FirstName , we are checking to make sure that outside code does not store any numeric or date values in the properties. If an illegal value is passed in, the code raises an error (see Chapter 6).
Checking for numbers and dates is a somewhat arbitrary choice of something to validate; the primary purpose in this example is to illustrate how important it is to use your Property Let procedures to ensure that programmers do not store any data in a property that does not belong. This technique is especially important given VBScript's lack of data type enforcement; since all variables have the Variant data type, any variable can hold any value.
Please note that we could have chosen many other types of validation. We could have checked for data length (minimum or maximum), special characters that might be illegal, proper formatting, and so on. For example, we could have also added a check to the PhoneNumber Property Let that verifies the format XXX-XXX-XXXX . Or we could have added a 'transformation' that coverts the phone number into that format if it already wasn't. What kinds of validations and transformations you choose depends on the situation. The point is to test the assumptions inherent in the rest of your code so as to avoid bugs and errors.
The ListEntry class has one method: DisplayEntry , which uses the MsgBox function to display the properties of a list entry. We chose to put this code in the ListEntry class because of the general principle that a class should provide functionality that it is 'natural' for that class to know how to do. The ListEntry class ' knows ' the last name, first name , and phone number. Therefore, in order to keep the code that manipulates data as close as possible to where the data is stored, we put the DisplayEntry method on the ListEntry class.
In object-oriented parlance, this is called separation of concerns or responsibility-based design . Each class has a set of 'things' it needs to 'know' and to 'know how to do.' We want to design our classes so that the separations between them are logical. The less one class 'knows' about other classes, the better.
However, sometimes there is functionality that we expressly do not want a class to know how to do. The idea is to keep our classes as generic as possible, so that they can be used in multiple ways in multiple contexts. We'll see examples of this as we continue to build our code.
Moving on, this is our second class, PhoneList .
Class PhoneList Private objDict Private Sub Class_Initialize Set objDict = CreateObject("Scripting.Dictionary") End Sub Private Sub Class_Terminate Set objDict = Nothing End Sub Public Property Get ListCount ListCount = objDict.Count End Property Public Function EntryExists(strPhoneNumber) EntryExists = _ objDict.Exists(strPhoneNumber) End Function Public Sub AddEntry(objListEntry) If TypeName(objListEntry) <> "ListEntry" Then Err.Raise 32000, "PhoneList", _ "Only ListEntry objects can be stored " & _ "in a PhoneList class." End If 'We use the PhoneNumber property as the key. If Trim("" & objListEntry.PhoneNumber) = "" Then Err.Raise 32001, "PhoneList", _ "A ListEntry object must have a " & _ "phone number to be added to the " & _ "phone list." End If objDict.Add objListEntry.PhoneNumber, objListEntry End Sub Public Sub DisplayEntry(strPhoneNumber) Dim objEntry If objDict.Exists(strPhoneNumber) Then Set objEntry = objDict(strPhoneNumber) objEntry.DisplayEntry Else Err.Raise 32002, "PhoneList", _ "The phone number '" & strPhoneNumber & _ "' is not in the list." End If End Sub End Class
The first thing to notice about this class is that internally it is using a private Dictionary object to store the phone list. This is a powerful technique for two reasons: first, it illustrates how your classes can ' borrow ' the functionality of other classes; and second, the fact that we don't expose the internal Dictionary object to any code outside of the PhoneList class means that scripts that use the PhoneList class do not need to have any knowledge of how the PhoneList class stores the data. If we want to change the Dictionary to some other data storage method (such as an array, hash table, text file, and so on), we could do so without breaking any other code that uses the PhoneList class. Next, as illustrated earlier in this chapter, notice we are using the Class_ Initialize and Class_ Terminate events to control the lifetime of the internal Dictionary object ( objDict ). This allows the rest of the class to be able to assume that there is always a Dictionary object to use.
Next, we have a Property Get procedure called ListCount and a method called EntryExists . The ListCount property is a 'wrapper' for the objDict.Count property, and likewise ExtryExists is a wrapper for the objDict.Exists method. We could have chosen to expose other Dictionary properties and methods as well. However, we want to be careful about this because we don't want to lose our future flexibility to change out the Dictionary object with another data storage structure.
For example, we could make things really easy and just expose objDict as a property and let outside code use it directly as a dictionary. However, if we did that, outside code would become too 'tightly coupled ' to the internals of our class-meaning that outside code would have too much 'knowledge' about how our class works internally. As much as possible, we want our PhoneList class to be a 'black box'; you can use the functionality of a black box, you can know what goes in and what comes out, but you can see what's inside the box that makes it all work.
Next we have the AddEntry method. This method really does only one thing: it calls the dictionary's Add method, using the phone number of the list entry as the key for the dictionary.
objDict.Add objListEntry.PhoneNumber, objListEntry
Notice that we are storing the ListEntry object itself in the dictionary, just as in Chapter 7 we stored a phone list entry array in the dictionary.
However, this is the last line of the method. All of the code that comes before it is validation code. The idea here is that we want to test and document the assumptions made by this method. This method has two important implicit assumptions:
To test these assumptions, first we use the TypeName function check to make sure that the outside code is passing us a ListEntry object, and not some other kind of data. This is necessary because, given VBScript's lack of data type enforcement, we need to do our own validation. Second, we check to make sure that the ListEntry object has a non-blank value in the PhoneNumber property. This way we can make sure that we have something that can be used as a key.
There are other assumptions that we could have tested as well, but these are the two that would be most likely to produce strange bugs or error messages that would make it difficult for programmers using our classes to figure out what they are doing wrong. These clear error messages document for all concerned what the important assumptions are.
Finally, PhoneList has a method called DisplayEntry . Now wait a minute-didn't we also have a DisplayEntry method on the ListEntry class? Why two methods that apparently do the same thing?
It all comes down to design options. There is not necessarily a 'correct' way to design these classes. The DisplayEntry method of the PhoneList class 'delegates' the responsibility of displaying of an entry to the ListEntry.DisplayEntry method, as you can see in these lines.
If objDict.Exists(strPhoneNumber) Then Set objEntry = objDict(strPhoneNumber) objEntry.DisplayEntry
So even though we have two methods, there is not really any duplication because the code that does the actual displaying only exists in the ListEntry class. The implicit design decision we made was to specialize the PhoneList class with methods (such as DisplayEntry ) that allow programmers to do specific things with phone list entries (such as displaying them), as opposed to going with a more generic approach that just exposes the list of entries, allowing the outside code to do the work embodied in the three lines of code above-that is, finding the correct entry and telling it to display itself. Both designs are valid, and nothing in our chosen design would prevent us from extending these classes in any number of ways in the future.
Now that we have our two classes, lets look at some code that makes use of these classes (again, all of this code can be found in PHONE_LIST_CLASS.VBS ).
Option Explicit Dim objList FillPhoneList On Error Resume Next objList.DisplayEntry(GetNumberFromUser) If Err.Number <> 0 Then If Err.Number = vbObjectError + 32002 Then MsgBox "That phone number is not in the list.", _ vbInformation Else DisplayError Err.Number, Err.Source, _ Err.Description End If End If Public Sub FillPhoneList Dim objNewEntry Set objList = New PhoneList Set objNewEntry = New ListEntry With objNewEntry .LastName = "Williams" .FirstName = "Tony" .PhoneNumber = "404-555-6328" End With objList.AddEntry objNewEntry Set objNewEntry = Nothing Set objNewEntry = New ListEntry With objNewEntry .LastName = "Carter" .FirstName = "Ron" .PhoneNumber = "305-555-2514" End With objList.AddEntry objNewEntry Set objNewEntry = Nothing Set objNewEntry = New ListEntry With objNewEntry .LastName = "Davis" .FirstName = "Miles" .PhoneNumber = "212-555-5314" End With objList.AddEntry objNewEntry Set objNewEntry = Nothing Set objNewEntry = New ListEntry With objNewEntry .LastName = "Hancock" .FirstName = "Herbie" .PhoneNumber = "616-555-6943" End With objList.AddEntry objNewEntry Set objNewEntry = Nothing Set objNewEntry = New ListEntry With objNewEntry .LastName = "Shorter" .FirstName = "Wayne" .PhoneNumber = "853-555-0060" End With objList.AddEntry objNewEntry Set objNewEntry = Nothing End Sub Public Function GetNumberFromUser GetNumberFromUser = InputBox("Please enter " & _ "a phone number (XXX-XXX-XXXX) with " & _ "which to search the list.") End Function
Running this code and entering the phone number 404-555-6328 results in the dialog box shown in Figure 8-5.
Running the code again and entering an invalid phone number results in the dialog box shown in Figure 8-6.
The most important point to take away from this elaborate example can be found in these two simple lines of code (without the error handling related code).
These two lines represent the total logic of our script: create a phone list, fill it up, ask the user for a phone number to search for, and display the entry. This is the beauty of breaking our code up into separate classes and procedures. If we make our classes and procedures as generic as possible, then the code that actually strings them together to do something useful can be relatively simple and easy to understand and change-not to mention easy to reuse in any number of ways.
The FillPhoneList procedure creates a PhoneList object ( objList ) and fills it up with entries. If you use your imagination , you could picture the phone list entries coming from a database table, a file, or from user entry. FillPhoneList uses a 'temporary object variable' called objNewEntry . For each entry in the list, it instantiates objNewEntry , fills it with data, then passes it to the objList.AddEntry method.
Notice that we use the New keyword in FillPhoneList to instantiate objects from our custom VBScript classes.
Set objList = New PhoneList
Set objNewEntry = New ListEntry
What happened to the CreateObject function? CreateObject is only for use in instantiating nonnative VBScript objects (such as Dictionary and FileSystemObject ), whereas we must use New to instantiate a custom VBScript class that exists in the same script file. The reasons behind this are complex, so keep this simple rule in mind: if you are instantiating an object based on a custom VBScript class, use New; otherwise , use CreateObject .
The GetNumberFromUser function is very simple. It uses the InputBox function to prompt the user for a phone number and returns whatever the user entered. The code at the top of the script then passes this value to objDict.DisplayEntry . If the entry exists, the ListEntry object will display itself. If not, objDict.DisplayEntry will return an error.
What is important is not to dwell on the way in which we have used the PhoneList and ListEntry classes. Rather, what is important is to realize that our PhoneList and ListEntry classes can be used in any number of ways for any number of purposes. As new needs arise, the classes can be extended without breaking any code that is already using them. Any future programmers who come to our script will have a very easy time understanding what our script is doing. After all, it's all in these two lines of code.
If a programmer wants to further understand the low-level details of how the script works, he or she can choose to read the rest of the code, digging into the procedures and the classes. But in many cases, that would be unnecessary, unless the programmer needed to fix a bug or add some functionality. If all a programmer wants to know is What does this script do?, the answer is right there in those two simple lines.
In this chapter we explained how to develop classes in native VBScript. This is a powerful ability that allows you to create scripts that are object oriented in nature, which, when properly designed, can give you greater understandability, maintainability, flexibility, and reuse. A VBScript class is defined using the Class End Class block construct. In general, classes must be defined within the same script file as the code that will make use of them. (For an alternative technique, please see Chapter 13 for information on Windows Script Components.)
Classes can have properties and methods . Properties are defined using either public variables or special procedure constructs called Property Let , Get , and Set procedures. Using private property variables and different combinations of Let , Get , and Set procedures, you can control whether properties are read-only, write-only, or both. Methods are defined like normal procedures and functions, and can be either public or private.
The final section of this chapter gives a detailed class-based example, including explanations of the design and programming techniques. The example is an extension of the phone list examples used in Chapters 3 and 7.