On the Microsoft campus, you sometimes see employees sporting T-shirts that advertise a mythical product called Object Basic. Not every Microsoft product that reaches the T-shirt stage reaches the product stage, at least not under the same name; just ask a Microsoft old-timer about Cirrus or Opus or Cashmere. In a larger sense, however, Visual Basic has become Object Basic, regardless of the name the marketing department puts on the box.
Object Basic has been on the horizon for a long time, and it still has a way to go. But, starting in version 4, objects became more than an afterthought, and they continued their march toward objectness in version 5--although not at a fast enough pace to suit me. Form modules provide a way to create visual objects with properties and methods, and class modules provide a way to create your own nonvisual objects. Controls, forms, classes, the Data Access Object--all can be created with Dim, assigned with Set, manipulated with properties and methods, and grouped in collections.
Objects are more than a new feature; they're a way of thinking. Objects can help you make your code more modular, and you can make your objects available to any program that understands COM Automation. In other words, your objects can be used by programs written in Visual Basic, by programs written in most versions of C and C++, and by macros in many applications, including Microsoft Word, Microsoft Excel, and the Visual Basic environment itself.
This chapter (and the rest of the book) preaches the object religion. From here on out, we'll be talking about Object Basic not as a product but as an attitude, a state of mind. By the time you finish, you'll be telling your children to change their methods and properties or face Punishment events. You'll go into a restaurant and ask your waitperson to create a Hamburger object and set its Tomato and Onion properties to True but set its Mustard property to False. You'll write your representatives letters demanding fewer Tax events and more Service methods. For Each friend in your Friends collection, the friend will call the Wish method with the named argument NeverSeenThisBook:=True.
Visual Basic has its own style for doing object-oriented programming, just as it has its own style for doing most everything else. Let's look briefly at how Visual Basic both resembles and differs from other object-oriented languages.
Purists will no doubt argue that Visual Basic isn't an object-oriented language at all because it doesn't fully support the three pillars of object-oriented languages--encapsulation, reusability, and polymorphism. (Most books describe the second pillar as inheritance rather than reusability, but bear with me.) In the first edition of this book, I just eyeballed the language and said it supported one and a half out of three pillars of object-oriented programming. In retrospect, this was an extremely generous rating. If I rated version 4 according to the rigorous scientific test I'm about to apply to version 5, it would come out at about 1.2 (.6 for encapsulation, .2 for reusability, and .4 for polymorphism).
So here's a complete scoresheet supporting my current rating of 1.9 (with an error factor of .2) for Visual Basic version 5 as an object-oriented language.
Encapsulation means bringing data to life. Instead of you, the outsider, manipulating data by calling functions that act on it, you create objects that take on lives of their own, calling each other's methods, setting each other's properties, sending messages to each other, and generally interacting in ways that more closely model the way objects interact in real life. Early versions of Visual Basic allowed you to use objects that were created in other languages; version 4 allowed you to create your own objects, although there were significant holes in the encapsulation model.
Visual Basic version 5 has got most of encapsulation right on the second try. Big problems with public constants have been fixed by the Enum statement. You can now set a default property or method (although it's unnecessarily difficult), and you can create collections (but that's even harder). Property procedures give Visual Basic an edge over C++ and many other object-oriented languages in the way they allow you to access data with a natural syntax--and without exposing internal data to unauthorized changes.
Visual Basic still lags behind other object-oriented languages in the way it allows data sharing between classes and between instances of the same class. The new Friend modifier allows you to share data between classes, but the design is debatable. On the one hand, it allows you to specify exactly what you want to share with other classes (unlike some languages that say if you share any private part of a class you share all of it). On the other hand, the Friend modifier doesn't allow you to specify with whom you're sharing data within a project (unlike other languages that allow you to specify who your friends are).
Visual Basic provides no language feature for sharing data between instances of an object (static variables in C++), but you can fake it with public variables in what Visual Basic still quaintly calls "standard" modules ("irregular" modules would be a better term). This hack exposes your data not only to multiple instances of an object, but to any other module that wants to see it. When you create a public class or control, friend members and public variables in standard modules within the project become invisible to clients outside the component, so there is some protection. But it's a kind of accidental feature that isn't up to the carefully designed protection schemes of C++ or Java, which provide protection for all classes, not just those in components. The protection mechanism isn't the most important feature of a language, but I still have to deduct points for an inferior design.
The biggest problem in Visual Basic encapsulation is its inability to initialize an object in its declaration. Unless your object can be successfully initialized with default properties, it will be invalid from the time it is created until the time you initialize the key data properties. This isn't a big problem with controls, which have property pages as their initialization mechanism. It is a big problem for most other objects, which must be initialized through some convention defined by the class designer. Convention is not a reliable way to create rock-solid objects. Despite the seriousness of this limitation (which you'll hear a lot more about), I don't deduct much for it because lack of initialization syntax is a fault of the language, not just of classes. You can't initialize an Integer any more than you can initialize a CMyClass object. If I were going to rate Visual Basic as a general language, I'd list this as its most serious limitation.
This pillar of object-oriented programming is usually called inheritance, but inheritance is actually one of several techniques for achieving the broader goal of reusability. Reusability means being able to create a new class that uses the features of an existing class without recoding those features. There are actually several ways to achieve reusability in object-oriented programming.
Inheritance means reusing code in a hierarchical structure. For instance, a BasicProgrammer is a Programmer is a Worker is a Person is an Animal. All Animals have heads, and therefore the Head property of a BasicProgrammer should inherit all the general features of Animal heads plus all the features of Person heads plus all the features of Worker heads plus all the features of Programmer heads. When creating a Head property for a BasicProgrammer object, you should need to write only the head code unique to BasicProgrammers. There's a lot of debate in the object-oriented community about the value of deep levels of inheritance such as the one described above, but there's no doubt that inheritance can be a useful way to achieve reusability. And Visual Basic doesn't have it.
The way to get reusability in Visual Basic is through a process called delegation (also called containment). For example, you put an Animal object inside your Person class. You write code to expose the appropriate Animal methods and properties as Person methods and properties. Then you put a Person object in your Programmer class and expose its methods and properties, and so on.
Many object-oriented design books talk about two kinds of reuse relationships. In the is-a relationship, one class is an enhanced version of another class. A Person, for example, is an Animal with additional features. Normally, these relationships are defined with inheritance. In the has-a relationship, one class has features of another class. Normally, these relationships are defined with delegation. For example, an Animal class might delegate blood circulation to a Heart class and digestion to a Stomach class. Since Visual Basic doesn't support inheritance, it forces you to define both kinds of relationships with delegation. If you could achieve the same results with delegation, this would be a problem only for object-oriented design purists. Unfortunately, inheritance is automatic while delegation is usually manual (although nothing stops a language designer from making it automatic). When using inheritance to model is-a relationships, you have to write code for the new features only. When using delegation for is-a relationships, you delegate everything, even the methods and properties that don't change. This mechanical process ought to be automated, and in fact, Visual Basic does automate it, but it works only for controls, not for classes. Furthermore, automatic control delegation is not a language feature. It's a wizard program that writes the code it thinks you want. The pitfalls and limitations of wizards are a subject I won't get into here.
The Component Object Model (COM) supports a third reuse technique called aggregation. Aggregation means combining several inner objects so that they appear to be part of an outer object. This is a collective organization rather than a hierarchical organization. If you tried to combine a BasicProgrammer, a Programmer, a Person, and an Animal using this method, you'd end up with a lot of duplication and confusion. Clients would have to decide whether to use the Person.Head or the Programmer.Head, so instead you would use an organization that adds features rather than extends them. If you started with an Animal class, you might then add person features, such as Reasoning and Emotions. Later you add programmer features, such as Coding and Debugging. Finally you add Visual Basic programmer features, such as IterateWithForEach and HandleEvents. Although Visual Basic doesn't directly support this feature, you can get some of the advantages of it with the new Implements statement.
The bottom line: reuse is unnecessarily painful and inconvenient in Visual Basic--a language that is supposed to make things easy.
Polymorphism means that any object will be able to do the right thing if you send it a message it understands. If I call the Hack method of a Programmer object, a Woodcutter object, and a Butcher object, each object should be able to Hack in its own special way without knowing who sent the message.
Visual Basic supports polymorphism in two ways. First, you can achieve polymorphism by using the Object type. Any Hack method works on an Object as long as the caller uses compatible arguments and return types. This dumb form of polymorphism was available in version 4 and remains in version 5, but you should avoid it because it is terribly slow and has no type protection. On the other hand, there are times when excessive type protection gets in the way and speed isn't critical. If you want your polymorphism dumb (that is, you don't know the type until run time), use Object.
Most of the time it's better to use interfaces through the new Implements statement to get fast, safe polymorphism. Visual Basic's syntax for Implements makes it work kind of like events. I find this syntax awkward and confusing, but perhaps it's a matter of taste. The mechanism would be more comfortable if you could typecast to interfaces to avoid creating extra object variables. But once you get used to it, Implements enables a lot of cool new techniques. Furthermore, Implements is almost a direct implementation of the powerful COM concept of interfaces. Unfortunately, some of the advantages of using COM interfaces get washed out because Visual Basic doesn't support the types used in most standard COM interfaces.
That's an important part of the polymorphism story, but only part. In most object-oriented languages, polymorphism goes hand-in-hand with inheritance. If Woodcutter and Butcher both inherit from Cutter, but Programmer inherits from Thinker, then Programmer.Hack will fail in contexts that expect Cutter.Hack. There are lots of techniques you can use in languages that have both inheritance and polymorphism that you can't do easily in Visual Basic. Java, for example, gives you the choice of polymorphism straight or with a side of inheritance, so I have to deduct for Visual Basic's one-track polymorphism.
So Visual Basic comes out with a rating of 1.9 as an object-oriented language. I'd set a higher cutoff level for what can be considered a "real" object-oriented language--maybe about 2.5. Languages like C++, Delphi, Java, and SmallTalk rate above that line, although they, too, have their faults. But object-orientation isn't the only basis for rating a language. Visual Basic has many advantages those other languages lack.
Regardless of limitations, you can certainly write object-oriented programs with Visual Basic. In fact, all my programs are object-oriented. I try not to use "irregular" modules any more than necessary, although avoiding them turns out to be harder than you might expect or hope it to be. Let's take a closer look at some of the things that make object-oriented programming object-oriented.
You don't have to know exactly what an object is in order to use one, but it helps. The following section might look like an academic essay about types, and at some point you might be tempted to skip to the part where we start playing with objects. And perhaps that will be OK for those of you who started object-oriented programming with Visual Basic. Perhaps the Basic Way of objects seems natural to you and you don't need my feature-by-feature comparison with UDTs and intrinsic types. It might be easier to just start out thinking of objects as being completely different from the other types and leave it at that.
But I started my object-oriented career with C++. When the light finally dawned and I understood how Visual Basic objects worked, it seemed like a revelation worth sharing. Unfortunately, some of those revelations came after I wrote the first edition of this book and I'm afraid I passed on some misinformation. I hope you'll at least attempt to wade through this, not only so I can atone for past sins, but also so you can avoid some of my mistakes.
A type is a template that describes the kind of data that variables of that type can contain and how variables of that type should behave. Visual Basic supports three kinds: intrinsic types, user-defined types, and classes. Types can be used to define variables and objects that hold data. Types also define the operations that variables or objects can perform. Before we look at how the three kinds of types manage this, let's define some terms for this discussion. These aren't necessarily standard terms, but they help me describe my mental model of how types work.
Now let's look at how these terms apply to different types.
Of course, you already understand intrinsic types such as Long, Double, and String. They contain data of the size specified by the type, and they contain built-in methods. Methods? On intrinsic types? Yes, they're called operators. You probably don't think of operators as methods--unless you've programmed in C++ or some other language that lets you redefine operators. But think about it for a minute. An operator is just a symbol that defines some standard operation for a type. Consider this statement:
iSum = iPart + 6 It's as if the Integer type had a Plus method: iSum = iPart.Plus(6)
In fact, in C++, an operator can use a function syntax:
iSum = iPart.operator+(6)
Operators are even polymorphic. The String, Integer, and Double types all have + operators that look the same and work in a way that intuitively looks the same, although what happens on the chip is very different for each type.
Operations on Integer and other intrinsic types are predefined. The only way to add new operations is to write procedures that have parameters of the given type. This is called functional programming, and it looks like this:
Sub SquareInteger(i As Integer) i = i * i End Sub
The point I want to make about intrinsic types is that the variable contains the instance data. After the expression i = 6, the variable i refers to an instance containing 6. Technically, you can make a distinction between the variable i and the instance containing 6 that i names. But nobody except me even uses the term instance when talking about intrinsic types, and I wouldn't either if I weren't leading up to an explanation of classes.
User-defined types are aggregates of other types. Here's a type:
Type TChunk Title As String Count As Integer End Type
And here's a variable that uses it:
Dim chunk As TChunk chunk.Title = "Thick and Chunky" chunk.Count = 6
As with the Integer variable, the UDT variable contains the instance. The name chunk refers to an instance containing the String Thick and Chunky and the Integer 6. (The string characters aren't really an instance, but don't confuse me with facts.) Unlike intrinsic types, UDTs have only two standard operations: assignment and member access.
Assignment works only on UDTs of the same type:
anotherchunk = chunk
Member access lets you do additional operations on the fields of a UDT variable:
c = chunk.Count
The other operations allowed on the fields are those supported by the field type. For example, Integer type supports assignment, so chunk.Count supports assignment. You can't give a TChunk any additional operators or methods any more than you can give an Integer more operations. The only way to operate on a UDT is the functional way:
Sub SquareTChunk(chunk As TChunk) chunk.Count = chunk.Count * chunk.Count End Sub
I'm stating the obvious limitations of types here so that I can contrast them to the real subject of this section--classes.
On the surface, a class has some things in common with other types. Often you can use a class type with a syntax similar to that of user-defined types, but don't let these similarities lull you into ignoring fundamental differences.
Like a UDT, a class aggregates data members, but the syntax is very different. It might be handy to define certain classes in the same way that you define UDTs, as follows:
Class CThing Title As String Count As Integer End Class
That looks like Visual Basic, but it isn't. Many languages define classes with a similar syntax, but in Visual Basic, classes are more like forms. One form, one file; one class, one file. A CThing class definition might actually look more like this:
Public Title As String Private c As Integer Property Get Count() As Integer Count = c End Property Property Let Count(cA As Integer) iCount = IIf(cA > 0, cA, 0) End Property Sub Square() c = c * c End Sub
The difference between the CThing class and the TChunk type is obvious. CThing does things. It squares itself. TChunk just sits there waiting for some outsider to square it.
You might say that a class is a type that acts. I'll go into a lot more detail about what it means to be a type that does things, but first let's take a closer look at how to create and use class instances.
Class instances are usually called objects, but let's keep calling them instances for a little while longer. Creating them works like this:
Dim thing As CThing Set thing = New CThing thing.Title = "Things Fall Apart" thing.Count = 6
You can't just declare a variable and start partying on it. You have to create a New one first (often with Set). Bear with me if you think you already know why.
The reason you have to use New is because with class instances, the variable isn't the same as the instance. Figure 3-1 shows the difference. The variable is your only means of communicating with the instance, but it's not the same thing.
Figure 3-1. User-defined types versus classes.
Let's look at an example that illustrates the difference:
Dim chunkJoe As TChunk, chunkSue As Tchunk Dim thingJoe As CThing, thingSue As Cthing
Like all Visual Basic variables, these are automatically initialized. The TChunk variables have their strings initialized to vbNullString and their integers to 0. The CThing variable doesn't have an instance yet, so the instance can't be initialized, but the variable is initialized to Nothing. To make them do something useful, you can assign properties as follows:
chunkJoe.Title = "Call me Joe" chunkSue = chunkJoe Set thingJoe = New CThing thingJoe.Title = "Call me Joe" Set thingSue = thingJoe
It might seem that you've created four objects and set them all to the same value. If you test in the Immediate window, the results indicate that everything is the same:
? chunkJoe.Title, chunkSue.Title Call me Joe Call me Joe ? thingJoe.Title, thingSue.Title Call me Joe Call me Joe
This is an illusion, as you can see if you change Title:
chunkSue.Title = "My name is Sue" thingSue.Title = "My name is Sue"
Print out the values:
? chunkJoe.Title, chunkSue.Title Call me Joe My name is Sue ? thingJoe.Title, thingSue.Title My name is Sue My name is Sue
Each TChunk variable contains its instances, so changing chunkSue doesn't affect chunkJoe. But the thingJoe and thingSue variables both reference the same instance. Changing one changes the other because there isn't any other. If you want another, you have to create it with the New operator:
Set thingJoe = New CThing thingJoe.Title = "Call me Joe" Set thingSue = New CThing thingSue.Title = "Call me Sue"
This difference between instances of classes and the instances of other types is so fundamental that we generally use different names. With classes, variables are called object variables and instances are called objects. With other types, the term variable usually refers both to the variable and the instance, because they are the same. I'll switch to this more standard terminology from here on out.
This certainly isn't the only way a language can declare, create, initialize, and use objects. But it is the most efficient way for a language to manage objects if you don't have pointers, which is why Java looks to me more like Visual Basic than like C++.
The CThing object might be complete as soon as it's created, but in real life most objects aren't meaningful at the instant of creation. Visual Basic allows you to initialize a class or a form with the Initialize event and clean it up with the Terminate event, but that's rarely enough information to construct a working object; most objects need more information from the user before their state is complete. This isn't a problem with controls and forms because their initial state can be set with properties at design time. But classes can be initialized only in code.
What you need (and what most object-oriented languages have) is a means to specify initialization data as part of the New syntax. If Visual Basic had constructors like other object-oriented languages, the syntax might look like this:
Set thing = New CThing(1, "Object") ' You can't do that
Instead, if you want to pass initialization data to a class, you must do so by convention and hope that your users will follow the rules. A traditional name I use for initialization methods is Create:
thing.Create 1, "Object" ' Initialize with Create method
Another convention I use with objects that have one primary property is to make that one property the default:
Dim shsTitle As CSharedString Set shsTitle = New CSharedString shsTitle = "Share and share alike" ' Initialize with default property
In Visual Basic, most objects of substantial size and complexity will be invalid between the time you create them with New and the time you initialize them with a Create method (or whatever other technique you choose). You also have to worry about what to do if the user fails to follow your initialization convention--assert, raise an error, or let it be.
The lack of initialization--both of variables and of objects--is, in my opinion, the greatest flaw of the Visual Basic language. No other major high-level language shares this flaw. You'll hear a lot more about initialization problems in this book. If I start to sound like a broken record, it's because the language feels like a broken record.
The New statement is often used in object declarations. This gives the declaration a syntax so similar to an intrinsic variable declaration that you might be tempted to think they work the same way. You understand how this works:
Dim chunk As TChunk chunk.Title = "Chunk-a-lunk"
Surely this must be the object equivalent:
Dim thing As New CThing thing.Title = "Thing-a-ling" thing.Square
Those statements seem simpler and shorter than these:
Dim thing As CThing Set thing = New CThing thing.Title = "Thing-a-ling" thing.Square
You might be tempted to fall into the old C trap of assuming that the more expressions you can cram into each line of code, the faster your program will run. It just isn't so. Whether you use Set to create the object in a separate step or whether you use New in the declaration, you're going to end up with the same thing--an object variable that references a separate object. You can get a clue about what's happening by stepping over these statements in the debugger. In neither case will you step on the Dim line. You can't set a breakpoint on it, either. That's because it's not executed at run time. The statement reserves space for the object variable at compile time, but it doesn't create the object.
Visual Basic intrinsic variables are always initialized to something--usually zero or empty. Object variables are initialized to Nothing. So whichever version you use, you still end up with Nothing. The difference occurs when you execute the next statement. If you declared the variable with New, Visual Basic automatically creates a CThing object for you the first time you use it. It's as if you wrote the following code:
Dim thing As CThing If thing Is Nothing Then Set thing = New CThing thing.Title = "Thing-a-ling" If thing Is Nothing Then Set thing = New CThing thing.Square If thing Is Nothing Then Set thing = New CThing ' Continue like this for all properties and methods
You don't have to write all that extra code to check for Nothing; Visual Basic writes it for you. But you do have to execute it. Why can't the compiler see that you've already created the object variable after the first statement and quit checking? Because it's a compiler. Consider this statement:
Dim thing as New CThing If fHellFrozenOver Then thing.Title = "The latest thing"
The compiler can't tell at compile time whether hell is frozen over. That won't be known until run time. Thus it won't know whether to create a new CThing object without checking. Theoretically, you could write an optimizing compiler that would analyze conditionals and eliminate redundant checks in cases where there was only one possible code path. But that compiler couldn't work on p-code, where every statement has to stand on its own. You're better off just using the Set statement to create the objects yourself.
Now, before you throw New out of your declarations toolbox, take a look at the results of the Performance sidebar on page 133. The real-world penalty for using New in declarations for compiled code just isn't worth worrying about. It's all but free.
Furthermore, using New in declarations can be a powerful way of managing memory allocation, especially with global or form-level variables. Let's say you declare this variable at the form level:
Private thing As New CThing
The thing variable is set to Nothing and no memory is allocated for the CThing object. If you follow a code path that never uses the thing variable, it never gets created. You pay no penalty (except for one object variable) for the unused object. But let's say you do use the thing variable at some point. It is automatically created and you can use it up. Once you've milked it dry, you might want to throw the worthless thing away. Since it's declared at the form level, thing isn't going to automatically go out of scope and destroy itself until the form is destroyed, but you can free that unused memory specifically like this:
Set thing = Nothing
As long as no other object variable has a reference to the same object, it will go away and its memory will become available for other uses. But if you later decide you need that object again, just reference it again and it will spring to life again (but without any previous state values).
This form of automatic memory management is often used with form objects, including the automatic ones used for startup forms. They're also used in global classes, as you'll see later.
Object-oriented programming turns traditional functional programming on its ear. In functional programming, you call a procedure indicating what you want to do and then pass arguments specifying what to do it to, and how to do it:
DoThis ToThat, WithThese
In object-oriented programming, you indicate what you want to work on and then specify methods to indicate what you want to do:
ToThis.DoThat WithThese
In traditional functional programming, you can pass two kinds of arguments: one indicates what you want to work on, and the other provides additional information about how to do the work. In object-oriented programming, you sometimes don't need to indicate what you want to work on, because a default object is understood. In Visual Basic terms, the statement
DoThis WithThese
might mean
Me.DoThis WithThese
At other times, you don't need to specify how to work on something because you have only one possibility:
DoThis ' WithDefaultThese
Internally, the processor doesn't know anything about object-oriented programming; assembly language is always functional. Object-oriented languages fake object-oriented programming internally by passing a hidden argument. When you write the code
ToThis.DoThat WithThese
in Visual Basic (or any object-oriented language), what the processor really sees, in simplified terms, is this:
DoThat ToThis, WithThese
To attach the DoThat method to the ToThis object in Visual Basic, you create a class for CThis objects, of which ToThis will be an instance. Within the class, you create a public DoThat sub and give it a WithThese parameter. When you create a new ToThis instance of the CThis class, you can call DoThat using object-oriented syntax:
Dim ToThis As New Cthis ToThis.DoThat WithThese
Of course, you can still use the functional style on objects. You must be able to do this so that one object can work on another. Only one object can be the base object, so if there are two objects involved, one has to be passed as a method parameter.
Creating a functional version of a method is easy. Assume you have the following object method:
Private iCount As Integer § Sub Square() iCount = iCount * iCount End Sub
It's easy to create a functional version that uses it:
Sub SquareThing(ByVal thing As CThing) thing.Square End Sub
This procedure can be either a Sub in a standard module, or a method in another class module.
Let's talk about properties. The syntax for using properties looks exactly the same as the syntax for using member fields of a UDT. But in fact, a property is very different from a UDT field.
Essentially, a property looks like a procedure to the implementor, but works like a variable to the user. The purpose of using property procedures rather than just giving the world access to internal variables is two-fold. You can validate the input data you receive from users, and qualify or process output data before giving it out to users. Another advantage is that procedures abstract data so that you can change the implementation without breaking client code.
Every object-oriented language has this problem, but most of them have a less elegant solution. C++ and Java, for example, depend on conventions such as always using Get and Set in the names of access methods.
c = thing.GetCount(); // C++ or Java access internal data thing.SetCount(c + 6);
But in the Basic philosophy, the details of data protection should not affect clients. They shouldn't have to use an unnatural syntax in order to be protected.
Instead, Visual Basic allows you to protect internal data with property procedures--Let, Get, and Set. The classic Get/Let property pair looks like this:
Private sStuff As String § Property Get Stuff() As String ' Qualify or process data output here Stuff = sStuff End Property Property Let Stuff(sStuffA As String) ' Validate data input here sStuff = sStuffA End Property
There are lots of ways to validate data. But what if you have data that just doesn't need validation? This often happens with Boolean properties. How are you going to come up with invalid input for a Boolean? By definition, everything is either True or False. Sometimes strings are the same way; if empty and null strings are valid for your property, then there probably isn't any need to validate them. Visual Basic provides a shortcut for defining properties that can't go wrong:
Public Stuff As String
This syntax makes Stuff appear to be a data member of the class in the same way that a field is a data member of a UDT. This is an illusion. Don't expect to get any noticeable speedup from using a Public property instead of a property procedure. What the syntax means is that Visual Basic will generate the property procedures for you behind the scenes. In the COM standard on which Visual Basic is based, all access to objects is through procedures.
Technically, property procedures are also an illusion. What you really get with a property procedure is something that would look more like this if you could write it in Visual Basic:
Private sStuff As String § Function GetStuff(sStuffRet As String) As HResult sStuffRet = sStuff GetStuff = 0 ' Always return error code or 0 for success End Function Function LetStuff(sStuffA As String) As HResult sStuff = sStuffA LetStuff = 0 ' Always return error code or 0 for success End Function
If Visual Basic is your mother tongue or if you have real work to do, you can skip this sidebar. On the other hand, if you enjoy seeing people make fools of themselves in public, you might want to watch me try to prove that Java and Visual Basic are twins, separated at birth, while C++ isn't even the same species.
To make this difficult argument, I'll ask you to put aside the issues that language aficionados normally argue about. I don't care whether the type or the variable comes first. Perhaps Dim is the worst keyword name ever; maybe it's typedef. Should blocks be enclosed in curly braces or terminated with an End statement? Who cares? Are free-format languages better than line-oriented languages? Leave it to the lawyers.
What really matters is how memory is allocated and used. For example, here's how you declare object variables in Visual Basic:
Dim thing As CThing ' VB declare object variable
This is similar to a C++ pointer variable, which you create like this:
CThing * pthing; // C++ declare object pointer variable
Java, like Visual Basic, has no pointers. Object variables are created like this:
CThing thing; // Java declare object variable
Java and C++ might look similar, but that asterisk in the C++ declaration makes a world of difference. In what really counts, Visual Basic and Java are the same.
All of these statements create a reference to a theoretical object, but none of them actually creates a CThing object. The Visual Basic statement creates a variable initialized to Nothing. The Java statement does something equivalent. The C++ statement creates an uninitialized variable. The big difference is that accessing thing in Visual Basic or in Java results in a polite, predictable error, while accessing an uninitialized C++ pointer variable fails rudely and unpredictably.
You can't create an object directly in Visual Basic or Java, but you can in C++:
CThing thing; // C++ declare object
This C++ statement creates a CThing object in memory. Notice that it looks the same as the Java statement shown earlier, but it means something quite different. It works more like a Visual Basic UDT, except that it can have methods as well as data members. You can start calling its methods immediately after you declare it. Some of you (such as those who read the mistaken claims in the last edition of this book) might say that the following Visual Basic variable is equivalent:
Dim thing As New CThing
Not at all. This is just a shortcut that creates an object variable, but delays automatic creation of it to a later time. If you want to create an object from an object variable, you do it like this in Visual Basic:
Set thing = New CThing ' VB creates object
Set creates a CThing object and connects it to the thing variable, and disconnects the thing variable from the object it was previously connected to.
The equivalent Java statement is similar, except that there's no Set:
thing = new CThing(); // Java create object
The nearest equivalent in C++ is this:
pthing = new CThing; // C++ create object and point to it
The C++ new operator allocates storage for a class in much the same way that New does in Visual Basic or new does in Java. The difference is that you have to use the delete operator to get rid of that storage in C++. Visual Basic and Java do the cleanup (sometimes called garbage collection) for you.
At this point you can use the thing variable as if it were a CThing object. You can call the CThing object's methods and use its properties:
thing.Square()
That's the Visual Basic version (the parentheses are optional). The Java version is the same except it needs a closing semicolon. But C++ can create either a pthing pointer variable or a thing variable. These two versions need a different syntax to distinguish them.
thing.Square(); // C++ variable containing an object pthing->Square(); // C++ variable pointing to an object
And that's not even mentioning a third possibility, a C++ reference variable which looks like an object and works like a pointer to an object. But we won't get into that.
I could continue with this comparison, but it would come to more of the same. Java looks like C++, but acts like Visual Basic. There are two kinds of languages, and the thing that separates them is their attitude toward pointers. C++ embraces and honors them. Java refuses to recognize their existence. Visual Basic--well, pointers seem to have squeezed a foot in the door, but only in relation to the Windows API. In the difference that matters most, Visual Basic and Java are on the same side.
Behind the scenes, property access would actually look something like this:
On e <> 0 Goto EHandler ' iLine = txtNote.Line e = txtNote.GetLine(iLine) ' txtNote.Line = iLine + 1 e = txtNote.LetLine(iLine + 1) § EHandler: HandleError e
Fortunately, Visual Basic handles these bookkeeping details so that you don't have to worry about them.
Visual Basic provides numerous ways to create an object and Set it to an object variable. We're going to look at several of them, but first there's one important truth that you should keep in mind at all times when dealing with objects: Visual Basic objects and classes, be they public or private, are all handled through the Component Object Model (COM).
Even in early versions before COM, Visual Basic had controls and forms that were like objects; however, the current implementation uses COM below the surface for everything. (Microsoft was just kidding when it told you not so long ago that you should call COM by the nonsense acronym OLE.)
Let's start with the granddaddy of object creation statements, CreateObject.
In the old days, the only way to define a class was to write it as a COM Automation component in another language (usually C++). You might not think of a COM Automation-enabled application such as Excel or Visio ShapeWorks as a group of classes, but to a Visual Basic program, that's pretty much what it is.
So let's say you have the COM Automation-enabled StoryTeller application and you want to create a CStory object in your Visual Basic program. To do this, you'll call some methods and properties to manipulate stories. (StoryTeller was one of the first COM Automation applications and the first version came out long before type libraries even existed. Stories, Inc., sent you a type library for the old version last year, but you thought it was marketing junk and threw the disk away.) To use a CStory class, you need to declare an object variable:
Dim objStory As Object
You might prefer to declare this object with type CStory or something more specific than Object, but you can't because you don't know the name of the class, and even if you did, you can't use it in Visual Basic at design time without a type library, and you don't yet know there is such a thing. You have to connect to the class at run time using CreateObject. In order to use CreateObject, you must learn the programmatic ID ProgID string of the class either from documentation or from samples provided by the vendor. By convention, the ProgID string consists of the name of the server application followed by a dot, followed by the name of the class. Nothing enforces this convention except the desire of vendors to meet the expectations of users. Fortunately, the StoryTeller documentation tells the ProgID and even gives an example of how to use it:
Set objStory = CreateObject("StoryTeller.CStory") objStory.Declaim 9, "Easy" §
Now let's look at what happens behind the scenes. The CreateObject function looks in the system registry (in HKEY_CLASSES_ROOT) to find the key called StoryTeller.CStory. Under this entry, it finds the CLSID for the CStory class. The CLSID is a 128-bit Globally Unique ID (GUID). (I'll tell you more about GUIDs in Chapter 10.) CreateObject looks up this number in a table of GUIDs under the CLSID key and learns (among other things) the full path of the StoryTeller program, C:\STELLER\STELLER.EXE. Next it checks to see if STELLER.EXE is already running and, if not, runs it. Finally COM binds the CStory class to the objStory variable.
Binding means that the objStory variable is assigned the base address of the code for CStory objects within the running STELLER.EXE program. Whenever your program uses a CStory property or method, COM will look up the pointer for that property or method code as an offset from the base address of the object. You got all that? Well, never mind. We'll be getting into more COM details in Chapter 10.
For now, the important point is that the variable is bound to the class, and it's bound late. You'll hear a lot about early binding and late binding in the rest of this book. Late binding means that the variable is connected to the class at run time. This is evil. If you learn only one thing from this book, it should be how to avoid late binding. Early binding means that the variable is connected to the class at compile time. This is good. You should always try to achieve early binding. Alas, there was no such thing back in the old days when the story server was written.
When you call the Declaim method of the CStory object, Visual Basic has to look up the method by name in the class data table to determine where the Declaim code is located. It has to pass arguments in Variants (whether the parameters are Variant or not) and do various other time-consuming things to get everything set up. It uses a COM interface called IDispatch (which I'm not going to explain) to do all this messy stuff. The important thing is that late binding always goes hand in hand with IDispatch calling. Like late binding, IDispatch calling is--well, perhaps evil is too strong a word--but it's definitely in bad taste. We want to avoid late binding and IDispatch whenever possible.
Let's fast forward. Stories, Inc., has updated its StoryTeller program by providing a type library. You download the type library STORIES.TLB from www.stories.com. Then you load it in the Visual Basic References dialog box. You open the Object Browser and find the class you want. Then you declare a variable for it:
Dim story As CStory Set story = New CStory story.Declaim 9, "Easy" §
This time, something very different happens. When your source file is compiled, Visual Basic looks in its type libraries and finds the CStory class. The class is essentially a data table with offsets to the address of each property and method. These offsets are bound to each property access and method call in your program when the program is compiled. All you need to execute those properties and methods is the base address of the CStory object they are part of. When the Set statement is executed at run time, Visual Basic gets all the information it needs to create a CStory object from the type library instead of from the system registry. This speeds things up a little, but the big savings comes from having looked up all the method and property offsets at compile time.
Let's look at some other ways of getting objects. Here's the most common. Stories, Inc., decides that the StoryTeller application is overkill for many users. What users want is a fast DLL version of StoryTeller's components. This version should have a visual interface for initializing property values, and it should be able to respond to story events, such as audience laughter or boredom. In other words, it should be an ActiveX control. So they write STORYSPINNER.OCX in a language that they won't specify and that is none of your business anyway (but that might be Visual Basic). You click the Toolbox to put this control onto a form. Visual Basic automatically provides hidden code that does the following:
Set spinner = New CStorySpinner
You don't have to Set this variable. It's pre-Set. You also don't have to initialize its properties with code (although you can) because you probably already set them at design time in the property page.
Another way to create COM objects is with forms. You insert a new form into your project and Visual Basic automatically generates something equivalent to the following:
Dim Form1 As New Form1
Like any competent programmer, the first thing you do is go to the Name field of the property page and change the Name property from Form1 to a real name such as FStoryDialog. Visual Basic automatically changes the hidden statement to the following:
Dim FStoryDialog As New FStoryDialog
If you follow my advice, you won't use this weirdly named object variable. Instead, you'll define your own sensible name like this:
Dim dlgStory As FStoryDialog Set dlgStory = New FStoryDialog dlgStory.TaleLength = "Tall" dlgStory.Show vbModal
This way, you specifically control form creation rather than having it done behind your back by hidden statements. But what happens to that FStoryDialog object variable created by Visual Basic? Check the last section where we discussed how New in declarations works. The FStoryDialog object variable is set to Nothing, but if you never use it, no form object is ever created for it.
Finally, we're ready to create COM classes with Visual Basic. Perhaps you don't care for the Stories, Inc., approach, or maybe it's just overkill for your project. To create the CYarn class, insert a class module into your project and name it CYarn. Add the methods and properties you need and use it the obvious way:
Dim yarn As CYarn Set yarn = New CYarn yarn.Spin "That wanders away from the truth"
You can use CYarn as a local class inside the same project, or turn it into an EXE or DLL server. If you like, you can wrap the whole package up as an ActiveX control with property pages and a Toolbox button. These are all manifestations of the same concept: an object whose properties and methods are referenced through an object variable.
When you call a method or property, it will work in different ways, depending on a factor that is mostly beyond your control. Some OLE classes have IDispatch interfaces, some have COM (sometimes called vtable) interfaces, and some have both (dual interfaces). From Visual Basic's viewpoint, IDispatch represents evil and COM represents virtue. Dual interfaces represent a pact with the devil to make do in a world of sin, but you should never need the darker IDispatch side.
This doesn't mean IDispatch is evil in general. You still need it for VBScript and other environments that always do late binding. Some authorities will claim that there are realistic situations where you don't know a server's name until run time and therefore you will need late binding. For example, you might have two polymorphic servers that provide the exact same services in two different ways. You have to ask your user (or perhaps ask an external database or a Web site) whether to use the UglyFast server or the PrettySlow server. These might be completely different servers running on different remote machines. You could write code like this:
If fFastAtAnyPrice Then Set obj = CreateObject("UglyFast.CJumpInTheLake") Else Set obj = CreateObject("PrettySlow.CPlungeIntoThePool") End If
I have yet to see a practical example of this situation under Visual Basic, but I'm told that there are some. Even in cases where you need CreateObject, it might still be possible to get early binding with the new Implements statement.
The IDispatch interface was designed for late-bound objects. It works (and works much faster) with early-bound objects, but it's less flexible. It still filters all arguments through Variants and it still does a certain amount of parsing and general thrashing to find the addresses and specify the arguments of methods and properties. Some of the information it finds is already known by the type library.
The COM interface, by contrast, is simple and direct. It consists of a simple table of addresses for each of the methods and properties. When you call a method, Visual Basic looks up its address and calls it. There are only two steps, and they translate into very few assembly language instructions. The table that makes this possible is called a vtable, and it has the exact same format as the vtable that makes virtual functions possible in C++. The tradeoff is information. If you have information, you construct a vtable and make fast calls through it. If you don't have information, you look up that information the hard way and make slower calls. If you have the information but don't use it, you're wasting your time.
So if COM servers with dual interfaces are better than those with IDispatch interfaces, where do you find dual interfaces? The answer is: almost everywhere. For example, you'll always get them from Visual Basic. In fact, there are only two development tools I know of that create dispatch-only ActiveX components. Unfortunately, one of them is probably the most common tool for creating components: the Microsoft Foundation Class (MFC) library. The other is the Delphi programming environment (version 2). Both Microsoft and Borland are moving to new tools that create dual interfaces.
Lest you think that MFC creates slow, fat COM objects, let me clarify one thing. What has the greatest effect on the speed of any component is its slowest part. If COM calls through IDispatch are the slowest part of a component's operation, then you can blame the provider of that component. Generally, the bottleneck for controls is window creation and management. The bottleneck for COM EXE servers is transfer across process boundaries through a process called marshaling. You'll see performance examples demonstrating this in Chapter 10. The Performance sidebar in Chapter 10 illustrates that a Visual Basic DLL component compiled to native code is significantly faster than an equivalent C++ MFC component. Dual interfaces, not the programming language or the compiler, make the difference.
Problem: If you choose a class solution over an equivalent functional solution, what performance penalty, if any, must you accept? There's no easy one-to-one comparison, but the following information might give you a clue about the relative performance of methods, procedures, properties, and variables. As a bonus, you get a comparison of creating objects with New in the declaration versus using Set with New.
Problem | P-Code | Native Code |
Call method function on object | 0.0476 sec | 0.0090 sec |
Call method function on New object | 0.0466 sec | 0.0096 sec |
Call method function on late-bound object | 1.0185 sec | 0.7828 sec |
Call private function | 0.0438 sec | 0.0071 sec |
Pass variable to method sub on object | 0.0268 sec | 0.0055 sec |
Pass variable to method sub on New object | 0.0287 sec | 0.0071 sec |
Pass variable to method sub on late-bound object | 0.8585 sec | 0.6980 sec |
Pass variable to private sub | 0.0384 sec | 0.0088 sec |
Assign through Property Let on object | 0.0232 sec | 0.0094 sec |
Assign through Property Let on New object | 0.0252 sec | 0.0057 sec |
Assign through Property Let on late-bound object | 0.7661 sec | 0.6229 sec |
Assign through private Property Let | 0.0176 sec | 0.0014 sec |
Assign from Property Get on object | 0.0327 sec | 0.0069 sec |
Assign from Property Get on New object | 0.0301 sec | 0.0068 sec |
Assign from Property Get on late-bound object | 0.8045 sec | 0.6790 sec |
Assign from private Property Get | 0.0172 sec | 0.0005 sec |
Assign to public property on object | 0.0068 sec | 0.0020 sec |
Assign to public property on New object | 0.0058 sec | 0.0019 sec |
Assign to public property on late-bound object | 0.8582 sec | 0.7918 sec |
Assign to private variable | 0.0032 sec | 0.0003 sec |
Assign from public property on object | 0.0079 sec | 0.0018 sec |
Assign from public property on New object | 0.0090 sec | 0.0016 sec |
Assign from public property on late-bound object | 0.9445 sec | 0.8459 sec |
Assign from private variable | 0.0035 sec | 0.0004 sec |
Conclusion: Hard to say. Using classes and objects definitely has a performance cost. Classes that wrap very simple operations might cost more than they are worth, but for most classes, the overhead is tiny compared to the total cost of what the class actually does.
Visual Basic doesn't provide much help in the simple task of analyzing the disk drives in the system, but fortunately, the Windows API does have some functions. GetDriveType, GetDiskFreeSpace, and GetVolumeInformation provide the information but, like most Win32 functions, they're oriented toward C programmers, not Visual Basic programmers.
Let's turn those Win32 functions into a Visual Basic-friendly class. Our drive objects won't simply get information about drives--they will be drives, telling the user everything about the physical drives the objects represent.
When I write a new class, I start by using it before I create it. I write some code using the class as if it existed, and then I declare a few object variables. If the class has a Create method, I call it in different ways to create different objects. Next, I set some object variables to refer to existing objects. I pass objects as arguments to subs and functions. I assign values to or read values from the properties of the objects. Finally, I call the methods.
It's easy and it's fun. I never get design-time or run-time errors. Imaginary objects of imaginary classes can acquire new methods and properties as fast as I can think them up.
But we all know that air code doesn't work. Once I start implementing methods and properties, some of them turn out to be more difficult than I expected. Sometimes I have to cut features or change the design. When I change the implementation, I change the test code to match.
When the implementation gets to a certain point, I start using it, one feature at a time. I comment out most of the client code, and implement some key properties and methods in the server code. Gradually, I uncomment more and more of the client code until everything works. Design is an iterative process. When you actually try to use your implementation, you might find that it's clumsy. Or you might find that your client code wants to do something that can't be done. Often, the process of implementing might give you new ideas for features that users would appreciate.
On major projects, you don't always have the luxury of designing by trial and error. A designer might write a specification describing all the interface elements in detail. The spec is then handed to an implementor, who makes it happen. Interface changes can have major repercussions for everyone involved. Even in this situation, however, the designer follows the same process, if only in his or her imagination. Any design-implementation process that depends on the infallibility of the designer is bound to fall short. I've seen language specs aplenty with sample code that never worked and never could have worked--or, worse yet, specs with no sample code. If you find features that don't quite seem to be designed for programmers on this planet, you can guess that the designer never actually tried out the feature--either in a virtual or a real sense. Unfortunately, we'll soon have to deal with some Visual Basic features that match this description.
We'll follow a use-first-implement-later strategy with the CDrive class, and you'll see a lot more of it throughout the book. Of course, I get to cheat; you won't see all my stupid ideas that got weeded out during implementation.
When I write a new class, I start by creating a test form that exercises the class. I put a command button and either a large label or a TextBox control on the form. Before I start writing the class itself, I declare an object of the proposed class in the command button's Click event procedure and write some code that uses the properties and methods of the class, outputting any status information to the label or the text box. Normally, I name the test program using the name preceded by a T, for example, TDRIVE.FRM to test the CDrive class in DRIVE.CLS. In this case, however, I combined drive testing with testing of other information classes and forms, including an About form. The actual name of the test project is ALLABOUT.FRM in ALLABOUT.VBG. You can see it, with output from CDrive, in Figure 3-2.
Figure 3-2. The Drives tab of the All About program.
To examine the class, let's start by declaring three CDrive object variables:
Dim driveDat As New CDrive Dim driveCur As New CDrive Dim driveNet As New CDrive
At this point, the drive objects have no meaning; they aren't yet connected with any physical disk drive. You must somehow connect each Basic drive object to a real-world drive object.
The modern way of specifying a drive is with a root path--the standard system introduced by the Win32 API. Lettered drives have root paths in the form D:\, and network drives that are not associated with letters have root paths in the form \\server\share\. Notice that root paths always end in a backslash. Our CDrive class will have a Root property as the means of specifying a drive:
driveDat.Root = "a:\" ' Set to lettered drive driveCur.Root = sEmpty ' Set to current drive driveNet.Root = "\\brucem1\root\" ' Set to network drive
Back in the ancient days of MS-DOS, drives were sometimes represented by the numbers 0 through 26 (with 0 representing the current drive) and sometimes by the numbers 0 through 25 (with 0 representing drive A). You needed to read the documentation carefully to see which system to use for any MS-DOS function call. One of the reasons for using a class is to eliminate such inconsistencies. The CDrive class recognizes numeric Root property values, with the number 0 representing the current drive and the numbers 1 through 26 representing local drives.
You can initialize a drive as follows:
driveDat.Root = 1 ' Set to A: driveCur.Root = 0 ' Set to current drive
Of course, in Basic a class can have a default property, and the obvious one for the CDrive class is Root:
driveDat = 1 ' Set to A: driveCur = 0 ' Set to current drive driveDat = "a:\" ' Set to lettered drive driveCur = sEmpty ' Set to current drive driveNet = "\\brucem1\root\" ' Set to network drive
If you don't like any of those, you don't have to set anything. By default, the Root property will be sEmpty and you'll get the default drive.
Once the drive is initialized, you can read its Root property (which has been converted to the root path format) or its Number property. The Number property is 0 for network drives:
' Assume that current drive is C: Debug.Print driveDat.Number ' Prints 1 Debug.Print driveCur.Number ' Prints 3 Debug.Print driveNet.Number ' Prints 0 Debug.Print driveDat.Root ' Prints "A:\" Debug.Print driveCur.Root ' Prints "C:\" Debug.Print driveNet.Root ' Prints "\\BRUCEM1\ROOT\"
Of course, what you really want is the drive data in the form of read-only properties FreeBytes, TotalBytes, Label, Serial, and KindStr. You can use them this way:
Const sBFormat = "#,###,###,##0" With driveCur s = s & "Drive " & .Root & " [" & .Label & ":" & _ .Serial & "] (" & .KindStr & ") has " & _ Format$(.FreeBytes, sBFormat) & " free from " & _ Format$(.TotalBytes, sBFormat) & sCrLf End With
This code displays information in the following format:
Drive C:\ [BRUCEM1:1F81754F](Fixed) has 3 bytes free from 1,058,767,000
The CDrive class is designed with the assumption that drive objects don't change their properties. In real life, of course, drives sometimes do change. You might remove a 14.4 megabyte disk from your A drive and insert a 7.2 megabyte disk. You might disconnect a large network drive from your O drive and connect a small one. CD-ROM technology might change while your program is running. You can make sure that a drive object reinitializes its state from the drive it represents by setting the object variable to a new drive object or by assigning the Root property (even if you reassign the current value).
This is a good time to introduce the class notation that I'll use throughout the rest of this book. Class notations are a dime a dozen in object-oriented programming books, and I don't claim that my notation has any special advantages. It does address the specific features of Visual Basic classes in a way that can either show off the methods and properties of a specific class or, at a higher level, indicate relationships between classes. Figure 3-3 shows the diagram for the CDrive class.
Figure 3-3. The CDrive class.
Types are shown for methods and properties in this example, but they might be omitted in later examples. If it's useful to show private members of a class, they'll go inside the object box. (This is seldom necessary, however; normally, the public interface is all that matters.)
Now that you know exactly what you want, you can start to create the CDrive class. You can declare a class to be either private or public. I provide a public version in the VBCore DLL project. (VBCore is discussed in more detail later.) CDrive can just as easily be private. I provide a separate, private copy of the CDrive class in a file called PDRIVE.CLS. If you want to use the private version, put PDRIVE.CLS into your project rather than DRIVE.CLS.
There are two ways to define a property. The first way looks like a variable declaration:
Public Root As String
But if you define the Root property this way, you lose all control of it. The user can set or read it at any time without your knowledge. In CDrive, you need to do a lot of work whenever anyone sets the Root property. First you must confirm that the user has passed an actual root path. Next you calculate and store permanent data about the drive as soon as it is initialized. Then, when the user asks for the information later, you return the pre-calculated values.
The mechanism that makes this possible is the property procedure. Property Get, Property Let, and Property Set enable you to write procedures that look like public variables.
There are generally two strategies for defining properties, and CDrive follows them both. For properties that don't change dynamically, it does all the initialization and calculation up front. In other words, the Property Let procedures do all the work and the Property Get procedures simply return pre-calculated internal data. But some data about a drive might change from moment to moment (for example, the number of free bytes). You want to recalculate these dynamic properties each time their Property Get is called. In other words, the Property Get procedure does all the real calculation and validation, while the Property Let does the least work it can get away with.
First let's look at the private variables that CDrive will have to fill:
Private sRoot As String Private edtType As EDriveType Private iTotalClusters As Long Private iFreeClusters As Long Private iSectors As Long Private iBytes As Long Private sLabel As String Private iSerial As Long Private fDriveMissing As Boolean
The key property for users is Root because it is the default property. Users will normally set it before they set anything else. The Property Let looks like this:
Public Property Let Root(vRootA As Variant) ' Some properties won't work for \\server\share\ drives on Windows 95 sRoot = UCase(vRootA) ' Convert to string InitAll End Property
Clearly, the key procedure from the programmer's standpoint is InitAll. This function gets called when a CDrive object is created (in Class_Initialize) and each time the user sets the Root property. InitAll looks like this:
Private Sub InitAll() sLabel = sEmpty: iSerial = 0 iSectors = 0: iBytes = 0: iFreeClusters = 0: iTotalClusters = 0 fDriveMissing = False ' Empty means get current drive If sRoot = sEmpty Then sRoot = Left$(CurDir$, 3) ' Get drive type ordinal edtType = GetDriveType(sRoot) ' If invalid root string, try it with terminating backslash If edtType = edtNoRoot Then edtType = GetDriveType(sRoot & "\") Select Case edtType Case edtUnknown, edtNoRoot Dim iDrive As String iDrive = Val(sRoot) If iDrive >= 1 And iDrive <= 26 Then sRoot = Chr$(iDrive + Asc("A") - 1) & ":\" Else sRoot = sEmpty End If ' Start over InitAll Case edtRemovable, edtFixed, edtRemote, edtCDROM, edtRAMDisk ' If you got here, drive is valid, but root might not be If Right$(sRoot, 1) <> "\" Then sRoot = sRoot & "\" GetLabelSerial Case Else ' Shouldn't happen BugAssert True End Select End Sub
That might look like a lot of work, but essentially this procedure is the CDrive class. It calculates almost everything necessary to allow the Property Get procedures simply to read internal variables.
Notice the recursive call to InitAll when the drive type is unknown or invalid. This happens if the user sets the Root property to a bogus value, such as an empty string. In other words, you have an error. Or have you?
There are many ways to handle errors in classes, but one choice is to refuse them. If you accept any input (including no input), there can't be any user error. If something goes wrong, it's the programmer's fault. The CDrive class attempts to follow this strategy. For example, if you enter the string invalid drive as the Root property, CDrive will first interpret it as a root drive string. If that fails (and it will), CDrive will interpret it as a drive number. If that fails (and it will), CDrive will interpret it as the current drive, and there's always a current drive. We'll look at other error strategies later.
The Kind, Number, and Root Property Get procedures can return pre-calculated data:
Public Property Get Kind() As EDriveType Kind = edtType End Property Public Property Get Number() As Integer Number = Asc(sRoot) - Asc("A") + 1 ' Network drives are zero If Number > 26 Then Number = 0 End Property Public Property Get Root() As Variant Root = sRoot End Property
The remaining properties can change dynamically, and shouldn't be pre-calculated. Instead, the calculations are done in the Property Get procedures:
Public Property Get FreeBytes() As Double ' Always refresh size since free bytes might change GetSize If Not fDriveMissing Then FreeBytes = CDbl(iFreeClusters) * iSectors * iBytes End If End Property Public Property Get TotalBytes() As Double ' Get size info only on first access If iTotalClusters = 0 And Not fDriveMissing Then GetSize If Not fDriveMissing Then TotalBytes = CDbl(iTotalClusters) * iSectors * iBytes End If End Property Public Property Get Label() As String If Not fDriveMissing Then Label = sLabel End Property Public Property Get Serial() As String If Not fDriveMissing Then Serial = MUtility.FmtHex(iSerial, 8) End Property Public Property Get KindStr() As String KindStr = Choose(edtType + 1, "Unknown", "Invalid", "Floppy", _ "Fixed", "Network", "CD-ROM", "RAM") If fDriveMissing Then KindStr = KindStr & " Missing" End Property
The FreeBytes and TotalBytes property procedures depend on GetSize, which calls the Win32 GetDiskFreeSpace Function and sets the fDriveMissing flag:
Private Sub GetSize() Call GetDiskFreeSpace(sRoot, iSectors, iBytes, _ iFreeClusters, iTotalClusters) fDriveMissing = (Err.LastDllError = 15) End Sub
Notice that TotalBytes and FreeBytes return Double rather than Long (as I originally coded them). When I wrote the original CDrive for the first edition of this book, disks larger than 2 gigabytes didn't exist and I didn't encounter one until I tested CDrive with a network server. Now 2 gigabyte drives are common.
The Label, Serial, and KindStr property procedures call GetLabelSerial to retrieve the appropriate disk information and to determine whether the disk is present. GetLabelSerial is a wrapper for the GetVolumeInformation API function:
Private Sub GetLabelSerial() sLabel = String$(cMaxPath, 0) Dim afFlags As Long, iMaxComp As Long Call GetVolumeInformation(sRoot, sLabel, cMaxPath, iSerial, _ iMaxComp, afFlags, sNullStr, 0) If Err.LastDllError = 21 Then ' The device is not ready fDriveMissing = True Else fDriveMissing = False sLabel = MUtility.StrZToStr(sLabel) End If End Sub
TotalBytes, FreeBytes, Serial, Kind, and KindStr have no Property Let statements because you can't directly change the size or type of a disk. To make a property read-only, you define the Property Get procedure but not the Property Let. You can change the Label, and CDrive provides a Property Let to do so:
Public Property Let Label(sLabelA As String) If SetVolumeLabel(sRoot, sLabelA) Then sLabel = sLabelA End Property
A method is simply a public sub or function inside a class (or form) module. CDrive has no methods, only properties. But it's easy enough to add one:
Public Sub Format() Shell Environ$("COMMSPEC") & " /c FORMAT " sRoot, vbHide End Sub
No! Wait! It's a joke. It's not in the sample program. Look, it has a syntax error; it won't work even if you type it in yourself. I'm not trying to reformat your hard disk.
Visual Basic's support of default members is one of the features that distinguishes it from other languages. Historically, Visual Basic supported default members on controls back in version 1--long before the Component Object Model borrowed the feature. Unfortunately, although Visual Basic recognized default members in controls or classes written in other languages, it didn't support creating your own default members until version 5. And it still doesn't support default members directly through the language.
The only way to set the default member for your class is through a dialog box that seems to have been carefully designed for the sole purpose of violating as many user-interface principles as possible. Visual Basic calls the dialog box shown in Figure 3-4 the Procedure Attributes dialog box. I call it the DBFH (Dialog Box From Hell). It's kind of embarrassing even to describe this feature, and an advanced programming book shouldn't have to explain specific fields of a dialog box. But there's no getting around it.
Figure 3-4. The Procedure Attributes dialog box (a.k.a., the Dialog Box From Hell).
The first step in setting the default property is to choose the best one. It's not always obvious. If no member waves its hand and hollers "pick me," you might be better off skipping the default member rather than picking one at random. That's not the case with the CDrive class; the Root property is clearly the right choice. You can initialize the object by setting it, or you can read it to get the most important element of the object:
driveCur = "c:\" ' Initialize a drive Debug.Print driveCur ' Print the drive name
Having chosen the Root property as the default, you might expect to be able to set the default property with a Default keyword or some other language feature. Nope. You have to do it with the dialog box that sets all the attributes of your methods and properties. The key attribute that you ought to provide for every single member of public classes is the description. The description will appear in the Object Browser, and your customers will consider you rude if you don't provide it. The previous version of Visual Basic had a way to set the description through the Object Browser, but it was so obscure and unintuitive that many users never figured it out. It's a little easier in version 5. You should get in the habit of providing descriptions even if you don't set default properties. Do as I say, not as I do.
Since you're going to be using this dialog box for every single member, let's start it off right. Put the cursor on the chosen property and press the accelerator key…oops. Now why would there not be an accelerator key or toolbar button for a dialog box you were going to call continually? Well, at least you can customize the toolbar and context menus to make access easier.
OK, so you choose Procedure Attributes from the Tools menu. The top half of the dialog box in Figure 3-4 appears. At this point, you can enter the description. In real life, you would also enter the Help Context ID. There's also a field for the project help file, but you must enter the help file elsewhere--in the Project Properties dialog box.
There's no visible place to specify that this is the default property. You must click the Advanced button to set a default member. Is setting a default member an advanced operation? Apparently someone thinks so.
When you click the Advanced button, the other half of the dialog box appears and you see several fields that have absolutely nothing to do with what we're doing. It turns out that this dialog box was designed for creators of controls, and since we're creating a class, not a control, most of the fields are irrelevant. Of course, Visual Basic knows whether we're creating a control or a class, so you might expect it to disable the extraneous fields. Nope. The dialog box lets you set a class property to be data bound or to have a property category even though these features have absolutely no effect on classes.
You might notice a checkbox that says User Interface Default. Perhaps checking this will make the current property the default. Don't even think it. This checkbox, too, is for control developers and will have no effect, although you can set it to your heart's content. Can you guess which of the gadgets in the dialog box actually sets the default property? OK, I'll give you a hint. Internally, in a way that is completely hidden from and irrelevant to Visual Basic programmers, the Component Object Model decides which class member is the default by looking at a variable that assembly language programmers would call an ID (even C++ programmers don't have to know this). Yes, you guessed it. The Procedure ID combo box is where you set the default property.
When you click the down arrow of the combo box, you'll see a list of random words. The first two, None and Default, are in parentheses for reasons that I couldn't even guess. The obvious choice is to select Default, and in fact that happens to be correct. But just out of curiosity, let's take a look at some of the other choices. What do you think it would mean to set the Root property of the CDrive class to BackColor or hWnd? If you guessed that it would have no effect whatsoever, you win a trip to Disneyland and the honorary title of Visual Basic Dialog Box Designer for a Day.
No matter what object-oriented language you use, you will probably run into a common naming problem. Often, you must assign different names to variables that represent the same value. For example, assume that you are defining a FileName property. The program using the property might have a variable containing a filename that it wants to assign to the FileName property. A user following the Hungarian naming convention described in Chapter 1 ("Basic Hungarian," page 16) might name the variable sFileName. When a user assigns that value to the property, the value is actually passed as an argument to the Property Let procedure. You need a name for the filename parameter; sFileName springs to mind. Now you need to store the same value internally. Again, sFileName is the obvious choice for the name of the internal version. And what about the property name itself? From the outside, the property looks like a variable, smells like a variable, and tastes like a variable. Why shouldn't it have a variable name? How about sFileName?
In practical terms, using the same name for the external and internal variables is not a problem. The external variable has a different scope than all the others and might be written by a different programmer years later; therefore, similar names are OK.
Nevertheless, following the Hungarian naming convention for properties is usually a bad idea. Your code ends up as gobbledygook: thgMyThing.nMyCollection.iProperty. Besides, your code will then be using a convention very different from the one used in controls, forms, and other predefined objects. Generally, it's bad manners to impose your own naming conventions on the outside world.
A conflict still exists between the parameter name in the Property Let procedure and the name in the internal version. You must arbitrarily make these different, even though they represent the same thing. You can mangle the internal version in one of two ways. The MFC class library for Visual C++, for instance, always prefixes the internal member variables in a similar situation with m_ (as in m_sFileName). But I have an irrational prejudice against underscores in variable names, particularly in front of Hungarian prefixes; I'd much rather use a postfix. I considered using I (for internal) for all my internal variables, but I eventually switched to using A (for argument) with all parameter variables in Property Let statements. That way, the modified name doesn't affect Property Get procedures.
Of course, if you find anything about this dialog box confusing, no problem. Just click the Help button. Or click the What's This icon. Oops! There is none. At least you can press F1 to get help that will attempt to explain the mess. But you haven't seen anything yet. Just wait until we talk about creating collections in Chapter 4.
The CDrive class doesn't exercise all the features of classes. I'll give a preview of some other features that we'll be covering as I get to them.
In most object-oriented languages, an object can share data with other objects of the same class. For example, each object might need a count of how many other objects of the same type exist. An object might behave differently if it is the last object, or it might refuse to be created if there are too many objects. Visual Basic does not provide a direct means of sharing data between instances, but you can fake it using Public variables in a standard module. There is only one instance of a Public (global) variable, and all instances of a class can access it. But so can any other module in your project. In other words, this system violates all standards of encapsulation decency. It works, but only if everyone behaves themselves.
Do you trust other people to always behave? Do you trust yourself? Neither do I.
Now, in defense of using Public variables in standard modules to communicate between instances, keep in mind that these variables are visible only inside a component. If you behave yourself inside your component, users of the component won't be able to get inside and find those Public variables that aren't supposed to be public. Of course, if you're using a bunch of private classes inside an EXE, you are the component and everything is visible. You just have to remember that Public isn't supposed to mean public.
The other disadvantage is that you end up with a lot of dummy standard modules that serve no purpose other than to get around limitations of the encapsulation model. You'll be seeing a lot of this in coming sections.
Often, the best way to implement an object-oriented algorithm is with two or more cooperating classes. These classes need to share data with each other, but not with other classes. Many object-oriented languages provide a Friend modifier to allow classes to share data with each other and Visual Basic is no exception.
Unfortunately, Visual Basic's interpretation of friends is pretty loose. Most languages that support the Friend concept allow you to say who your friends are. Visual Basic only allows you to say that you have friends. If anybody is your friend, then everybody is your friend. Or, more specifically, everyone in your component is your friend and everybody else doesn't know you. Again, this works fine if everybody in your component behaves. But the implicit assumption is that you won't put totally unrelated classes in the same component. Well, guess what the VBCore component does?
We'll be using the Friend keyword to share data between classes when we get to the CRegNode and CRegItem classes in Chapter 10. The Friend keyword can also be used to make class data available to standard modules and forms within the component. You can see this in the CTimer class (TIMER.CLS).
Visual Basic now allows you to declare events in your server with the Event statement and raise them with the RaiseEvent statement. Your client programs can receive events from a server using the WithEvents statement. That's the easy way.
You can also create events by defining an interface in the server and having the client implement the interface. The client must also pass a reference from the implementing object to the server so that the server can call the client, thus creating the events. That's the efficient way.
Either way, events are an important new class feature, but we'll temporarily skip them.
Forms are just classes with windows. Think of it this way: somewhere in the heart of the source code for the Basic language lives a module named FORM.CLS.
You didn't know that Visual Basic is written in Visual Basic? Well, I have inside information (incorrect, like most inside information) that Visual Basic is actually written in Visual Basic. The FORM.CLS module contains Property Let and Property Get statements for AutoRedraw, BackColor, BorderStyle, Caption, and all the other properties you see in the Properties window. It has public subs for the Circle and Line methods and public functions for the Point method. It defines events with the Event statement. All forms automatically contain a hidden WithEvents statement for their own events and one for each control on the form.
The FORM.CLS module didn't change much in Visual Basic versions 1 through 3--a few new properties here, a few new methods there. But for version 4, somebody got the bright idea that if a form is just a class, users should be able to add their own properties and methods. By customizing a form with properties and methods, you make it modular. It's easy to define your own standard forms that can be called from any project.
The With statement--stolen from Pascal and modified to fit Basic--is a fancy form of Set. The primary purpose is to make it easier and more efficient to access nested objects. For example, consider this With block:
With Country.State.City.Street.Houses("My") .Siding = ordAluminum .Paint clrHotPink .LawnOrnament = "Flamingo" End With
This is equivalent to the following:
Dim house As CHouse Set house = Country.State.City.Street.Houses("My") house.Siding = ordAluminum house.Paint clrHotPink house.LawnOrnament = "Flamingo"
The With version is more readable, and you don't have to declare the reference variable. Instead, a hidden one is created for you. Of course, you don't absolutely need With or Set. You can do the same thing this way:
Country.State.City.Street.Houses("My").Siding = ordAluminum Country.State.City.Street.Houses("My").Paint clrHotPink Country.State.City.Street.Houses("My").LawnOrnament = "Flamingo"
This code is not only harder to read but is also much less efficient. Internally, Basic must look up every nested object for every access.
When you're not using nested objects (and we're not, in this chapter), the With statement is mostly syntactical sugar. It might save you some line wrapping, particularly if you need to access an object more than once in a line:
With lstHouses .ItemData(.ListIndex) = Len(.Text) End With
You might find that more readable than this:
lstHouses.ItemData(lstHouses.ListIndex) = _ Len(lstHouses.Text)
But you won't see a large difference in performance. In fact, the With statement can actually slow you down in some cases, although usually not enough to change my code style.
The only problem with custom forms is that they can't be public. You can't expose your standard forms in components. This turns out to be an advantage. Forms have all sorts of properties and methods that you don't want clients messing with. If anybody can do anything with your standard form, it's not standard. The way you expose standard forms in components is to wrap them in a class. The class initializes and controls the form by manipulating the appropriate methods properties, but it blocks clients from doing so except through properties and methods you control. We'll get to an example of this with a standard About form in Chapter 11.
Interfaces are the hottest concept in language design. It's not as if they're new. COM has had interfaces all along, and Visual Basic version 4 had them behind the scenes. They're very similar to what C++ calls abstract base classes. But things really started popping when Java incorporated interfaces as a language feature. In fact, the Java literature claims that interfaces are a better replacement for the much maligned concept of multiple inheritance.
Who am I to say that Visual Basic copied Java interfaces? But the new Implements keyword provides essentially the same feature. (Coincidentally, a very similar feature appears in the latest version of Delphi.) Visual Basic's Implements keyword has a very different syntax than Java's interface and implements keywords. The Visual Basic version looks a lot like the event syntax, and requires setting extra object variables in situations where Java gets by with type casting.
It's ironic that Visual Basic gets a feature designed to replace multiple inheritance before it even gets single inheritance. Of course, if avoidance of irony were our chief goal, we wouldn't choose programming as a profession. In any case, the purpose of Implements is not multiple inheritance, but polymorphism.
Visual Basic version 4 had polymorphism, but the price was exorbitantly high. In order to use polymorphism, clients of polymorphic classes had to receive them as Objects. This worked, but it always resulted in minimal type protection, late binding, and slow operations. I described this crude version of polymorphism in the first edition of this book, but warned against using it for operations that required high performance. The new Implements keyword gives you safe, fast polymorphism. Let's check it out.
Interfaces are one of the most important foundations of the Component Object Model. You can't read more than a page or so of COM documentation without running into IReadThisBook, IDontUnderstand, or some other interface. By convention, interface names always start with the capital letter I followed by initial-cap words that describe the interface.
Actually, interfaces exist whether you use this convention or not. In fact, every Visual Basic class or form you create has a hidden interface that directly violates the standard. For example, if you create a class called CCallMeIshmael, Visual Basic will create an interface for it called _CCallMeIshmael.
I won't go into all the details of how interfaces work, but I can recommend a good tool for exploring them on your own. The OleView program provided in the \TOOLS\OLETOOLS directory of the Visual Basic CD-ROM will tell you all you ever wanted to know about classes (coclasses to COM) and interfaces. It's interesting to see the COM view of classes that you're familiar with. For example, load OleView, click the View TypeLib button, and open VBCORE.DLL. Expand the CoClasses list and then expand the CDrive coclass.
The resulting information is a little overwhelming, but you can see that the CDrive coclass has a _CDrive interface. The _CDrive interface shows the names, parameter types, and return type of methods and properties, but it doesn't give any hints about the implementation. You can change the implementation of a class and clients won't care in the least as long as you don't change its interface. In fact, you can have several different classes with completely different implementations, but as long as they have the same interface, you can use them interchangeably. That's polymorphism.
Interfaces are the COM way (and the Visual Basic way) of providing polymorphism. Let's define an interface for polymorphic filter classes. Filters are the class equivalent of MS-DOS filters such as SORT or MORE. The role of a filter is to convert data from one form to another by applying a set of rules.
The filter we'll define consists of two parts. It must iterate through lines of text, reading them from a source and writing them to a target. This code works the same regardless of what the filter does. The filter must also apply the transformation to each line. This code is different for every kind of filter. It's a familiar problem. One part of the code is generic; one part is specific. You want to reuse the generic part but provide different implementations of the specific part. So you write an interface for the generic part and you write classes that implement the interface for the specific part. The generic code uses the objects with specific implementations without knowing or caring what those objects do or how they do it. All that matters is that the objects match the interface.
This technique turns object-oriented programming on its head, as shown in Figure 3-5 on page 152. The primary purpose of the CDrive class discussed earlier was encapsulation. All implementation details of a particular operation are hidden within the class, and many different users call the same class in different ways to get different results. With polymorphism, you implement multiple versions of a class with the same interface. One user calls the standard interface of any of the class implementations to get different results.
Figure 3-5. Encapsulation and polymorphism.
Another way to look at interfaces is that they provide one more level of abstraction. Interfaces encapsulate classes in the same way that classes encapsulate members and methods. Just as a class hides the internal state of its objects, an interface hides the implementation of its classes. If that leaves you more confused than before, well, let's get to the examples.
I use filter classes to implement wizard programs. For example, the Bug Wizard program uses the CBugFilter class to transform assertion and profile statements. The Global Wizard program uses several different classes to transform global classes into standard modules and vice versa. All these classes implement the IFilter interface.
In the filter problem, the generic part is the FilterTextFile or the FilterText procedure (in FILTER.BAS). FilterTextFile applies a filter to a text file specified by name in the Source and Target properties of the filter. FilterText works similarly except that the actual text to be transformed (rather than the name of a file containing the text) is passed in the Source and Target properties. Both of these procedures work the same no matter what the filter does. The specific part is the filter class (CBugFilter, one of the global filter classes, or your own favorite filter class). The filter class implements the generic IFilter interface with class-specific code that analyzes each line of text and modifies it according to its own rules.
In languages with inheritance, the generic part of the algorithm (the FilterTextFile or FilterText procedure) might be provided as a non-virtual method in a base class that also contains virtual methods for the specific part. Filter classes would inherit the FilterTextFile or FilterText method from the base class and use it without change, but would implement the virtual methods. You use a different strategy in Visual Basic, mixing functional and object-oriented techniques. The generic FilterTextFile and FilterText procedures go in a standard module. They take IFilter parameters to receive the object that provides the application-specific part of the algorithm. There are many other ways to use polymorphism, and we'll see some of them in later chapters.
To create an interface in Visual Basic, you simply create a class module with empty methods and properties. Here's an IFilter interface (in IFILTER.CLS) that defines the standard members of a filter. IFilter has Source and Target properties to specify the data to be processed. It has a Translate member to do the work. And, in this case, there's an EChunkAction Enum type that indicates the results of the Translate method:
Enum EChunkAction ecaAbort ecaSkip ecaTranslate End Enum Property Get Source() As String End Property Property Let Source(sSourceA As String) End Property Property Get Target() As String End Property Property Let Target(sTargetA As String) End Property Function Translate(sChunkA As String, ByVal iChunkA As Long) As EChunkAction End Function
Nothing to it. Literally. An interface has no code because it doesn't do anything. You have to create at least one separate class that implements the interface. The interface itself doesn't care how the members work, or even whether they work.
Since IFilter is an interface class, I use the convention of starting the names of such classes with an uppercase I. IFilter is what you would call an abstract base class in many object-oriented languages. Its purpose is to provide a type that your program can bind to at compile time, even though you won't know the actual object type until run time.
Now let's look at the generic FilterTextFile procedure (in FILTER.BAS). Notice that it takes an IFilter parameter:
Sub FilterTextFile(filter As IFilter) BugAssert filter.Source <> sEmpty ' Target can be another file or replacement of current file Dim sTarget As String, fReplace As Boolean sTarget = filter.Target If sTarget = sEmpty Or sTarget = filter.Source Then sTarget = MUtility.GetTempFile("FLT", ".") fReplace = True End If ' Open input file On Error GoTo FilterTextError1 Dim nIn As Integer, nOut As Integer nIn = FreeFile Open filter.Source For Input Access Read Lock Write As #nIn ' Open target output file On Error GoTo FilterTextError2 nOut = FreeFile Open sTarget For Output Access Write Lock Read Write As #nOut ' Filter each line On Error GoTo FilterTextError3 Dim sLine As String, iLine As Long, eca As EChunkAction Do Until EOF(nIn) Line Input #nIn, sLine iLine = iLine + 1 eca = filter.Translate(sLine, iLine) Select Case eca Case ecaAbort GoTo FilterTextError3 ' Stop processing Case ecaTranslate Print #nOut, sLine ' Write modified line to output Case ecaSkip ' Ignore Case Else BugAssert True ' Should never happen End Select Loop ' Close files On Error GoTo FilterTextError1 Close nIn Close nOut If fReplace Then ' Destroy old file and replace it with new one Kill filter.Source On Error Resume Next ' No more errors allowed Name sTarget As filter.Source ' If this fails, you're in trouble BugAssert Err = 0 End If Exit Sub FilterTextError3: Close nOut FilterTextError2: Close nIn FilterTextError1: MErrors.ErrRaise Err End Sub
If you study the code carefully, you can see that it does not depend in any way on what the filter actually does. FilterTextFile just loops through each line of Source, calling the Translate method on each line and writing the result to Target. It doesn't matter what file names Source or Target contain or what Translate does to the text.
If you have read the previous edition of this book, you might remember that the old version of FilterTextFile took an Object parameter rather than an IFilter parameter. The description of the old FilterTextFile was largely a diatribe on what real object-oriented languages did and how that compared to Visual Basic's pitiful version of polymorphism. Well, that flame is gone. Visual Basic's version of polymorphism didn't come out the way I expected. But, the functionality is there, and…well, judge for yourself.
Now that FilterTextFile takes an IFilter, you can't just pass a form or a ListBox or some other random object. Any object you pass must have a class that implements IFilter. Passing an inappropriate object causes a polite error at compile time rather than a rude one at run time. More importantly, Visual Basic can bind the IFilter calls at compile time, making polymorphic classes almost as fast as non-polymorphic ones. That's a big turnaround from version 4 where polymorphic algorithms were often as much as 10 times slower than comparable non-polymorphic algorithms.
I've shown FilterTextFile here because it's more interesting, but I'll be using FilterText in some of the examples. FilterText does a similar operation, but it assumes that Source and Target contain a text string with each line of text separated by a carriage return/line feed combination. The code just grabs lines of text from Source, filters them, and writes the result to Target. The Bug Wizard uses FilterTextFile and the Global Wizard uses FilterText for reasons that need not concern us. There's nothing sacred about Source and Target. You could write filter procedures that assume Source and Target are URLs or database record identifiers.
Let's begin the process with a simple implementation of the filter class in Bug Wizard, CBugFilter. The key statement in an interface implementation is the Implements statement. At first glance, there's not much to it:
' CBugFilter implements IFilter Implements IFilter
The interesting thing about this statement is not what it looks like, but what it does to the code window containing it. The object dropdown gets a new entry for IFilter. If you select IFilter, the procedure dropdown shows the event procedures that you must implement. When you select the Source property get procedure, the IDE automatically puts the cursor on the property method, if one exists, or creates a Source property get procedure if one doesn't exist. The IDE creates the following procedure and puts the cursor on the blank line in the middle, ready for you to type in the implementation code:
Private Property Get IFilter_Source() As String End Property
Interface procedures look like event procedures because, like event procedures, you can't change their names. But unlike event procedures, you can't ignore them. You must implement each and every member defined by the interface. By using the Implements keyword, you are signing a contract. You agree to implement all members exactly as they are defined in the interface. Try violating the contract. Change IFilter_Target to IFilter_Targot, or change the return type of IFilter_Translate to Integer. When you try to compile, Visual Basic will inform you in no uncertain terms that you are a cad and a deceiver and that you must fulfill your obligations if you wish to continue.
Here's the interface part of the implementation I gave my CBugFilter class:
' Implementation of IFilter interface Private sSource As String, sTarget As String Private Property Get IFilter_Source() As String IFilter_Source = sSource End Property Private Property Let IFilter_Source(sSourceA As String) sSource = sSourceA End Property Private Property Get IFilter_Target() As String IFilter_Target = sTarget End Property Private Property Let IFilter_Target(sTargetA As String) sTarget = sTargetA End Property Private Function IFilter_Translate(sLine As String, _ ByVal iLine As Long) As EChunkAction IFilter_Translate = ecaTranslate ' Always translate with this filter Select Case eftFilterType Case eftDisableBug CommentOut sLine, sBug Case eftEnableBug CommentIn sLine, sBug Case eftDisableProfile CommentOut sLine, sProfile Case eftEnableProfile CommentIn sLine, sProfile Case eftExpandAsserts ExpandAsserts sLine, iLine Case eftTrimAsserts TrimAsserts sLine End Select End Function
Source and Target are unvalidated properties that you would normally implement with a Public member rather than with separate property procedures. The Implements statement won't let you take that shortcut. If you look back at the IFilter class, you'll notice that it also doesn't take the Public member shortcut, although it could. You might want to experiment with Public members in interface classes, but don't be surprised if Visual Basic makes choices about parameter names and ByVal attributes that you wouldn't make yourself. By defining the Property Get and Let procedures yourself, you get better control over the code that will be used in the implemented class.
There's no room for creativity in how you declare interface members in the implementing class. The creativity comes in the implementation of members such as the Translate function. Notice how the sLine parameter is passed by reference so that the modified version will be returned to the caller, but iLine is passed by value so that the caller's copy can't be changed.
As for what Translate actually does, it calls other procedures that do the actual work. If you reread the description of the Bug Wizard program, you can probably guess most of what these procedures are actually doing. The decision of which procedure to use is based on another property that is part of CBugFilter but not part of IFilter:
Property Get FilterType() As EFilterType FilterType = eftFilterType End Property Property Let FilterType(eftFilterTypeA As EFilterType) If eftFilterTypeA >= eftMinFilter And _ eftFilterTypeA <= eftMaxFilter Then eftFilterType = eftFilterTypeA Else eftFilterType = eftMaxFilter End If End Property
This property uses an EFilterType Enum and an eftFilterType member variable (not shown). Next we'll see how a client can use both the IFilter properties and the CBugFilter properties.
We have an interface. We have a class that implements it. Now for the tricky part: the code that actually uses the filter object. The Bug Wizard program has a control array of command buttons whose indexes correspond to the filter types in the EFilterType enum. The code to perform the appropriate filter action on a text file looks like this:
Private Sub cmdBug_Click(Index As Integer) HourGlass Me ' CBugFilter part of object Dim bug As CBugFilter Set bug = New CBugFilter ' IFilter part of object Dim filter As IFilter Set filter = bug ' Set FilterType property on bug variable bug.FilterType = Index ' Set Source property on filter variable filter.Source = sFileCur ' Pass either variable to FilterTextFile #If fOddDayOfTheMonth Then FilterTextFile bug #Else FilterTextFile filter #End If HourGlass Me End Sub
If you're like me, you might have to study this code for a few minutes before you get it. Notice that there's only one object (because there's only one New statement), but there are two object variables that refer to that object--and each object variable has a different type. You set the filter variable to the bug variable. This works because a CBugFilter is an IFilter (because it implements IFilter).
Next we set the properties. We set the FilterType of the bug object. We set the Source of the filter object. It wouldn't work the other way around because a CBugFilter doesn't have a Source, and an IFilter doesn't have a FilterType. Notice that we use filter as if it had both public Source and Target properties, even though we know the real properties in CBugFilter are private and that their names are IFilter_Source and IFilter_Target.
When you get ready to pass the object to the FilterTextFile function, however, it doesn't matter which object variable you pass because the FilterTextFile function is defined to take an IFilter parameter. You can pass the filter object variable because it has the same type as the parameter. But you can also pass the bug object variable because it is also an IFilter (because CBugFilter implements IFilter). It doesn't work the other way around. An IFilter is not a CBugFilter. It's the object itself, not the object variable, that has type CBugFilter. This is an interesting concept--an object with a different type than the object variable that references it.
I can't say that I find this intuitive. The virtual method syntax used in most other object-oriented languages seems more intuitive. But the syntax grows on you the more you use it.
I do have one complaint. Visual Basic lacks class type casting. Most object-oriented languages allow you to cast the outer type to the inner type. Instead of creating a separate filter variable, you could cast the bug variable to an IFilter like this:
' Set Source property on filter part IFilter(bug).Source = sFileCur
Chapter 10 will tell how to fake this syntax.
To get another view on this strange phenomenon, let's look at another implementation of IFilter. Like Bug Wizard, Global Wizard does several conversion operations. But unlike Bug Wizard, it handles each operation with a separate IFilter class rather than a Select Case block.
We'll look at a typical global conversion class, CModGlobDelFilter. The Source and Target properties are implemented exactly the same as for CBugFilter, so we'll skip to Translate:
' Great big, long, complex state machine all in one ugly chunk Private Function IFilter_Translate(sLine As String, _ ByVal iLine As Long) As EChunkAction §
That comment looks so intimidating that we'll just skip over what a global class is and how you might convert one until Chapter 5. The important point is that IFilter_Translate will convert each line of one, count the line numbers, and return an indicator of what it did.
CModGlobDelFilter, like all of the other Global Wizard filter classes, has a Name property, which I won't show because it's simply the classic Get/Let property procedure pair wrapping an sName variable. The Name property is a property of the class, not of the IFilter interface. But its internal sName variable is visible inside the class to the implementation of the interface Translate method.
Hmmm. These conversion classes are all polymorphic through the IFilter class. But they also all have a Name property with the same String type. That's dumb polymorphism. Why would anybody want to do that?
The Global Wizard program picks which conversion it wants to perform through some logic that we don't care about, and sends the results to the following function:
Sub UpdateTargetFileDisplay() HourGlass Me ' Select the appropriate filter and assign to any old object Dim filterobj As Object Select Case emtCur Case emtStandard If chkDelegate Then ' Translates standard module to global class with delegation Set filterobj = New CModGlobDelFilter Else ' Translates standard module to global class w/o delegation Set filterobj = New CModGlobFilter End If Case emtClassPublic ' Translates public class to private class Set filterobj = New CPubPrivFilter Case emtClassGlobal ' Translates global class to standard module Set filterobj = New CGlobModFilter Case emtClassPrivate ' Translates private class to public class Set filterobj = New CPrivPubFilter Case Else txtDst = "" Exit Sub End Select ' Setting name isn't performance sensitive, so do it late bound filterobj.Name = txtDstModName ' Use early-bound variable for performance sensitive filter Dim filter As IFilter Set filter = filterobj filter.Source = txtSrc FilterText filter txtDst = filter.Target HourGlass Me End Sub
You might recall my saying at the start of this chapter that the Object type and late binding are evil, and yet look at the type of the filterobj variable at the top of this procedure. Call me a liar. Call me a realist. All these filter classes have a Name property, but it's set only once. No one will be able to tell the difference if the class-specific objects are bound late rather than early. On the other hand, the properties and methods of the IFilter variable passed to FilterText will be called over and over. You'd notice the difference on large conversion operations if you passed a variable with Object type rather than IFilter type.
Methods and properties of Visual Basic interfaces are similar to what C++ and many other object-oriented languages call virtual functions. As with virtual functions, many syntactical variations are available, but not all of them make practical sense. And sometimes polymorphism needs a little help from other techniques--especially delegation. Let's check out some simple scenarios.
The CBugFilter class presented earlier implemented the IFilter interface. You could access the IFilter interface or the default _CBugFilter interface through separate object variables. Think about this for a moment. If you can get two interfaces through polymorphism, why not three interfaces? Why not 12? Why not 137 interfaces?
No problem.
Consider the CJumpHop class. It implements two interfaces: IHop and IJump. Here's IHop:
' IHop interface Function Hop() As String End Function
You can probably guess the IJump interface:
' IJump interface Function Jump() As String End Function
The CJumpHop class implements both of these interfaces:
' CJumpHop class Implements IJump Implements IHop Private Function IHop_Hop() As String IHop_Hop = "Hop" End Function Private Function IJump_Jump() As String IJump_Jump = "Jump" End Function
You can access the implementation of either interface through an appropriate object variable:
Dim h As IHop, j As IJump Set h = New CJumpHop Debug.Print h.Hop Set j = h Debug.Print j.Jump
You can access the Hop method polymorphically through the h object variable or you can access the Jump method polymorphically through the j object variable. Notice also that you can get at the IJump interface through an IHop object variable (Set j = h).
You could extend the CJumpHop class to have members of its own:
' Additional methods belonging to CJumpHop Function Skip() As String Skip = "Skip" End Function Function Hop() As String Hop = IHop_Hop End Function Function Jump() As String Jump = IJump_Jump End Function
These methods aren't polymorphic. They belong directly to the class and can be used like this:
Dim j As New CJumpHop Debug.Print jh.Skip Debug.Print jh.Hop Debug.Print jh.Jump
Notice that the Hop and Jump methods simply delegate to the polymorphic versions so that the CJumpHop class can have non-polymorphic versions without doing any work. The Hop method and the IHop_Hop method are separate methods accessible through different parts of the object, but they share the same implementation code.
This probably looks like a whole lot of nothing, and with interfaces this simple, it is. But imagine that you have a collection of graphical objects--stars, polyhedrons, ovals, and so on. There's a separate class for each kind of object, but the classes all implement the same interfaces. The IDrawable interface has methods and properties for setting the position and color of the shape object, and for drawing it. The IScaleable interface has methods and properties for scaling the objects to different sizes. The IMangleable interface has methods and properties for twisting and skewing the object. Now let's say you have a collection of these objects, and you want to draw the ones that are scaleable. You might do it something like this:
Dim scaleable As IScaleable, drawable As IDrawable, shape As CShape On Error Resume Next For Each shape In shapes Set scaleable = shape ' An error indicates that the shape isn't scaleable If Err = 0 Then scaleable.Scale Rnd Set drawable = shape drawable.Color = QBColor(GetRandom(1, 15)) drawable.X = GetRandom(0, pbShapes.Width) drawable.Y = GetRandom(0, pbShapes.Height) drawable.Draw pbShapes End If Next On Error Goto 0
Like most air code, this snippet is probably full of bugs and errors, but I'm sure you could make it work. In fact, there's an example that does something very similar with multiple interfaces on the Visual Basic CD.
When you create a public COM class or control, you are signing a contract never to change that component in any way that would break existing clients. I'll talk more about this in Chapter 10. In real life, things change, contract or no contract. The convention for creating a new class that is a superset of an existing class is to give it the same class name, but to append a digit. For example, if your main client, Big Brother And Company, complains that your CMotivate class just isn't doing the job, it might be a good idea to enhance it by creating the CMotivate2 class.
First, let's check CMotivate:
' CMotivate Function Cheer() As String Cheer = "Rah, rah!" End Function
That's OK, but maybe Big Brother wants to try some different incentives. Since you don't want to rewrite the whole class from scratch, you can use delegation to reuse the existing part of the class and add new features in CMotivate2:
' CMotivate2 Private motivate As New CMotivate Function Cheer() As String Cheer = motivate.Cheer & " Rah!" End Function Function Threaten() As String Threaten = "Shape up!" End Function
This class contains a CMotivate object, which it uses to provide the existing functionality. It enhances the existing Cheer method and adds a Threaten method. The technique of including an object in a class and using that object's features to enhance the new object is called containment--your outer object contains an inner object to which the outer object delegates part of the work. We'll be seeing a lot more delegation in this book--too much, in fact. We'll have to use delegation for tasks that would be done with inheritance in other object-oriented languages.
The CMotivate2 class does everything CMotivate does and more, but it doesn't do that work in the same contexts. For example, Big Brother might want to pass a CMotivate2 object to their Motivator function. Motivator was written by Big Brother and we have no control over it. They are perfectly satisfied with Motivator and have no intention of changing it. But they want the other benefits of the CMotivate2 class. Motivator looks like this:
Function Motivator(motivate As CMotivate) As String Motivator = motivate.Cheer End Function
The client wants to use it like this:
Dim motivate2 As New CMotivate2 Debug.Print motivate2.Cheer Debug.Print motivate2.Threaten Debug.Print Motivator(motivate2) 'Error! Fail because motivate2 is not a CMotivate.
You receive a nasty note from Big Brother telling you that if they're going to have to rewrite all their programs, they might as well rewrite them with classes from some other vendor. You assure them that the CMotivate2 error was a fluke and that the CMotivate3 class will not only be compatible with CMotivate, but will also add more new features. In real life, CMotivate3 would have to be compatible with both CMotivate and CMotivate2, but to keep things simple, we'll pretend CMotivate2 was a beta version that was never deployed and that Big Brother doesn't need compatibility with it.
To be truly compatible, the CMotivate3 class needs to use the Implements statement to make the new class polymorphic with CMotivate. The methods and properties of the interfaces we've implemented so far have been empty, but that's not a requirement. In fact, every class you define has an interface, and in a sense, is an interface. IFilter is an interface by convention. Technically, it's an ordinary class that happens to have empty members and a name that follows the interface convention. Under the hood, IFilter has a real COM interface called _IFilter, but the Visual Basic Implements statement lets you use the class name to access the interface. It doesn't matter whether the implemented methods have their own implementation.
In object-oriented languages with inheritance, virtual functions are frequently given base functionality in the base class. Other classes inherit the base functionality and extend it. Since the functions are virtual, the extended class can work polymorphically. You'll find it difficult to do this with Visual Basic because it lacks inheritance, protected members, and other common features of object-oriented languages. Instead, you must use delegation to fake inheritance. Here's how the CMotivate3 class does it:
' CMotivate3 Implements CMotivate Private motivate As New CMotivate 'Delegate to internal CMotivate object Private Function CMotivate_Cheer() As String CMotivate_Cheer = motivate.Cheer & " Rah!" End Function 'Reuse inner implementation for outer method Function Cheer() As String Cheer = CMotivate_Cheer End Function Function Threaten() As String Threaten = "Shape up or ship out!" End Function Function Bribe() As String Bribe = "Cash under the table!" End Function
By implementing the CMotivate_Cheer method with delegation, CMotivate3 provides an enhanced version of the Cheer method for CMotivate clients. It must provide a separate Public Cheer method for CMotivate3 clients, but it can delegate the work to CMotivate_Cheer.
Interfaces are a very important part of COM, and they're also the latest fad in API design. Many of the coolest new features of both COM and Windows aren't provided through traditional Windows API functions. Instead, they're provided by interfaces.
There are two kinds of standard interfaces. Some are implemented by the system so that you can call them. Examples include IShellLink (shortcuts) and IStorage (a new model for file I/O). You create the objects that implement these interfaces through API calls or through coclasses in type libraries. It's a case-by-case deal. We'll examine some specific examples later.
Other interfaces are implemented by you so that the system can call your objects. For example, if you implement IContextMenu, Windows can call your implementation to handle context menus associated with your documents. Techniques for registering or installing these interfaces vary on a case-by-case basis.
It's unfortunate that most of the standard interfaces you might want to use have Visual Basic–hostile definitions. Here are some of the common problems with interfaces:
In short, most standard interfaces are designed for C++ and other low-level languages. High-level languages like Visual Basic and FORTRAN are out of luck. Fortunately, Visual Basic programmers who buy my book have a workaround. They can use the Windows API type library. It contains most of the standard interfaces you might want to use, but the names have been changed to protect the innocent. (The flame below takes care of the guilty.)
FLAME The Visual Basic people and the Windows and COM people don't seem to be talking to each other. New Windows and COM interfaces keep coming out, but they're language-specific and have no type libraries. So whose fault is it? Should the Windows and COM developers start writing Basic-friendly interfaces and type libraries? Or should the Visual Basic designers add features that allow Basic to use interfaces the way they are? Both. The origin of this problem is the split between COM Automation and the rest of COM. Automation is handled by the Visual Basic group and is mostly language-independent. The rest of COM is handled by different groups that until recently didn't seem to realize that non-C-based languages even existed. In fact, some Windows development is done by masochists who actually do COM development in C rather than C++ (check out the samples for new Windows 95 interfaces). I see some evidence that both sides are beginning to recognize that they have a problem, but in the meantime, Visual Basic programmers are the losers.
So how do my interface definitions get around the problems discussed above? Well, I lie. I claim that unsigned integers are signed. I change the definitions so that Visual Basic understands them. My type library uses a specific type library technique to make interfaces private to the applications that use them so that they won't overwrite the official interfaces used by other programs on your machine. Comments in the type library source files and in other documents on the CD describe the process in general. It's a type library problem, so I won't explain the details in this Visual Basic book.
As a Visual Basic programmer, you need to know only that the interfaces exist in the type library and that they follow a specific naming convention. I add the letters VB after the I in the interface name. So my version of IShellLink is IVBShellLink. My IEnumVARIANT is IVBEnumVARIANT. In theory, you can simply use the Implements statement on standard interfaces in the type library the same way you implement Visual Basic interfaces. In practice, many interfaces require workarounds and hacks. We'll be looking at some examples in later chapters.