Chapter 3: An Object Way of Basic


Contents

Introduction

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.

The Three Pillars

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.


WARNING One of the early reviewers of this manuscript accused me of arrogance for claiming the ability to scientifically rate a language with an error factor of .2. Well, for those with a sense of humor slightly different than mine, let me point out the bulge in the side of my face (tongue in cheek). The reviewer in question, whose opinions I value, used a method at least as scientific as mine to give Visual Basic an OOP rating of 2.95. I considered his rating ridiculous. He considered mine ridiculous. But you, dear reader, have the only scientific rating system that matters.

Encapsulation

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.

Rating for encapsulation: .8

Reusability

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.

Rating for reusability: .4.

Polymorphism

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.

Rating for polymorphism: .7

Object-Oriented Anyway

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.

Object-Oriented Programming, Visual Basic Style

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.

Types

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.

Intrinsic 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

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.

Classes--types that act

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.

Declaring 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.

Types vs. classes

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++.

Initializing Objects

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.

Declarations with New

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.

Procedures Versus Methods

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.

More About Properties

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

Visual Basic with Curly Braces

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.

Where Do Objects Come From?

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.)

Objects from CreateObject

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.


NOTE Although Visual Basic programmers should avoid CreateObject if at all possible, a similar convention is still commonly used in a related language, VBScript. Objects are created through their CLSIDs (not their ProgIDs) in VBScript, and all object references are late-bound. But that's another book.

New Objects from Type Libraries

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.

ActiveX Control Objects

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.

Form Objects

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.

Class Objects

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.

Dual Interfaces and IDispatch

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.


PERFORMANCE

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.


First Class: CDrive

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.

Drive Design

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.

Test Drive

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.

Drives tab

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).

Class Diagrams

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.

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.)

Implementing CDrive

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.


NOTE Unfortunately, you can't use the same class file for a private class in one project and a public class in another. I provide separate but otherwise identical private copies of all my public classes. The private module names are the same as the public names with P_ as a prefix letter. They can be created by using the Save As command and then changing the Instancing property to Private. The Global Wizard described in Chapter 5 can translate private classes into public classes and vice versa automatically. This is an annoyance for maintenance, but you have to live with some similar scheme if you want to use the same class in different contexts.

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.

Initializing CDrive data

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.

Reading CDrive data

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

Drive methods

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.

Default members

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.

Procedure Attributes d.b.

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

NOTE The default member is normally a property, but it's also possible to define a method function as the default. There's not much difference semantically between a method function and a property get procedure. In fact, if you want a read-only member, the only reason to implement it as a property get rather than as a method is that the property will generate a better error message if a user tries to assign to it.

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.


Naming Private Data

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.

Other Class Features

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.

Static Members

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.

Friend Members

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).

Events

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.

The Form Class

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.


Get With It

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.

Polymorphism and Interfaces

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

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.

Polymorphic Filters

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.

 Encapsulation and polymorphism

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.

Interfaces in Visual Basic

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.

Using an interface class

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.


NOTE The error trap in FilterTextFile doesn't have anything to do with polymorphic classes, but it does illustrate a stair-stepped error trap. Often, when something goes wrong while you're building something with a series of Basic statements, you need to unbuild it in the opposite order, undoing only the parts you have finished. A carefully designed series of error traps can take you back to the initial state.

Implementing an interface

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.

Using an interface

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

NOTE The HourGlass sub is a useful little procedure that displays an hourglass cursor during an operation and then removes it. Check it out in UTILITY.BAS. Always use a pair of HourGlass procedures--the first to display the hourglass cursor and the second to remove it.

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.

Implementing another interface

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?

Using another interface

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.

Variations on Implements and Polymorphism

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.

Implementing multiple interfaces

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.

Enhancing public classes

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.

Implementing windows and COM classes

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.