Creating Global Procedures
On the surface, it’s a simple matter to convert standard modules containing utility functions into global class modules that can be used in a component. Let’s do it. We’ll create the VBCore component, step by step—including a few false steps.
The AllAbout program is a good starting point. It uses many of the procedures, classes, and objects of the VBCore component. In the old version, AllAbout consisted of an FAllAbout form with the real functionality of the program and a bunch of support modules, classes, and forms that might be used in any program. The goal was to move all those functions from the AllAbout project to the VBCore project and make AllAbout use VBCore.
You can see the final results of the whole operation in the files ALLABOUT.VBP and VBCORE.VBP. I also provide a project named LOTABOUT.VBP, which is essentially the AllAbout project before I converted it to use VBCore. You could re-create AllAbout from LotAbout using steps similar to those I’m about to describe (although changes to AllAbout in the course of writing this book would make the steps slightly different). If you want to experiment with this, do it in a separate directory so that you won’t overwrite existing files.
I started by adding a new project to the AllAbout project by selecting the Add Project item from the File menu. I made it an ActiveX DLL project and named it VBCORE.VBP. This created a new project group, which I named ALLABOUT.VBG. In VBCore, I added a reference to the Windows API type library, and
then I started moving the general-purpose modules from AllAbout to VBCore.
I planned to eventually compile the component into a native-code DLL, but at first I just used the uncompiled component project. AllAbout or any other program that uses VBCore must select it in the References dialog box so that Visual Basic will know to use the component.
You might expect that I would start with the module used by almost every other module—DEBUG.BAS. But stop. This isn’t such a great idea. DEBUG.BAS makes extensive use of conditional compilation. The key routines, BugAssert and BugMessage, disappear if you turn off the debug constant. The disappearing act happens at compile time. When all the bugs are gone and clients start using the compiled component, there won’t be any debug routines. So I left DEBUG.BAS in AllAbout but added a separate copy to VBCore. Every project that uses debug procedures needs DEBUG.BAS. That causes a little bit of duplication but not enough to worry about since the debugging procedures will disappear in the long run in both the component and in the client.
The next module I selected was BYTES.BAS, but I couldn’t just move the standard module to VBCore because the only thing you can make public in a component is a class. That hasn’t changed in version 5. The solution was to create a new class module named BYTES.CLS in VBCORE.VBP. I copied all the code from BYTES.BAS, pasted it into a new class, named the new class GBytes, and set its Instancing property to GlobalMultiUse. Notice that I didn’t name the class CBytes because, although it is a class, it will look like a standard module to clients. I didn’t call it MBytes because it won’t be a standard module no matter how much it looks like one (and it won’t even look like one to other classes).
The resulting class module had all the functionality of BYTES.BAS, and to a user of VBCore, it worked like BYTES.BAS. How could this be? Normally, a class without an object is like a bicycle without a fish, but in this case, we want clients to call the class as if it had no object. This is an illusion caused by setting the
Instancing property to GlobalMultiUse (which might better be called LooksLikeAModuleWorksLikeAClass). There’s actually a kind of default object behind the scenes. I’ll get to the unexpected complications caused by this hidden object shortly.
With BYTES.CLS in place, I no longer needed BYTES.BAS, so I deleted it from ALLABOUT.VBP. I tried to run ALLABOUT again but encountered a predictable problem. BYTES.BAS uses UTILITY.BAS, so BYTES.CLS must use UTILITY.CLS. I created UTILITY.CLS using the same technique. I got a “Sub or Function not defined” compiler error from StrToBytes in the following statement:
If IsArrayEmpty(ab) Then
Next I right-clicked IsArrayEmpty and selected Definition from the context menu. The IDE reported Identifier under cursor is not recognized. Notice that it said the identifier was not recognized, not that it didn’t exist. In fact, it did exist right there in UTILITY.CLS.
What’s going on here? Why can’t code in BYTES.CLS recognize public procedures in UTILITY.CLS? To make a long story short, the modules in the AllAbout project can see procedures in UTILITY.CLS with no problem, but the modules in VBCore can’t. When Visual Basic says that modules with global instancing are public, it means exactly what it says, not what any sane programmer would expect. To most of us, public includes private, but global classes are actually public only to outside projects; they’re invisible to other modules in the same component.
I’ll tell you what I think of this feature shortly, but for now I’ll just say that there are two workarounds. Actually, there are more than two workarounds, but the others are terrible, whereas the ones I’m going to explain are merely bad. I know this because I tried all the terrible ones before I figured out the bad ones. For example, one terrible solution is to create a separate component for every
module—UTILITY.DLL, ERRORS.DLL, BYTES.DLL, and so on. But then UTILITY.DLL won’t work unless ERRORS.DLL is loaded….Let’s not even think about it. Another terrible solution is to throw all those standard modules into a single global class—KITCHENSINK.CLS. Not only is this idea terrible, it doesn’t work because non-global classes such as CDrive and CVersion will also be part of VBCore, and they’ll need to use the invisible procedures in KITCHENSINK.CLS.
Let’s just skip the how and why. In the long run, I used one or the other of the bad techniques to convert all the standard modules used by AllAbout to global class modules in VBCore. The new AllAbout consisted of only two modules—ALLABOUT.FRM and DEBUG.BAS. Everything else was in VBCore. The AllAbout project ran in the IDE. Furthermore, I could compile the component to native code. To switch from the source version to the compiled version, I had to make sure VBCore rather than AllAbout was selected in the Project Explorer window. Then I chose Make VBCore.dll from the File menu to compile the DLL. The section “Developing for the Real World” later in this chapter will explain how to switch easily between the version of the client that uses the compiled component and the version that uses component source files.