Vous êtes sur la page 1sur 234

Helpmate

COM+ Programming with Visual Basic


Developing COM+ Servers with COM, COM+, and .NET

Introduction to .NET
At the time I started writing this chapter, .NET had entered its Beta 1
cycle. Even though this product is only in Beta 1 (and if you have read the
documentation shipped with the .NET SDK, you will see that there is at
least a Beta 2 planned), I know that with the Microsoft marketing muscle,
many of you may be feeling in some ways as if you are already behind for
not having already converted all your applications to use .NET. The reality
is that .NET is a brand new architecture; it is not the next version of
COM+. What's more, all the Microsoft compilers that were written before
have to be rewritten to emit code compatible with the new architecture. In
many cases, the language constructs themselves have also been rewritten.
Visual Basic, for example, has gone through many syntactical changes--so
many that some may argue it is not the same language.
In this chapter, you are first going to get an introduction to the .NET
architecture, then you are going to get an overview of some of the new
features in VB.NET, and after you have an understanding of how to use
the features, you will learn about how to mix .NET components with
COM+ components. Because I am currently using beta software, the
information in this chapter is subject to change. There is no way that I can
pretend that this chapter will give all the information necessary to be a
.NET developer, but it is my hope that you will learn enough to satisfy
your curiosity.

The .NET Architecture


Why are we talking about .NET and not the next version of COM or
COM+? .NET in fact is a brand new architecture with few things related to
the current architecture. So what is wrong with COM and why is
Microsoft going in a different direction? Well, before we can point out the
benefits of .NET over COM/COM+, let's talk about the architecture itself.
First, how do you get .NET? .NET comes in two main parts. One of the
parts is the .NET SDK. The .NET SDK team builds what was previously
known as the Universal Runtime (URT) and is now called the Common
Language Runtime (CLR). It is a runtime environment that includes a
loader for .NET code, a verifier, certain utilities, and a number of .NET
DLLs that compose what is called the .NET Common Type System. The
SDK also includes four command-line compilers: one for VB.NET, one
for a new language called C# (C-Sharp), a new version of the C++
compiler and linker that produces what is called managed C++ (or
MC++), and one for the Intermediate Language (IL), which you will learn

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

about shortly. A number of other language compilers are also being


developed by third parties to emit .NET-compatible code, such as
Cobol.NET, Component Pascal, Eiffel, and others.
The second part of .NET is called Visual Studio.NET. Visual Studio.NET
is composed of the IDE that you use to write code, several programming
tools, and online documentation. Visual Studio.NET uses the .NET SDK
compilers to compile your program. In reality, if you like using
NotePad.exe, you do not need Visual Studio.NET; you could write your
VB programs in Notepad and then run the command-line compiler.
One important thing to understand about .NET is that it is a lot more than
its name implies. At first glance, it may seem like a technology geared
toward writing Internet applications. Although this is true in some sense, it
is a lot more than that. .NET is primarily an architecture for writing
applications that are object-oriented in nature, and both hardware and
operating system agnostic. You may have heard similar claims from
another language--Java. Under the covers, .NET is very different from
Java, but conceptually the two architectures have the same goal.
IL
The heart of .NET is the Intermediate Language (IL). IL is a hardware-
independent object-oriented form of assembly language. The following
code shows a "hello world" program written in IL:
.assembly hello {}
.assembly extern mscorlib {}
.method static public void main( ) il managed {
.entrypoint
.maxstack 1
ldstr "Hello World"
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
}
A line-by-line discussion of the preceding code is beyond the scope of this
book; however, we will discuss some of the most interesting parts of the
code shortly. Notice for now that the code resembles assembly language
but uses a different set of commands, and it does look like a much higher-
level language than pure .asm.
You could take the previous code and save it as a text file with Notepad,
giving it the extension .IL (helloworld.il, for example). If you have the
.NET SDK installed, you could run the IL compiler from the command
line by entering the following command:
ILASM helloworld.il
The output of that command would be the executable helloworld.exe. You
could then run the program and witness "Hello World" appear on your
console.
IL-generated code is processor and operating system independent. The IL
source code must be changed to native code before it is run. IL programs
are not interpreted; they are instead converted to native code using one of
three compilers provided in the .NET SDK.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

The default compiler is the Just-in-Time (JIT) compiler. The JIT compiler
takes IL and first compiles the entry-point function and any code that the
function needs; then as the code executes, any other code that that code
needs is also compiled; and so on. Sometimes some of the earlier
compiled code may be thrown out from memory to make room for other
code, then recompiled when needed.
Another option for compilation is to use the EconoJIT compiler, which is
due to come out in a future release of the Platform SDK. The EconoJIT
compiler does the same job as the JIT compiler, but it produces less
efficient code. Sometimes developers feel that having code compiled at
runtime may decrease the performance of the program considerably.
Although this may be the case, depending on how the compiler is written,
it is more likely that your code may see better performance when it is JIT
compiled than when it is compiled in a traditional way. The reason for this
is that the JIT compiler can take into consideration your hardware and
optimize the code to function well with it. If you think about it, when code
is precompiled from the factory, it follows a "one size fits all" approach; it
is often optimized to run on a machine that has an Intel processor. If you
ran your program on a machine with an AMD processor, a JIT compiler
would be able to use the AMD extensions as needed. This is the way that
the JIT compiler is supposed to work, and it produces high-quality
machine code at the price of load time. The EconoJIT compiler, on the
other hand, compiles faster at the cost of execution performance.
The third type of compilation is called OptJIT. The OptJIT compiler is due
to come out in a future release, but the idea is that some third-party
vendors will emit a subset of IL called Optimized IL (OptIL). OptIL is IL
with instructions embedded into it that tell the OptJIT compiler how to
optimize its output for certain tasks. For example, the third-party language
may be optimized to do mathematical calculations and would like the
generated code to do mathematical calculations in a certain fashion. The
third-party OptIL output would embed information in the IL that would
tell the OptJIT compiler how to optimize calculation code when it
generates the machine code.
The operating system that you are using does not know how to take IL,
run it through the JIT compiler, and run the results directly. Later versions
of Windows (probably beyond Windows XP) will be IL ready. This means
that the operating system may be able to see a text file with IL in it and
run it as is. However, Windows 2000 cannot do this, so IL must be
embedded into an executable or a DLL. The ILASM compiler can take the
IL and build a PE file around it. PE stands for portable executable and is
the format that .EXE files and .DLL files use. The PE wrapper that the
ILASM compiler generates has code to invoke the runtime loader found in
mswks.dll and other files shipped with the .NET SDK. The PE file has the
IL embedded in it. The IL may be embedded as "text," although some
formatting of the text is done to make it easier to parse, or it may be
prejitted (turned into native code). Microsoft will ship some files that have

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

IL already changed to native code or prejitted. The runtime is able to run


compiled IL code or text IL code.
Most of the time, you will not be writing IL code from scratch. Instead,
you will use your language of choice. A number of language compilers
have been rewritten to generate IL instead of native code. Visual Basic is
one of these languages. In addition Microsoft has created a brand new
language called C# (C-Sharp). C# is a C++-like language that in many
ways also resembles Visual Basic. It eliminates a number of features from
C++ that, although they provided a lot of "power," also produced a lot of
confusion. For example, C# does not have pointers. It also does not have
macros or templates. The successor to VB 6 is VB.NET. VB.NET is a
completely new version of Visual Basic. Many things have changed, and
later in this chapter you will learn about some of the new features.
Assemblies
Along with a new form of assembly language and a new set of compilers,
Microsoft has also redefined what it means to be an application. If you
think about the current operating system boundaries, there are two main
entities: processes and threads. Threads give us an order of execution. You
may recall from Chapter 5 that a program may launch another thread in
order to do two tasks seemingly simultaneously. A process determines
primarily a memory boundary. Two processes do not share memory. Their
memory is isolated, and although it is possible to share memory using
low-level functions, that is not the standard. However, more than a
memory boundary, the process also serves as a security boundary. In
Chapter 10, you learned that each process in the operating system runs
under a certain set of credentials.
Microsoft has redefined what it means to be a process. In fact, the new
world does not address processes per se; the new world uses assemblies.
You're probably wondering whether assemblies are DLLs or EXEs. The
answer is that assemblies are neither (it is almost better to forget that
EXEs and DLLs ever existed). In many ways, processes are better
matched to a new boundary in the .NET architecture known as
AppDomains.
As you already know, the runtime runs IL. A developer may declare one
file or a number of files containing IL as being part of an assembly. An
assembly is the smallest unit that can be versioned; it determines the
boundary for which classes are made public or private; it is also a unit that
can be secured. Because IL files are packaged in EXEs and DLLs, an
assembly can be a single EXE, a single DLL, or a combination of EXEs
and DLLs. An assembly may also contain other files, such as resource
files and even help files. Figure 11-1 shows the relationship between
EXEs, DLLs, and assemblies.
Figure 11-1. The relationship between modules, assemblies, EXEs, and DLLs

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Each image file containing valid IL is called a module. To be an assembly,


one of the files in the group must have an assembly manifest. The
assembly manifest is created with an assembly definition. If you look at
the "hello world" example presented earlier, the code begins with the
directive .assembly hello. Without the .assembly directive, the assembler
would produce only a module. Having the .assembly directive does two
things: it declares an assembly with the name hello, and it creates the
assembly manifest.
A manifest is metadata, which is a fancy word for descriptive text. You
can think of the manifest as the type library for the assembly. It defines
properties of the assembly such as the version number of the assembly and
the culture (or locale) that the assembly was built for. The manifest also
lists all the modules, all the files, and all the external assemblies on which
the assembly depends. For example, if your assembly requires data access,
you will need to reference the Microsoft.Data assembly. In the earlier IL
example, the second line of code, .assembly extern mscorlib, tells the loader
that the assembly uses a system assembly known as mscorlib. The reason
the code needs to reference this assembly is because it uses a class called
System.Console. This class has a method called WriteLine that the IL
code uses to output "Hello World" to the console.
As you may suspect, Microsoft provides a number of prebuilt assemblies
that you may use. These assemblies contain a series of public classes,
interfaces, and attributes (you will learn about attributes shortly). These
sets of classes, interfaces, and attributes constitute the .NET Common
Type System.
The Common Type System
If you think about the type system in Visual Basic 6, you may separate the
types into two main groups: objects and intrinsic types. We can say that
intrinsic types include things like integers, strings, doubles, and so on,
while object types refer to classes you define. C++ has its own type
system. It also includes some native types like int, double, short, and long,
and it includes object types--classes that you define. C++ also has several
class libraries, among them the Microsoft Foundation Classes (MFC) and
the Active Template Library. If you look at a third-generation language
like Delphi, for example, you see that that language also has its own type
system. A common problem was making these type systems communicate
with one another. COM+ handled this by letting each compiler decide how
they were going to map types to a few C++ types. If you wanted your
component to be VB compatible, you had to figure out what subset of all
the C++ types mapped neatly to VB types, and the VB compiler had to
look at a C++ interface definition and translate it as best it could to VB.
To resolve most of the issues of compatibility between languages,
Microsoft is also introducing a common type system. I say most, because
it turns out that every type that can be represented in the type system is not
necessarily available to every language. Instead Microsoft defines the
Common Language Subset (or CLS) to be a subset of the type system that

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

every language should support. However, for the most part, having a
common type system means that every language that produces IL knows
how to use types declared in any other language. Three things make the
type system particularly interesting:
• Every object has the same root object: System.Object.
• Classes are self-describing. Through a set of classes that define
reflection, a developer can find out from a running object all the
information about the class that was used to generate that object.
• Microsoft decided to distinguish between reference types and value
types. A reference type is a reference to an object that is allocated
on the heap. A value type is an object allocated on the stack. The
concept of value types is nothing new; after all, in VB 6 we have
things like Integers, Doubles, Singles, and UDTs. These are all
examples of value types.
What makes value types in the new type system different is that they also
derive ultimately from System.Object. In other words, even value types
are classes with methods, fields, and interface implementations. For
example, when you dim a variable as type Integer, VB turns that
declaration into a variable of type System.Int32. System.Int32 is a class.
What distinguishes a value type from other classes is that value types are
derived from a class called System.ValueType. Sometimes it may be
necessary to take a value type and cast it to a variable of type
System.Object (think of System.Object as the VB 6 Variant type or the
VB 6 Object type). However, System.Object declarations are reference
types, and the runtime treats them differently from value types. To allow
this conversion, the runtime supports an operation known as boxing.
Boxing means that the system duplicates the data stored in the value type
and creates a copy of the object on the heap. The reverse procedure, in
which a value type is created from a reference type and the data is
replicated once again, is called unboxing. You are going to see an example
later on in the chapter.
To make it possible for every object, including value types, to derive from
System.Object, the CLR uses inheritance. There are two types of
inheritance in the system: class inheritance and interface inheritance. You
can inherit from only a single class, and, in fact, every class must inherit
from at least one class, System.Object. On the other hand, you can
implement any number of interfaces. That stated, let me complicate things
by saying that interfaces are also classes derived from System.Object.
However, they are a special type of class marked as abstract, and every
member in the interface is really a pointer to a function (the concept of
vtables to vptrs is still the same).
Why .NET and Not COM+ 2.0?

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

The first question that people often have is why Microsoft had to come up
with a different component technology. Why not improve COM+? Let's
talk about some of the limitations in COM and how .NET addresses them.
One problem with COM+ was the lack of a common type system. We
have talked a little about this problem. To summarize, each language
involved in COM had its own type system, and the best the compilers
could do was match a type from one language to another by the amount of
memory that the type consumed and the semantics of the type. With .NET,
we have a common type system, each language creates types that follow
the rules of the type system, and every type is a subtype of System.Object.
Another problem with COM was how to advertise the types that the server
exposed. C++ developers relied primarily on header files that described
the set of interfaces exposed by the server. VB relied on type libraries.
Often an interface would originate from one of the Microsoft groups in
C++ syntax. Then a developer would have to write a type library for
Visual Basic that had VB-friendly syntax. .NET uses a better approach.
Assemblies expose types, and an assembly can be referenced directly
when building a new assembly. Thus, if you create a program (an
assembly) that relies on a database class, for example, when you compile
your assembly, you will tell the compiler to reference the database
assembly. Visual Studio.NET will give you an easy way to tell the
compiler what assemblies you need. In fact, there is almost no difference
visually between referencing a type library in VB 6 and referencing an
assembly. Later on you will see how to expose a class in an assembly and
how to reference the assembly in another assembly.
A third problem with COM was that the architecture did not have perfect
knowledge of the types in your process. For example, there was nothing in
COM that told the operating system what COM servers your client
program was dependent on at load time. The type library told COM about
what types your server exposed, but the client relied on CreateObject or
New. So there was no way for the OS to know at load time if your EXE
needed a server that wasn't available in the system. The OS didn't know
until it executed the line of code that tried to create a type in the server
whether that server was available. With .NET, the manifest contains a list
of not only the types that your server exposes, but also the types that the
server needs. The CLR loader verifies that it can find all the assemblies
that your assembly is dependent on before running your program.
Another related problem concerned versioning. How many versions of
ADO could you use on one computer at a time with COM+? Only one.
When you registered ADO on the machine, there was a single registry key,
InProcServer32, that told the system where ADO was located. If you had
another version on the system, there was no way to use it side-by-side with
the first one. You would have to register the new version, and that would
override the InProcServer32 key to point exclusively to the new version, and
every program would use the new version. One problem resulting from
this approach was that there was no guarantee that existing client

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

programs could use the new version of ADO. Nothing in your client
process told the OS what version of ADO your program was dependent on
and whether you could use a new version or not.
In contrast, .NET has an improved versioning scheme. When you build a
client assembly, the manifest tells the loader the version of each assembly
that you are dependent on. In addition, .NET recognizes shared assemblies
and private assemblies. Private assemblies are used with just your
applications. Something like ADO.NET would be a shared assembly--an
assembly that many assemblies count on. Shared or public assemblies are
signed with a private and public encryption key. The process of signing
the code produces an originator. Once your assembly has an originator,
you may put it in the global access cache (GAC). The GAC physically
lives under the WINNT\Assembly directory and stores a copy of all shared
assemblies. The CLR loader looks at the list of assemblies you are
referencing, and if it does not find a private assembly that is a later version
than the version the client was compiled against, then it will try to find the
assembly in the GAC. The GAC can store multiple versions of the same
assembly. For example, you may have ADO.NET Version 1, 2, and 3 in
the GAC. When you build an assembly, the manifest will contain a
reference not only to the assembly name, but also to the assembly's
version number. It could happen that your application requires one version
of the shared assembly and another assembly requires another version, and
it could happen that the same process may be running both versions at the
same time. Therefore, .NET now gives you the capability of having side-
by-side versions of shared components. Later in this chapter, I will show
you how to sign your code with a key and add it to the GAC.
Yet another problem resulting from the lack of perfect knowledge of the
types you used internally was knowing when to release an object from
memory. You can tell if a programmer has done COM+ at the C++ level if
you ask about circular references. As VB developers, we do not have to
deal with things like reference counting and circular references directly,
but C++ COM+ developers do. A common problem with COM
components is that object A may be holding a reference to object B, and,
because B needs to make a callback call into A, it may be holding a
reference to object A. B will never be released from memory as long as A
is holding a reference to it, and A will never be released because B is
holding a reference to it. This is known as a circular reference.
.NET does not use reference counting to determine when an object's
memory should be reclaimed. Instead the system implements garbage
collection. Managed code can no longer ask for a chunk of memory
directly. To take advantage of garbage collection and other services, your
code creates an instance of a type or of an array of types, and the memory
needed is allocated by the runtime in a managed heap. When there is no
more memory to hand out, the system will release memory for objects no
longer in use. The garbage collection system differs from reference
counting in the way that memory is cleaned up--no longer is memory

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

reclaimed as long as there are no references to the object. Instead, the


system waits to release memory until there is need for more memory.
There are also commands for telling the garbage collector to collect
memory immediately.
You should now have a basic understanding of the .NET architecture and
how .NET improves on the existing COM+ architecture. Shortly, you are
going to learn about how to have COM+ components using .NET
components and vice versa. But before talking about interoperability and
about the new features in VB.NET, let's talk about how to compile and
version .NET assemblies.

Developing Assemblies
There are two ways to create VB.NET applications. One way is to use the
next version of Visual Studio, Visual Studio.NET. Visual Studio.NET is a
development environment built on top of the .NET SDK. The .NET SDK
is packaged separately from the Visual Studio.NET environment. The
SDK includes a C# command-line compiler, a VB.NET command-line
compiler, and the DLLs and EXEs necessary to run your .NET
applications.
I have decided that in order to make the information in this chapter last, I
am not going to use the Visual Studio.NET designer. One reason is that
the product has not been released yet. Another reason is that the product is
not yet stable. A third reason is that, for the first time, Visual Basic has a
true command-line compiler that can be used in conjunction with NMAKE
and MakeFiles. You may be familiar with MakeFiles if you have ever
worked with C++. MakeFiles are text files that tell NMAKE.EXE how to
build your program. For all these reasons, I have decided to use the second
most widely used development environment in the Windows platform,
Notepad.EXE. So for the next set of examples, you will need three things:
the .NET SDK downloadable from Microsoft, Notepad.EXE, and a
command prompt. Let's start with a simple Hello World application to get
a taste for how to use the command-line compiler.
Run Notepad.EXE and enter the following text:
Public class HelloSupport
Shared Public Function GetGreeting(ByVal sName As String) As String
return "Hello " + sName
End Function
End Class
This code declares a class called HelloSupport. At first it seems strange to
create a class for just one function, but in VB.NET every function is
exposed through a class. Even when you declare a module with functions,
the module becomes a class when it is compiled, and all of the functions in
the class become shared, very much like in the preceding example.
So what is Shared, anyway? Classes in VB.NET have two types of
methods: instance methods and shared (or static) methods. VB 6 class
methods were instance methods: you had to create an instance of the class
to use them, and the method performed a task on the data for that instance

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

only. Shared methods are new to VB.NET. They can be executed without
creating an instance of the class. The only limitation is that if your class
has both shared methods and instance methods, you cannot call the
instance methods from within the shared methods. You can call shared
methods only from within shared methods. If you need to call the instance
method, then you have to create an instance of your class and make the
method call just as any other function outside the class would. You also
cannot use any of the member fields in the class from shared methods
unless the fields are also marked as shared. In a module, the VB.NET
compiler turns all functions to shared.
The GetGreeting function returns a string that says "Hello x", where x is
the string that the calling program passed to it. Save the preceding file as
hellogreeting.vb. Then run the VB command-line compiler, VBC.EXE. If
you installed the SDK, the path to the VBC.EXE compiler should be
reflected in the environment so that you can run it from any folder. Open a
command window and switch to the directory where you saved the files,
then enter the following command:
vbc /t:library hellogreeting.vb
This command creates a DLL file with the name hellogreeting.dll. If you
look at the command line, you will notice that the first option is the /t
switch, which tells the compiler the target type. VB.NET is able to
produce Windows applications, console applications, DLLs, and modules
that can be linked with other modules to produce a multimodule assembly.
They can be packaged as EXEs or as DLLs. An assembly is the smallest
unit of code that can be versioned.
The keyword library produces a DLL. If you are unsure about the syntax,
you can enter vbc /? for a list of command-line switches. Now it's time to
create an executable that can use the function in the DLL. Create a new
file in Notepad and enter the following text:
class Helloworld

Shared Sub Main( )


Dim sName As String = "World"
Dim sGreeting As String = HelloSupport.GetGreeting(sName)
System.Console.WriteLine(sGreeting)
End Sub

End Class
Save the file as helloworld.vb. To compile the program, use the following
command line:
vbc /t:exe helloworld.vb /r:hellogreeting.dll
The preceding code declares a class called Helloworld. The class has a
procedure called Sub Main. The Sub Main function is the equivalent of
Sub Main in VB 6. The only difference is that the function must be
declared as a shared (or a static) subroutine. If you are a hard-core VB 6
(or earlier) developer, you will appreciate the fact that you can now
declare variables and assign them a value all in the same line, which is
what I have done in the first line of code inside Sub Main. The function

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

declares two variables: sName holds the name that the GetGreeting
function will use for the greeting, and sGreeting holds the response from
the GetGreeting function. Notice that to use the GetGreeting function, you
have to qualify it with the name of the class. Because the GetGreeting
function was declared as a shared function, you do not have to create an
instance of the class to use it. The code in Sub Main then prints the
greeting to the console using the System.Console.WriteLine function.
System.Console.WriteLine is a new function in the CLR. The function is
part of the System assembly. Microsoft has defined a set of classes that
gives support for the operating system functionality. Not only are these
classes a replacement for using the WIN32 API functions directly, but also
for other COM libraries that Microsoft ships, such as MSXML and ADO.
The runtime has assemblies that Microsoft has included for XML, for
HTTP communication, for data access, for threading, for IIS
programming--for practically anything you can think of. There is still a
way to call WIN32 APIs directly using the Interop classes; however,
jumping outside of the runtime is a costly operation, and you are
discouraged from doing so.
To build the executable, you must specify the name of the DLL (and the
path to the DLL) that your executable needs to resolve all functions. The
result of running the previous command-line command is the
Helloworld.exe image. If you run helloworld.exe, you should see the
phrase "Hello World" displayed in the command prompt.
Microsoft ships a disassembler with the SDK called ILDASM.EXE. Let's
run ILDASM.EXE on the resulting executable to see how the
helloworld.exe assembly references the hellogreeting.dll assembly. Run
the following command from the command prompt:
ILDASM.EXE HelloWorld.EXE
You should see the window in Figure 11-2.
Figure 11-2. ILDASM program

Once you open ILDASM, double-click on the MANIFEST branch, and


you should see the following code:
.assembly extern mscorlib
{
.originator = (03 68 91 16 D3 A4 AE 33 ) // .h.....3
.hash = (52 44 F8 C9 55 1F 54 3F 97 D7 AB AD E2 DF 1D E0 // RD..U.T?........
F2 9D 4F BC ) // ..O.
.ver 1:0:2204:21
}
.assembly extern Microsoft.VisualBasic
{
.originator = (03 68 91 16 D3 A4 AE 33 ) // .h.....3
.hash = (5B 42 1F D2 5E 1A 42 83 F5 90 B2 29 9F 35 A1 BE // [B..^.B....).5..
E5 5E 0D E4 ) // .^..
.ver 1:0:0:0
}

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

.assembly extern hellogreeting


{
.hash = (12 09 10 58 91 53 C7 13 7D 3D 53 87 A4 62 79 4F // ...X.S..}=S..byO
14 63 47 99 ) // .cG.
.ver 1:0:0:0
}
.assembly helloworld as "helloworld"
{
.hash algorithm 0x00008004
.ver 1:0:0:0
}
.module helloworld.exe
// MVID: {4C781D06-B3E4-4376-A19D-E068C05D7A39}
The manifest shows a list of assemblies that your assembly is dependent
on. That is nothing new; after all, before COM, DLLs had a list of
imported types that you could see with the depends.exe program.
However, even with the depends.exe tool, the system had no idea what
DLLs you were loading dynamically in code. For example, the OS knew
that your VB program was dependent on MSVBVM60.DLL (the VB
runtime), but it had no knowledge of any other DLLs you might have been
using through Declare statements, not to mention any COM DLLs you
loaded with CreateObject. What is also interesting is the difference
between private assemblies and public assemblies. If you notice, the
manifest shows that you are dependent on Version 1.0.0.0 of the
hellogreeting assembly. The version number for this assembly is not
crucial because hellogreeting is not a public assembly; it is meant to be
used only with this application. If you look at the rest of the manifest,
however, you will notice that the helloworld assembly is also dependent
on the mscorlib assembly and the Microsoft.VisualBasic assembly. These
latter assemblies are public assemblies; the creator went through the
process of creating a public assembly, and because it is a shared assembly,
the rules for using the assembly with our program are more stringent. For
example, we have to tell the runtime whether our assembly can use a
newer version of Microsoft Visual Basic or whether it must have the same
assembly it was built with.
Versioning Assemblies
To illustrate the concept of versioning in the CLR, let's turn the
hellogreeting assembly into a shared assembly, install it to the GAC, then
build a second version of the assembly and create a configuration file to
control which version of hellogreeting the helloworld assembly will use.
The first step is to digitally sign the code with a public/private digital key.
It is impossible for us to discuss the encryption algorithm in this chapter,
but the basic idea is that with a public/private key, encryption is done with
the private portion of the key and decryption is done with the public
portion. The various compilers first run an algorithm over the manifest
information and produce what is called a hash. Then they use the
public/private key file (from now on referred to as a private key file) and
run an encryption algorithm through this hash to encrypt it. The signature

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

is then embedded into the image. Also, the manifest for the signed
assembly includes an originator field. This field advertises the public key
to the world. Anyone can therefore use this public key to decrypt the
encrypted manifest hash, run the hash over the manifest once again, and
detect if there was any tampering to the manifest itself. In addition, any
client programs referencing the assembly have a token of the public key
known as the public token. This token is the result of a one-way hash
algorithm run through the public key. Anyone can run the same algorithm.
This step is done to verify that the assembly the runtime has found is
really the assembly that the client program was compiled against.
To generate the private key, you run sn.exe, the strong name utility that
ships with the .NET SDK. Enter the following command in a command-
prompt window:
sn.exe -k "widgetsusa.snk"
The name of the file--even its extension--is something you make up
entirely, although tools like VS.NET look for the .SNK extension. I'm
using the name of a fictional company, since a company would likely use
its name for the filename and use this key to sign every shared assembly it
produces.
Once you have generated the private key file, now you can sign your
assembly with that code. Modify the hellogreeting.vb source code as
follows:
<Assembly: AssemblyKeyFile("widgetsusa.snk")>
<Assembly: AssemblyVersion("1.0.0.0")>

Imports System.Runtime.CompilerServices

Public class HelloSupport


Shared Public Function GetGreeting(ByVal sName As String) As String
return "Hello " + sName
End Function
End Class
Notice that there are three new lines of code at the beginning. The first
line of code uses an assembly attribute. Attributes are specified with angle
brackets and are classes derived from System.Attribute. They are special
classes that provide tools with configuration information. They can be
specified at the assembly level, the module level, the class level, and even
at the method level. The first attribute specifies the name of the private
key file. The second attribute specifies the version number of the
assembly. Notice that the third line in the code asks the compiler to use
another assembly called System.Runtime.CompilerServices. We need to
include a reference to that assembly, because that is where the
AssemblyKeyFile and AssemblyVersion attributes are declared. The
compiler will look for these attributes and, if it finds the AssemblyKeyFile
attribute, it will run a cryptographic algorithm on the manifest for the code
using the private/public key file stored in the file you specified. The
visible effect of signing your code is that you will now see an originator

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

(or public key) in the assembly's manifest. The AssemblyVersion attribute is


very important. The version number follows the following format:
major.minor.build.revision
You will see how it is used shortly. Once you have modified the code, you
must build the assembly again. At the time of this writing there was a
problem with the VB.NET compiler; it would ignore the AssemblyVersion
attribute. This should be fixed in Beta 2. For now, there is a workaround:
the VB.NET compiler lets you specify the version number as a command-
line parameter. Compile the hellogreeting assembly as follows:
vbc /t:library hellogreeting.vb /version:1.0.0.0
Because we have changed the version number of the assembly and
assigned the assembly a strong name, you must rebuild the client program.
Rebuilding is necessary so that the client program's manifest will contain
information about the strong name and version of the hellogreeting
assembly.
If you look at the manifest of the client program once again with
ILDASM, you will notice that two things have changed for the external
reference to the hellogreeting entry:
.assembly extern hellogreeting
{
.originator = (A9 FE 7C 65 17 60 13 3E ) // ..|e.`.>
.hash = (4E 89 F0 45 B1 1E 0F 4B 31 65 BB 75 9D A8 71 6E // N..E...K1e.u..qn
64 0A 93 27 ) // d..'
.ver 1:0:0:0
}
As you can see, the client program has a dependency on an assembly
named hellogreeting. Furthermore, it expects the assembly to come from a
certain originator (this is the public key token that was generated from the
hash of the public key at the time the client program was compiled), and it
expects a certain version number, 1.0.0.0. You will see how important the
version number is shortly.
Once the assembly has been signed, it can be moved to the GAC. You do
this with another program, Gacutil.exe. Gacutil.exe has a very simple set
of commands. You can see the complete list by using /? as the command-
line switch. The command-line switch to add an assembly to the GAC is -
i. Enter the following command at the command prompt:
Gacutil -i hellogreeting.dll
Adding the assembly to the GAC turns the assembly into a public shared
assembly. If you use Windows Explorer, you should be able to see the list
of shared assemblies under \WINNT\Assembly, as depicted in Figure 11-3.
Figure 11-3. Global Assembly cache

You can now delete hellogreeting.dll and run helloworld.exe. The program
runs without the DLL being in the same directory, because a copy has
been moved to the GAC.
Suppose that we change the greeting to "New Hello" + sName, as follows:

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

<Assembly: AssemblyKeyFile("widgetsusa.snk")>
<Assembly: AssemblyVersion("1.0.0.1")>

Imports System.Runtime.CompilerServices

Public class HelloSupport


Shared Public Function GetGreeting(ByVal sName As String) As String
return "New Hello " + sName
End Function
End Class
Next, rebuild with the same version number as before. If you run the
program again, you will notice that the client program still outputs "Hello
World"--it uses the assembly in the cache. What if we recompile with the
version number of 1.0.0.1? Then the story changes. The client program
uses the local assembly instead of the one in the GAC. We can move the
new version to the GAC as follows:
gacutil -i hellogreeting.dll
This is the exact same command as before--nothing interesting, unless you
look at the assembly list in WINNT\Assembly once again (see Figure 11-4).
Figure 11-4. Global Assembly cache with two versions of the assembly

You can see that there are two versions of the assembly in the cache. If
you run the client program again, it would use the latest version. However,
let's make another change to the code:
<Assembly: AssemblyKeyFile("widgetsusa.snk")>
<Assembly: AssemblyVersion("2.0.0.0")>

Imports System.Runtime.CompilerServices

Public class HelloSupport


Shared Public Function GetGreeting(ByVal sName As String) As String
return "New Hello 2.0.0.0" + sName
End Function
End Class
This time, build the assembly as Version 2.0.0.0. Once you build the DLL,
if you run the client once again, the client will use the version of the DLL
in your local directory. The change occurs when you move this version to
the GAC and delete the local copy. The GAC, according to our examples,
so far contains three versions of the DLL. The latest version is 2.0.0.0. If
you run the client program this time, you should see that the client
program continues to use Version 1.0.0.1. What has happened is that the
CLR is conscious of the difference between a major/minor release and a
build/revision release. If your version number differs only by
build/revision number, then by default the CLR assumes you can use a
later version. On the other hand, if your version number differs by
major/minor numbers, then the CLR assumes that you are better off with
your previous version--it only gives you the version that matches the
major/minor numbers exactly.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

In either case, by default, if you have a private copy in your directory, the
CLR assumes that you put it there because that's the one you want to use.
You can control the process of locating a certain assembly with a
configuration file. The following code shows an application configuration
file:
<BindingPolicy>
<BindingRedir Name="hellogreeting"
Originator="a9fe7c651760133e"
Version="*" VersionNew="2.0.0.0"
UseLatestBuildRevision="yes"/>
</BindingPolicy>
You would save the code in a file with the same name as the executable
but with the extension .CFG and in the same directory as the executable.
Notice that the syntax of the application configuration file is XML. The
<BindingPolicy> tag has a <BindingRedir> subtag. This subtag has a Name
attribute. The Name attribute points to the name of the assembly that needs
to be resolved, in this case the DLL file. The next attribute is Originator;
this is a public key token. The easiest way to obtain this number is from
the GAC. If you look back at Figure 11-4, you will see the Originator
field. The third attribute is Version, the version number. This attribute lets
you redirect any requests for a certain revision number. In this case, we
are redirecting any requests. The next attribute is VersionNew; this is the
version number we would like to use. Finally, there is a
UseLatestBuildRevision attribute, which is set to yes in our example. This
attribute says that if there is a later build differing only by build/revision,
then that one should be used. Therefore, if the GAC contained 2.0.1.0, it
would use that one.
What if you wanted to use Version 1.0.0.0? After all, that is exactly the
version that the client program was built in. By default, the resolver uses
the assembly with the latest build revision. You could do that two ways.
The first way is to change the VersionNew attribute to 1.0.0.0 and set the
UseLatestBuildRevision attribute to no, as shown in the following
configuration file:
<BindingPolicy>
<BindingRedir Name="hellogreeting"
Originator="a9fe7c651760133e"
Version="*" VersionNew="1.0.0.0"
UseLatestBuildRevision="no"/>
</BindingPolicy>
The second way is to use what is called safe mode. Safe mode uses a
different subtag named <AppBindingMode>, as illustrated in the following
code:
<BindingPolicy>
<BindingMode>
<AppBindingMode Mode="safe"/>
</BindingMode>
</BindingPolicy>
This code shows the <AppBindingMode> subtag's Mode attribute set to safe;
by default, it is set to normal. When you use safe mode, the resolver tries

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

to locate an assembly with the exact same version number as the one used
to compile the client program. Thus, running helloworld would produce
the original output, "HelloWorld." If you were to set the mode equal to
normal or just delete the configuration file, you would obtain the second
output, "New Hello World." To obtain the Version 2.0.0.0 output, you
would have to use the redirection technique shown earlier and redirect any
assembly (or any 1.0.0.0 assemblies in our case) to Version 2.0.0.0.
At this point, you should have a good feel for how to build assemblies and
how to turn private assemblies into shared assemblies. Now let's discuss
some of the more exciting language features in VB.NET.

VB.NET Features
Now that you know the basics of running the VB compiler and the
difference between private assemblies and shared assemblies, let's discuss
some of the new features in VB.NET. In many ways, VB.NET is a
different language. The language has been extended to have a number of
object-oriented features that it did not have before, such as method
inheritance, method overloading, and exception handling.
Inheritance
In Chapter 2, you learned the difference between interface inheritance and
code inheritance. VB 6 enabled you to do only one type of inheritance--
interface inheritance. VB.NET adds support for the second type of
inheritance, code inheritance. The way it works is that you first create a
base class. In our ongoing example of a banking application, let's suppose
that your design accounted for two main classes: a Checking class and a
Savings class. It is likely that these two classes have functionality in
common. In fact, a better design may be to create a base class named
Account from which these two classes are derived. The following code
shows an application that declares three classes: Account, Checking, and
Savings. The Checking and Savings classes inherit all their functionality
from the Account class:
Public class Account
Dim m_Balance As Decimal
Public Sub MakeDeposit(ByVal Amount As Decimal)
m_Balance += Amount
End Sub

Public ReadOnly Property Balance( ) As Decimal


Get
return m_Balance
End Get
End Property
End Class

Public Class Checking


Inherits Account
End Class

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Public Class Savings


Inherits Account
End Class

Public Class App

Shared Sub Main( )


Dim Acct As Account = new Checking
Call Acct.MakeDeposit(5000)
System.Console.WriteLine(Acct.Balance)
End Sub

End Class
The Account class has a member variable, called m_Balance, that stores a
Decimal (Decimal replaces the VB 6 Currency type). The class has a
MakeDeposit method that accepts an amount in its parameter and adds the
amount to the balance. The class also has a Balance property that reports
the internal balance. The code then declares two subclasses, Checking and
Savings, that inherit all their functionality from the Account class. The rest
of the code declares the class that will host the Sub Main function. In Sub
Main we are declaring a class of type Account and assigning an instance
of the class Checking. Why can we assign an instance of Checking to a
variable of type Account? Because inheritance establishes an "IS A"
relationship between the base class and the derived class. For all purposes,
a Checking class "IS AN" Account. The opposite is not true; Account
classes are not Checking classes.
The fact that Checking is derived from Account means that we can send
an instance of Checking to a function that receives an Account as its
parameter. Consider the following code:
Module GenFunctions
Public sub ReportBalance(ByVal AnyAccount As Account)
System.Console.WriteLine(AnyAccount.Balance)
End Sub
End Module
The preceding code defines a module. Even though it seems as if the
module provides a way to export standalone functions without having to
declare a class, in reality, VB changes the module to a class and makes
any functions inside of it shared. The previous code declares a function
called ReportBalance that reports the balance of any class derived from
Account. As expected, you can send the function an instance of the
Checking class or an instance of the Savings class. In fact, the following
code should work as expected:
Public Class App
Shared Sub Main( )
Dim check As Checking = new Checking
check.MakeDeposit(500)
Dim sav As Savings = new Savings
sav.MakeDeposit(100)
Call ReportBalance(check)
Call ReportBalance(sav)
End Sub

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

End Class
Suppose that there is a request for the Savings class to work differently.
The MakeDeposit method should add $10 to every deposit (this is a very
good Savings account). That means that we have to change the
functionality of the MakeDeposit method. The following code shows how
you might do this. First you have to modify the MakeDeposit method in
the Account class as follows:
Public class Account
Dim m_Balance As Decimal
Public Overridable Sub MakeDeposit(ByVal Amount As Decimal)
m_Balance += Amount
End Sub
Notice that there is a change in the MakeDeposit method--it has the
keyword Overridable. It is necessary for the developer writing the Account
class to have foresight and add the word Overridable to any methods that
may be modified in subclasses. The code for the Savings class has also
been modified as follows:
Public Class Savings
Inherits Account
Public Overrides Sub MakeDeposit(ByVal Amount As Decimal)
Amount += 10
MyBase.MakeDeposit(Amount)
End Sub
End Class
In the Savings class we add the MakeDeposit method with the attribute
Overrides. The code adds 10 to Amount, then forwards the call to the base
implementation of MakeDeposit. This is done with the MyBase object.
MyBase is a new global object that references your most direct base class.
The runtime supports only single inheritance. All classes must derive from
at most one class, and all classes must derive from System.Object. If you
do not specify a class to derive from in code, then the compiler
automatically makes System.Object the base class.
Suppose that we add to the Account class a withdrawal method that
subtracts a certain amount from the balance:
Public Overridable Sub MakeWithdrawal(ByVal Amount As Decimal)
If m_Balance - Amount >= 0 Then
m_Balance -= Amount
End If
End Sub
In the preceding example, MakeWithdrawal subtracts the amount from the
balance only if the resulting balance is greater than or equal to 0. What if
the MakeWithdrawal method in the Checking account needs to work
differently? Suppose that the Checking account allows a client to
overdraw the account up to $1,000. If you were to write the following
code, you would get an error:
Public Class Checking
Inherits Account
Public Overrides Sub MakeWithdrawal(ByVal Amount As Decimal)
If m_Balance - Amount >= -1000 Then
m_Balance -= Amount

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

End If
End Sub
End Class
The code is attempting to access the m_Balance variable, which is marked
as Private in the Account class. The problem is that m_Balance must be
private to the client but public to the derived class. For this reason,
VB.NET has a third category named Protected. You must change the
declaration of the m_Balance variable as follows:
Public class Account
Protected m_Balance As Decimal
You can specify that it is illegal to write a derived class from your class
with the NotInheritable attribute. NotInheritable results in what the runtime
refers to as a sealed class. For example, we could have said that the
Checking class cannot be inherited as follows:
Public NotInheritable Class Checking
On the other hand, we may want to prevent a developer from creating
instances of the Account class directly. It may be our rule that a developer
must create instances of a derived class. This is done with the MustInherit
attribute, as in the following:
Public MustInherit Class Account
This doesn't prevent a client from declaring a variable of type Account,
only from creating instances of the Account, as in var = new Account. It is
possible also to force a developer into not only creating derived classes,
but also overriding certain methods. For example, we could have stated
that every derived class must override the MakeWithdrawal method. Of
course, that change produces a number of changes. If a developer must
always override the MakeWithdrawal method in the Account class, then
the MakeWithdrawal method should not have any code in the Account
class, and the compiler should issue an error if there is code. In addition,
since everyone must override the method, a client cannot just create an
instance of the class. Therefore, the class must also be marked with the
MustInherit attribute. Suppose that we marked every method with the
MustOverride method; we would have the equivalent of an interface.
An interface is a class in which every method must be implemented in a
concrete class. There is a shorthand for defining interfaces in VB.NET
using the keyword Interface instead of the Class keyword. The IAccount
interface can be defined in VB.NET as follows:
Public Interface IAccount
Overloads Sub MakeDeposit(ByVal Amount As Decimal)
Overloads Sub MakeDeposit(ByVal Source As Account)
ReadOnly Property Balance( ) As Decimal
End Interface
The preceding definition shows the new way of defining interfaces in
Visual Basic. Notice that the methods in the interface do not have an End
Sub statement (or its equivalent). Also notice that there is no Public
attribute--that is because all the methods must be public in an interface.
Another interesting thing is that there is method overloading in an

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

interface (a feature you will learn about shortly). This code is roughly
equivalent to the following:
Public MustInherit Class IAccountClone
Overloads MustOverride Sub MakeDeposit(ByVal Amount As Decimal)
Overloads MustOverride Sub MakeDeposit(ByVal Source As Account)
ReadOnly MustOverride Property Balance( ) As Decimal
End Class
The only difference is that interfaces do not have a constructor--code that
is executed when an instance of a class is instantiated. The fact that they
do not have constructors means that they cannot be subclassed except by
an entity that would also not have a constructor. This means you cannot
inherit from an interface unless you are an interface. Classes can produce
subclasses, and interfaces can produce subinterfaces (if that were a term).
For example, interfaces can derive from other interfaces, as in the
following example:
Public Interface IAccount2
Inherits IAccount
Sub CloseAccount
End Interface
You are still required to implement the entire interface. If you implement
the preceding interface in a class, the class will support both the IAccount
and IAccount2 interface. Let's take a look at how implementing an interface
has changed slightly:
Public Interface ISaveToDisk
Public Sub Save( )
End Interface

Public Class Checking


Implements ISaveToDisk
Public Sub Bark( ) Implements ISaveToDisk.Save
End Sub
End Class
Notice that you now have to specify in each method implementation what
method in the interface you are implementing. You do this by declaring
the method, then adding Implements Interface.Method at the end of the
method declaration.
Method Overloading
A new feature in VB is the ability to do method overloading. Method
overloading involves having several implementations of the same method,
each method with a different set of parameters. For example, you may
want to add to the Account class a second MakeDeposit method that takes
as a parameter an instance of a second Account object. The idea is that the
client can transfer money from one account to another by just calling the
MakeDeposit method in the Account receiving the money and passing the
Account where the money will come from as the parameter. As in C++,
you cannot overload a method if the only difference is the return value--at
least one of the parameters must be different, or the number of parameters
must be different. The following code shows the two MakeDeposit
methods:

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Public Overridable Overloads Sub MakeDeposit(ByVal Amount As Decimal)


m_Balance += Amount
End Sub
Public Overridable Overloads Sub MakeDeposit(ByVal Source As Account)
m_Balance += Source.Balance
Source.m_Balance = 0
End Sub
You do not have to override every form of the overloaded method in a
derived class.
Value Versus Reference Type
By default, to use a class, you must declare a variable of the type of class
you wish to use and create a new instance of the class. In VB 6 there was a
difference between a UDT and a class. The same is true for VB.NET. In
VB.NET you can define a UDT using the keyword Structure, as in the
following:
Public Structure MyPoint
Public x As Long
Public y As Long
End Structure
Structures are classes that are derived from a class called
System.ValueType. Ordinarily, when you create a class that is not derived
from System.ValueType, the variable that holds the instance of the object
really contains a pointer to a vptr that points to the location in memory
where your data is stored. You must create an instance of the class with
the New operator. If your class is derived from System.ValueType, the
runtime treats your class differently. With a structure, you do not have to
call New--when you Dim a variable of the structure type, the system
allocates space for the structure automatically and the variable points to
the storage directly. If you send in an instance of a structure as a parameter
(marked as ByVal), the contents of the structure are put on the stack, and
the receiving procedure gets a copy of the structure. If you pass a class
instance to a function as a parameter (also marked as ByVal), then you are
passing the pointer to the class' storage, and the receiving side gets a copy
of the pointer. In the case of the structure, any changes made to the
members of the structure do not change the original values in the caller's
structure. In the case of the class reference, if you change the value of a
class variable, the changes affect the caller's instance of the class, since
both the caller and the function share the same instance of the class. Other
examples of classes that derive from System.ValueType are the Integer,
Decimal, and Boolean classes--any basic datatype. (Yes, Integer, Decimal,
and Boolean are actually classes in the runtime that are ultimately derived
from System.Object.)
Structures have more functionality than Types had in VB 6. Structures can
now have methods and can implement interfaces. Perhaps the most
interesting new feature of structures is Boxing. Boxing creates a clone of
your structure whenever a reference type is needed. Consider the
following code:
Public Interface IAccount

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Sub MakeDeposit(ByVal Amount As Decimal)


ReadOnly Property Balance( ) As Decimal
End Interface

Public Structure AccountInfo


Implements IAccount
Public m_Balance As Decimal
Public Sub MakeDeposit(ByVal Amount As Decimal) Implements _
IAccount.MakeDeposit
m_Balance += Amount
End Sub
Public ReadOnly Property Balance( ) As Decimal Implements _
IAccount.Balance
Get
return m_Balance
End Get
End Property
End Structure

module modMain
Sub Main( )
Dim Acct As AccountInfo
Acct.MakeDeposit(500)
Dim AcctBoxed1 As IAccount
AcctBoxed1 = Acct
AcctBoxed1.MakeDeposit(300)
Dim AcctBoxed2 As IAccount
AcctBoxed2 = AcctBoxed1
AcctBoxed2.MakeDeposit(300)
System.Console.WriteLine("Acct.Balance=" & Acct.Balance)
System.Console.WriteLine("AcctBoxed1.Balance=" & AcctBoxed1.Balance)
End Sub
end module
This code defines an interface called IAccount. There should be no surprises
in the interface definition if you have been following along in this book.
The interface, as usual, has a MakeDeposit method and a Balance
property. I am implementing the interface in a structure called
AccountInfo. This structure has a member called m_Balance to store the
balance. It also implements both the MakeDeposit method and the Balance
property.
The code example begins by allocating an instance of the AccountInfo
structure. Remember that you do not have to call New to allocate a
structure's memory. The code then calls the MakeDeposit method to
increase the Balance to 500. Next, the code declares a variable named
AcctBoxed1 of type IAccount. The code then uses the AcctBoxed1
variable to make another deposit for $300. This is where it gets tricky. In
the runtime, an interface is a reference type; it is not a value type, like the
AccountInfo structure. So when you assign the reference type to the value
type, the system creates a copy of the structure and assigns the pointer of
the copy to the reference type variable. In the preceding example, after
setting AcctBoxed1 to Acct, there will be two copies of the data members
in memory. The second copy has a starting balance of 500 because that's

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

what the structure had before the copy was made. However, when you call
the MakeDeposit method through the structure, the values of the original
remain at 500, while the balance of the copy increases to 800. The code
then creates a second reference type, AcctBoxed2 from AcctBoxed1.
Because both are reference types, AcctBoxed2 is set to point to the same
memory as AcctBoxed1. Therefore, after calling MakeDeposit for the
third time, the original balance in Acct1 is still at $500, but the balance of
the reference type is now at $1,100. In fact, when you output the values in
the last two lines of code, the value for Acct.Balance will be reported as
500, and the value for AcctBoxed1.Balance will be 1100.
Delegates
VB 6 had a limitation with function pointers. It was possible to get the
address of a function in memory with the AddressOf operator. The AddressOf
operator returned a Long with the location in memory of the function, but
something that I always envied C++ for was that you couldn't take that
Long value and turn it back into a function. For example, C++ lets you
define what is called a function pointer declaration. You can define a
function signature (function name, parameters, and return value) and use
the definition as a datatype. With this datatype, you can declare a variable
to hold the address to a function with the same signature. Then you can
make a method call through the variable. In VB 6 you couldn't do this.
VB.NET now lets you create function pointer datatypes; they are called
delegates. Delegates are classes derived from System.Delegate. The
following example shows how to define a delegate. Suppose that you want
to create a general function for our banking server that reports the balance
but that, instead of accepting Account or Checking or Savings, will accept
any class that has a subroutine to report the balance. To do this, we can
define a delegate with the signature for the ReportBalance function as
follows:
Delegate Sub ReportBalanceSig( )
The delegate declaration defines a datatype that can be set to the AddressOf
any function with the same signature. Let's suppose that the BankServer
application has the following classes:
Public Class Checking
Public Sub CheckingBalance( )
System.Console.WriteLine("CheckingBalance")
End Sub
End Class
Public Class Savings
Public Sub SavingsBalance( )
System.Console.WriteLine("SavingsBalance")
End Sub
End Class
Notice that the two classes, Checking and Savings, each has a method to
report the balance, CheckingBalance and SavingsBalance, respectively.
These two methods serve the same purpose but do not have the same name
and are not implementing any interface. They do, however, have the same

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

signature. Let's now define the ReportBalance function and the client code
in a module:
module modMain
Sub ReportBalance(ByVal Func As ReportBalanceSig)
Func
End Sub
Sub Main( )
Dim Check As Checking = new Checking
Dim Sav As Savings = new Savings
ReportBalance(AddressOf Check.CheckingBalance)
ReportBalance(AddressOf Sav.SavingsBalance)
End Sub
End module
The first function in modMain, ReportBalance, accepts as a parameter a
function of type ReportBalanceSig. It does not matter what the function is
called; it just needs to have the same signature as the delegate. Notice that
the code in ReportBalance simply calls the routine that was sent in. (It
looks a little awkward because calling the function can be done by just
writing the name of the variable holding the function pointer.) The second
function in the module is Sub Main. In Sub Main we create an instance of
the Checking class and then an instance of the Savings class. Then, the
function calls ReportBalance, passing the address of the CheckingBalance
function, followed by a second call to the ReportBalance function passing
the address of the SavingsBalance function.
Constructors and Finalizers
There is no more Class_Initialize or Class_Terminate. Every class not
derived from System.ValueType and not defined as a structure or an
interface has a default constructor. The default constructor has the name
New and takes no parameters; it is called when the developer uses the New
operator. For example, the following code shows how to write code for the
default constructor in the Checking class:
Public Class Checking
Private m_Balance As Decimal

Public Sub New


m_Balance = 0
End Sub

Public Function ReportBalance( ) As Decimal


return m_Balance
End Function
End Class

module modMain
Sub Main( )
Dim Acct As Checking = New Checking
End Sub
end module

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

In this code example, the runtime calls the New method in the class when
the developer creates an instance of the class. In this case, this happens in
Sub Main.
A more interesting feature is that you can now add parameterized
constructors. For example, it may make more sense to require the
developer using the Checking class to create the class with an initial
balance as follows:
Public Class Checking
Private m_Balance As Decimal

Public Sub New(ByVal InitialBal As Decimal)


m_Balance = InitialBal
End Sub

Public Function ReportBalance( ) As Decimal


return m_Balance
End Function
End Class

module modMain
Sub Main( )
Dim Acct As Checking = New Checking(500)
End Sub
end module
In this code, there is a definition for a parameterized constructor. This is
done by adding a New function that receives a parameter, in this case the
initial balance. As soon as you add a parameterized constructor, the
compiler no longer adds the default constructor. This means that the
developer cannot just say New Checking without passing a parameter. As
you can see in the code for Sub Main, the code creates an instance of
Checking passing in the initial balance of $500.
As with other functions, you may overload the constructor. In fact, if you
wish to have both the parameterized constructor and the default
constructor, you could rewrite the class as follows:
Public Class Checking
Private m_Balance As Decimal

Public Overloads Sub New


m_Balance = 0
End Sub

Public Overloads Sub New(ByVal InitialBal As Decimal)


m_Balance = InitialBal
End Sub
There are things to watch for when your class derives from a base class.
Constructors are not inherited. If the base class has a default constructor,
the system will call the base constructor first before calling your
constructor. However, if the base class does not have a default constructor,
then you must call a constructor for the base class in your constructor, and

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

the call must be the first call in your constructor. The following code
shows you how to do this:
Public Class Account 'base class
'this class only has a parameterized constructor
Public Sub New(ByVal InitialBal As Decimal)

End Sub
End Class

Public Class Checking 'derived class


Inherits Account
Public Sub New(ByVal InitialBal As Decimal)
'call the base class' constructor
MyBase.New(InitialBal)
End Sub
End Class

module modMain
Sub Main( )
Dim Acct As Account = new Checking(500)
End Sub
end module
The Account class does not have a default constructor. Therefore, the
runtime cannot call the base constructor for your class automatically. You
must add a constructor to the derived class, then call the base constructor
for your class programmatically. You can refer to your direct base class by
using the MyBase object. Notice that you must call the base constructor
first before doing anything else in the derived constructor.
Just as you can write constructors for your classes, you can also write
finalizers. Finalizers are a little different from destructors because they do
not necessarily happen when a client releases your object. When the client
creates an instance of your class, the object becomes part of the global
managed heap. When all clients release their instances of your object, your
object becomes marked for garbage collection. The garbage collector will
call your finalizer when it is time to destroy your object, and that may
happen any time after all clients have released their references to your
object but not sooner. To add a finalizer to the class, you must write a
Finalize subroutine. (Interestingly, Finalize is an overridable method in
System.Object.) The following code shows how to add a Finalize method:
Public Class Checking

Protected Overrides Sub Finalize( )


'do cleanup code here
End Sub

End Class

Exception Handlers
Error catching in VB 6 was done with the Err object and either On Error
Resume Next or On Error Goto. If you found this aspect of programming very
limiting, you will be glad to know that VB.NET supports exception

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

handling. With exception handling, you write try...catch blocks. The try part
of a try...catch block contains the code that you want to execute. Outside of
the try block, you write handlers for different kinds of exceptions.
Exceptions are classes derived from System.Exception. Because every
exception is generated from System.Exeception, you can also write a
general exception handler to handle any exception you may get. The
System.Exception class has properties that provide very rich error
information. Part of the try...catch block is the finally block. The finally block
executes at the end of the function whether an exception occurs or not.
This block lets you do cleanup for the function. The following code shows
how to add a try...catch...finally block:
Public Interface IAccount
End Interface
Public Interface ISaveToDisk
End Interface
Public Class Checking
Implements IAccount
End Class
module modMain
Sub Main( )
Dim Acct As IAccount = new Checking
Try
Dim Sav As ISaveToDisk = CType(Acct,ISaveToDisk)
Catch e As System.InvalidCastException
System.Console.Writeline("The cast failed")
Catch e As System.Exception
System.Console.WriteLine(e)
finally
System.Console.WriteLine("cleanup code here")
End Try
End Sub
End module
This code defines two interfaces, IAccount and ISaveToDisk. The code also
defines a class called Checking. The Checking class implements only
IAccount. The code in Sub Main creates an instance of the Checking class
by asking for the IAccount interface, then tries to convert the type to
ISaveToDisk using the new VB.NET command CType. Since the class does
not support the second interface, the system generates an exception:
System.InvalidCastException. The code places the cast attempt code
within a Try block. Notice that there are different exception handlers after
the code inside the Try block. The first exception handler handles
System.InvalidCastExceptions only. If the code generates this exception
(and the previous code will), the line System.Console.WriteLine ("The
cast failed") is executed followed by the code in the Finally block. After the
handler for System.InvalidCastException, the code has a general exception
handler. This is done with a catch section that either has the word Catch
by itself or by catching the System.Exception class.
In VB 6, generating an error was done with Err.Raise. In VB.NET you can
create your own exception class derived from
System.ApplicationException. System.ApplicationException is derived

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

from System.Exception. Then use the Throw command to generate the


exception. The following code shows you how to define your own
exception class and generate it:
Public class MyException
Inherits System.ApplicationException
Sub New( )
MyBase.New("My Error Description")
End Sub
End class
Module modMain
Sub Main( )
Dim e As MyException = new MyException
Throw e
End Sub
End Module

Mixing COM+ and .NET


Many of you are feeling the urge to experiment with .NET components
and like the new features in Visual Basic. Most likely, you have already
made an investment in COM and would like to know how to use your
existing COM components in VB 6 with the new .NET components.
Microsoft is very aware of this need and has built in capabilities for
mixing the two.
As you know from the previous sections in this chapter, code that runs in
the CLR is managed code. It is in IL, and it is run by the common
language runtime. The runtime has its own techniques for allocating
objects and managing things like thread allocations and so forth. In the VB
6 world, code was unmanaged. The compiler translated your code to
native code that the processor could run. Thus, if we are going to mix
these two worlds, we must create intermediary components that enable us
to travel to and from managed space into unmanaged space. Let's address
the idea of using VB.NET components from your Visual Basic 6 code
first.
Using VB.NET Components with VB 6
Microsoft provides a tool called tlbexp.exe with the .NET SDK that
enables you to create a type library (.TLB) file from an assembly manifest
but does not register it. A type library (especially an unregistered one)
does not really provide the functionality of a COM server. If we were to
try to use a managed DLL as a COM server, the managed DLL would
have to have the functionality of an in-process COM server. For example,
it would have to have the four entry points to any COM DLL:
DllRegisterServer, DllUnregisterServer, DllGetClassObject, and
DllCanUnloadNow. It would also have to add the necessary keys to the
registry for the SCM to load the DLL. Remember that the SCM must find
the CLSID followed by the InprocServer32 key pointing to the DLL.
Unfortunately for COM developers, CLR assembly DLLs do not have
COM entry points, nor are they self-registering. However, Microsoft has

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

made the CLR execution engine (MSCorEE.dll ) a full COM server. It can
load your assembly at runtime and provide you with a proxy to your .NET
classes. Microsoft provides another tool called RegAsm.exe, also included
with the .NET SDK, that enables you to register your .NET assemblies.
Let's look at the process. Consider the following VB.NET code:
Public Class Inventory
Private m_Quantity As Integer

Public Sub AddWidgetToInventory(ByVal Amount As Integer)


m_Quantity += Amount
End Sub

Public ReadOnly Property Quantity( ) As Integer


Get
Return m_Quantity
End Get
End Property

End Class
This code declares a single class named Inventory. The Inventory class has
a private member named m_Quantity; it stores the number of widgets in
inventory. The class also has a public method for adding widgets to
inventory, cleverly named AddWidgetToInventory, as well as one
property for retrieving the quantity. You can save the preceding code as
inventory.vb and compile it as a DLL with the following command:
vbc /t:library inventory.vb
Let's suppose that we would like to use the Inventory class in a VB 6
client program. The best way to do this is to use the RegAsm.exe tool.
From a command prompt, locate the inventory.dll .NET assembly and
enter the following command:
Regasm inventory.dll /tlb:inventory.tlb
I mentioned earlier that Microsoft has another tool called tlbexp.exe that
creates a type library. However, that tool does not automatically register
the type library, nor does it add registry keys for a client program to be
able to create an instance of your .NET classes. RegAsm does all of these.
It adds registry keys so that your classes can be instantiated from COM
and, if you use the /tlb command-line switch, it also creates a type library
for all public classes in the assembly and registers the type library. After
you run the RegAsm.exe tool, you should be able to use your .NET
assemblies from VB 6.
RegAsm plays a trick with the registry. If you look at the registry, you
should see that RegAsm has added COM registry keys (see Figure 11-5).
Figure 11-5. Registry keys added by RegAsm

First of all, in Figure 11-5, the CLSID key with the visible subkeys
contains the class identifier of our assembly. In fact, your CLSID should
be the same as mine, even if you typed the program from scratch. If this

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

doesn't sound strange to you, then you may need to read Chapter 3 again.
In VB 6, if two people compile the same program on different machines,
VB generates different GUIDs by default unless there is an existing type
library that both machines could reference using project compatibility and
binary compatibility. VB 6 uses CoCreateGUID to generate a unique
number, and, because it was guaranteed to be unique, it would be different
from machine to machine (it would actually be different on the same
machine if you compiled twice with no compatibility).
VB.NET does not use GUIDs in the same way as VB 6. They are present
just for compatibility with COM, but internally they are never used.
What's different about VB.NET is that it assigns a CLSID to each class,
but it uses a different algorithm that is based on the combination of the
name of the assembly (in our case, inventory) and the name of the class (in
our case, also inventory). Therefore, if we name the class and the
assembly the same thing, we are going to end up with the same CLSID.
This has the potential of two companies having a conflict if they name
their assemblies and classes the same. There is a solution to this--the
developer can assign the class a specific GUID using the GuidAttribute
class in System.Runtime.InteropServices.
Notice from Figure 11-5 that the InprocServer32 key has a number of values.
First, notice that the path to the COM server points to MSCorEE.dll.
MSCorEE.dll is the DLL that is responsible for loading your assembly into
managed space. However, it also serves as a COM entry point. When a
COM client requests a class through DLLGetClassObject, MSCorEE
looks at the other values in InprocServer32, in particular the Assembly value
entry. This value tells MSCorEE the name and location of your assembly,
the version, localization information (such as EN_US), and the originator.
You should know from reading the earlier sections how to assign an
originator to the assembly by compiling it with a private key.
There is a little inconvenience with using RegAsm.exe. If you notice, there
is no exact path to your assembly. You could manually set this path in the
registry (as seen in Figure 11-6), although this capability may be removed
in later versions of the SDK.
Figure 11-6. Adding the exact path to the assembly in the registry

Another solution, in fact the optimal one, is to sign the assembly with a
private key and add it to the global assembly cache. Once the assembly is
in the GAC, you do not have to specify a path to use the assembly. To use
the assembly from VB 6, all you have to do is create a program as usual
and find the assembly name in the Project References dialog box.
Using VB.NET for versioning VB 6 components
A good use for RegAsm.exe and VB.NET is to serve as a replacement for
IDL. If you are hesitant to use IDL to version your components because it
is yet a new syntax to learn and it is like C++, then you may want to

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

define your interfaces and manage your GUIDs in VB.NET and use it for
versioning purposes. Let's look at a short example of how to define your
interfaces in VB.NET and manage the GUIDs using attributes. Examine
the following VB.NET code:
<Assembly: System.Runtime.InteropServices.Guid("3C53B8E3-81FC-4645-B65F-
ACABE77A79D0")>

Imports System.Runtime.InteropServices

Interface <Guid("1393732E-8D27-431a-A180-8EDA0E4499E2"), _
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)> IAccount
Sub MakeDeposit(ByVal Amount As Currency)
ReadOnly Property Balance( ) As Currency
End Interface
The code is in VB syntax. What makes this code different from regular
VB is the use of attributes throughout the code. These attributes are used
by the various tools, like tlbexp, to dictate how the tool ought to do its job.
In this case, attributes are used to control how the type library is
generated. Notice that the library name is the name of the assembly. The
LIBID for this library is the GUID specified with the Guid attribute at the
Assembly level. The Guid attribute is part of
System.Runtime.InteropServices. Actually, all the attributes used in the
VB.NET code are part of the same assembly. The interface also uses the
Guid attribute to assign an IID to the interface. In addition to the Guid
attribute, it uses the InterfaceType attribute to tell tlbexp that the interface
should be derived from IUnknown (the default is to make it a dual
interface).
Once you compile the preceding code as a DLL, you can run the tool
tlbexp.exe to generate the type library. Suppose you named your DLL
bankinterfaces.dll; you can generate a type library entering the following
command in console mode:
tlbexp.exe bankinterfaces.dll
By default, the tlbexp tool uses the root filename of the DLL to name the
type library; thus, the resulting file would be bankinterfaces.tlb. The
resulting type library source is as follows:
// Generated .IDL file (by the OLE/COM Object Viewer)
//
// typelib filename: bankinterfaces.tlb

[
uuid(3C53B8E3-81FC-4645-B65F-ACABE77A79D0),
version(1.0)
]
library BankInterfaces
{
// TLib : // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
importlib("stdole2.tlb");

// Forward declare all types defined in this typelib


interface IAccount;

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

[
odl,
uuid(1393732E-8D27-431A-A180-8EDA0E4499E2),
oleautomation,
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},
"BankInterfaces.IAccount")

]
interface IAccount : IUnknown {
HRESULT _stdcall MakeDeposit([in] CURRENCY Amount);
[propget]
HRESULT _stdcall Balance([out, retval] CURRENCY* pRetVal);
};
};
Tlbexp.exe creates a type library but does not register it, but in Chapter 6,
you learned that you could use regtlib.exe to register the type library. After
you register the type library, you could use it like any other type library in
Visual Basic. First, add it to your project through the Project References
dialog box, then implement it in a concrete class. The interface methods
resulting from the preceding VB.NET code would look like the following
in VB 6:
Option Explicit

Implements IAccount

Private Property Get IAccount_Balance( ) As Currency


End Property

Private Sub IAccount_MakeDeposit(ByVal Amount As Currency)


End Sub

Using VB 6 Components with VB.NET


Going from .NET to VB 6, you needed to create a type library; going from
VB 6 to .NET, you must generate an assembly. In the previous section,
you learned about tlbexp.exe. In this section, you'll learn about tlbimp.exe.
Tlbimp.exe creates an assembly from the definition of a type library.
Consider the following VB 6 code:
Option Explicit

Private m_Balance As Currency

Public Sub MakeDeposit(ByVal Amount As Currency)


m_Balance = m_Balance + Amount
End Sub

Public Function Balance( ) As Currency


Balance = m_Balance
End Function
This is the usual Checking class. Let's say for the sake of argument that
this code represents a class in BankServer.DLL. You can create an
assembly that contains the definitions in your type library using tlbimp. To

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

do so, you would enter the following command in a command-prompt


window in the same directory as your DLL:
tlbimp BankServer.dll /out:BankServerAsm.dll
It is very straightforward; you run the tool, and it will create an assembly
you can reference from your .NET program. For example, you may write a
client program as follows:
module mainmod
public Sub Main
Dim Acct As New BankServer.Checking
Acct.MakeDeposit(new System.Currency(5000))
System.Console.WriteLine(Acct.Balance)
End Sub
End module
To compile the client, you would have to reference the
BankServerAsm.DLL assembly as follows:
vbc /t:exe bankclient.vb /r:BankServerAsm.dll
The COM DLL and the .NET assembly do not have to be in the same
directory. If at some point you need to debug your Visual Basic code, you
can easily do this by running the VB 6 COM code in the VB 6 IDE. You
can put breakpoints in the VB 6 code and run the .NET code. The code
should stop at your breakpoints. The only requirement to make this work
is that you must set the project to binary compatibility with the DLL you
used for creating the assembly.

Using COM+ Services


I hesitate to write this section because many things are going to change
down the road with respect to using .NET assemblies in COM+
applications. However, for the sake of completion, let's talk about how to
add a .NET class to a COM+ application as of Beta 1.
In the future, the MTS (now COM+) team will migrate all the COM+
services to work seamlessly with all the .NET architecture. For now, we
must use a few COM interop tricks. In essence, the interaction occurs
through the same mechanism explained earlier, by which you must make
your .NET class look like a COM class by adding information to the
registry and using MSCorEE.dll as the COM server wrapper for your
assembly. However, we must also add information to the COM+ catalog.
Microsoft provides another tool called RegSvcs.exe that does the job of
RegAsm with the /tlb command-line switch, plus adds information about
the class to the catalog. In addition, RegSvcs.exe is able to interpret certain
attributes from the assembly, which enables you to set the declarative
attributes in COM+.
For Beta 1, if you want a .NET class to work with COM+ services, you
must derive the class from System.ServicedComponent. Because RegSvcs
is going to generate COM information for the registry, you should also
manage the IIDs and CLSIDs with attributes. For example, the following
VB.NET code shows an interface named IAccount implemented in a
Checking class:

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

<Assembly: System.Runtime.InteropServices.GuidAttribute("1D1D3D4C-52BE-46de-
9100-8F5AEB8207C0")>
<Assembly:
System.Runtime.CompilerServices.AssemblyKeyFileAttribute("complusservices.key")>
<Assembly: Microsoft.ComServices.Description("Book - Dotnet")>
<Assembly: Microsoft.ComServices.ApplicationName("Book - Dotnet")>
<Assembly: Microsoft.ComServices.ApplicationID("7319F24B-6DEA-4479-8027-
1E8E1816C626")>
<Assembly:
Microsoft.ComServices.ApplicationActivation(Microsoft.ComServices.ActivationOption
.Server)>

Imports System.Runtime.InteropServices
Imports Microsoft.ComServices

Interface <guidattribute("874A6DD2-E141-41fd-A379-E066E8E23921")> IAccount


Sub MakeDeposit(ByVal Amount As Integer)
ReadOnly Property Balance( ) As Integer
End Interface

Public Class <guidattribute("FB03ABE6-982D-436e-919C-CA1D8BE1B71A"), _


Transaction(TransactionOption.Required)> _
Checking
Inherits ServicedComponent
Implements IAccount

Private m_Balance As Integer

Private Sub MakeDepositImpl(ByVal Amount As Integer) _


Implements IAccount.MakeDeposit
m_Balance += Amount
End Sub

Private ReadOnly Property BalanceImpl( ) As Integer Implements IAccount.Balance


Get
Return m_Balance
End Get
End Property

End Class
The top portion of the code uses a number of Assembly attributes. The first
is GuidAttribute, which is used at the assembly level to control the LIBID
for the type library that is generated. The second one is AssemblyKeyFile,
which you should also be familiar with. One of the requirements to use
your assembly in COM+ is that it be a public assembly. As you know, that
means that you must assign an originator to the assembly. Thus, this
attribute points to a private key file. The other four attributes have to do
with COM+ application properties. If you remember from Chapter 7, you
can create COM+ applications programmatically using the catalog COM
components. When you create a COM+ application, you can specify
various attributes, such as the Application ID (this is a GUID that
represents the true name of the application). You can also specify the

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Application Name and give the application a description. Also, you can
specify things like the Activation property (Server or Library).
All these properties can be set easily in VB.NET through attributes. The
advantage of doing this is that when you run the RegSvcs tool on your
VB.NET DLL, the tool will automatically create the COM+ application
for you and add your components to it. All the COM+ attributes--not only
the ones at the application level, but also the ones at the class interface and
method levels--have been replicated as attributes. They are contained in
the Microsoft.ComServices namespace.
The sample code also uses the Transaction attribute at the class level to
specify that this class requires transactions.
Something that might look strange is that the Checking class implements
the IAccount interface and makes the implementation methods private. In
addition, it also gives each method a different name. This has nothing to
do with the fact that I am going to use this interface in COM+. If you
make the implementation methods public, that means that there are two
ways of calling the method: through the interface and through a class
reference directly. If you make the implementation methods private, the
only way to reach the methods is through the interface. Notice also that
you do not need to name the implementation methods the same as the
interface methods; you must only match the signatures and use the
Implements directive at the end of the method. It is interesting that both of
these aspects of implementing interfaces in VB.NET (making methods
private and giving them different names) are just like implementing
interfaces in VB 6--except that somehow it seems clearer in VB.NET.
To compile the code, you use the standard vbc command at the command
prompt. Because the code uses a security key file, you have to create one
with sn.exe. Also, because the code uses the Microsoft.ComServices
assembly, you must reference this assembly in the command line. The
following code shows how to compile the code in a command-prompt
window:
vbc /t:library bankserver.vb /version:1.0.0.0 /r:Microsoft.ComServices.dll
The next step is to add the assembly to the GAC. To review, that is done
with the gacutil.exe tool. Once the assembly is in the GAC, you can run
the RegSvcs.exe tool as follows:
RegSvcs /fc bankserver.dll
The /fc switch tells the tool to find an application or create one. The name
of the application is defined by the ApplicationName attribute in the
preceding source code. Optionally, you can enter an application name in
the command line after the assembly name. Figure 11-7 shows the end
result of running the RegSvcs tool.
Figure 11-7. BankServer .NET application in Component Services
administration program

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Using the components is just like using any COM+ component. In fact,
the following code should present no surprises:
Dim acct As IAccount
Set acct = CreateObject("bankserver.Checking")
Call acct.MakeDeposit(500)
MsgBox acct.Balance
One thing that may take you by surprise, however, is that as of Beta 1,
unless you add an attribute to your class to turn on a specific declarative
attribute, the attribute will be turned off by default. One nice thing about
creating COM+ components with .NET is that the resulting component
uses a different threading model than VB 6 components. If you recall, VB
6 components use the apartment-threading model. VB.NET COM
components are marked as using the both-threading model. That means
that VB.NET components run in the MTA by default instead of in the
STA. Also, VB.NET COM components can be pooled. Pooling VB.NET
COM components is beyond the scope of this chapter, but be aware that
living in the MTA means that you must handle synchronization issues in
your methods.

Summary
In this chapter, you have learned the basics of the .NET architecture. You
learned some of the limitations in COM and why Microsoft has created the
new architecture. A number of languages are being written to support the
new architecture. These languages compile to a processor-independent
form of assembly language known as Intermediate Language (IL). Two of
the main languages for .NET are VB.NET and C#. Both of these
compilers generate IL. When you run a program written in IL, a Just-in-
Time compiler converts the code into native machine code.
VB.NET has a number of enhancements over VB 6. Among them are:
code inheritance, method overloading, enhanced user-defined types,
function pointers, parameterized constructors, and true exception handling.
.NET components follow a different versioning scheme than COM+.
When you build an assembly that references another assembly, the client
assembly's manifest contains the version number of the referenced
assembly. The runtime matches the major and minor numbers in the
version for the assembly. You can redirect the runtime to use a different
version with a configuration file.
To use an assembly from a VB 6 program, you use a tool called
RegAsm.exe. Alternatively, you can use the tlbexp.exe tool to create a type
library. However, RegAsm.exe does the job of adding keys to the registry
to make the public classes in the assembly creatable from COM. It also
builds a type library and registers it. In addition to providing you with a
way to use .NET components from COM, you can use this functionality
for versioning your existing COM components. To use COM components
from .NET, you can use the tlbimp.exe tool to create an assembly from
your type library.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

You can also add a .NET component to a COM+ application. In fact, it


makes good sense to mix COM with .NET components, because .NET
components do not have the same threading restrictions as COM+
components. To use an assembly in COM+, you must first sign it and add
it to the cache. Then you must run the tool RegSvcs to register the classes
in the assembly, create a type library, and add it to the catalog. There are
attributes in the Microsoft.ComServices assembly that enable you to
specify the declarative attributes of the COM+ application.

Versioning VB 6 Components with VB .NET:


An Excuse to Use VB .NET Today

Many of us live in a world where we are not allowed to use beta versions for our
production code. In fact, it may be sometime after a final version is released before
management allows us to start migrating code. By this time you have probably heard of
VB .NET and you may have downloaded the Visual Studio .NET beta, perhaps with the
justification that you intend to evaluate it for future releases. You may also be looking for
any excuse to use it now.
It turns out that there is at least one compelling reason for using VB .NET today: to solve
an existing VB 6 problem--versioning. This article shows you the problems with
versioning VB 6 code and how to solve them by creating custom type libraries with VB
.NET. A great thing about this technique is that the type library you will generate will not
be dependent on the .NET SDK. Therefore, you can use it without shipping beta code to
your clients.

Versioning Components in VB 6
The problem with versioning components in VB 6 boils down to one thing: VB 6 does
not give you full control over your interface's GUIDs. The basic scenario is that you
create a component and compile it, then write a client program to use the component.
Sometime later you make modifications to your component and you recompile it.
Suddenly, the client program no longer works. It usually fails with error 429--ActiveX can't
create object. Most developers solve this problem by recompiling the client program. Thus,
teams normally end up rebuilding every component and every client to ensure that they
are all compatible.
The reason client programs stop working is because of numbers called GUIDs (globally
unique identifiers). GUIDs are 128-bit numbers that are assigned (in COM) to different
aspects of your component. An example of a GUID is {FAE3A31F-693C-4ca3-B0EC-
0BD471042D52}. A typical VB ActiveX DLL or ActiveX EXE project has GUIDs
assigned to three different parts of the project: to the type library, to each class, and to
each interface.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Visit vb.oreilly.com for a complete list of O'Reilly books about Visual


Basic.

Information about every public class in the project is grouped into a binary file called a
type library. The type library file is embedded in the image of your COM server (ActiveX
DLL or ActiveX EXE) as a Windows resource. To distinguish your type library from
other type libraries, the VB compiler assigns a GUID, following the COM specifications.
A GUID assigned to the type library is called a LIBID.
Every class in a project also receives a GUID, called a CLSID. Public classes in VB 6
have default interfaces. This is a table of all the public functions in the class. Each
interface receives a GUID as its unique name. According to COM rules, if you modify
the public methods in some way, you must assign a new GUID to the interface. Because
VB 6 follows these same rules, if you modify a method in any way (add, remove, or
change a method), it assigns a new GUID to the interface. However, what happens if you
keep every method the same? It turns out that if you are not careful VB 6 will also change
the GUIDs each time you compile, even if you have not changed a method.
Why is it a problem if VB changes a GUID for the interface? It's a problem if you create
a client program to use the component. The client program builds a dependency on the
component's interface GUID and on the class's GUID. If VB changes various GUIDs in
the component, then the client program stops working.
If you have not made any changes to the component's interface, you must tell VB not to
change the GUIDs of the default interface when you compile. You do this by setting the
version-compatibility property of your VB project to binary compatibility. When you turn
on this setting, each time you compile your server code, the clients continue to work fine.
At some point, however, you will need to change, delete, or add a method. VB 6 has a
mechanism for letting you add methods without breaking compatibility. That means your
client code is safe as long as you only add methods. It is a different story if you remove
or change a method. When you do so, COM says that you must change the GUIDs of
your interface, and VB 6 obeys this rule by forcing you to switch to project compatibility.
The only problem is that VB does something unusual--it changes the GUIDs of every
interface in the project, even the ones you haven't changed. So, if you have four
components in one ActiveX DLL or ActiveX EXE and four client programs using these
components, you have to recompile every single client, even if the component they are
using is not the one you changed. This may be OK once in a while, but what about when
you are developing and testing? What if you are deploying the application in a company
where it would be difficult to change every single client?
Languages like C++ enable you to take full control over the interface GUIDs. Although
that means you have to know when to change them, it also means you do not have to
change all the GUIDs each time you make a change to a single interface. In C++, project
interfaces are defined in IDL (Interface Definition Language), a language similar to C++,
for defining interfaces. In the past, the only way to take full control of your GUIDs in VB
was to learn IDL and use it to define your interfaces, then compile the IDL into a type
library and use it in your VB project.
Enter VB .NET

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

VB .NET does not build COM components. It builds a new type of component that can
run in .NET's common language runtime (CLR). Think of CLR as a virtual machine. In
actuality, it may work very differently from the Java Virtual Machine, but in concept it is
a lot like Java. The approach in .NET is that VB .NET compiles your code to a high-level
assembly language Microsoft developed called Intermediate Language (IL). The compiler
turns your code to IL and wraps it inside a DLL or an EXE. Then, when you execute the
EXE or load the DLL, the just-in-time compiler (JIT compiler) transforms your code to
x86 machine code that the processor can run.

Internally, your COM classes look little like .NET classes. However, Microsoft provides
a tool that enables you to use .NET classes through COM. The way this tool works is that
it produces type libraries that can be used from VB 6. If all you have in a VB .NET
project is the definition of the interfaces, then the type library produced will not have any
dependency on the compiled .NET code. That means you can use the type library and
compile it into your code without your clients needing to install the .NET SDK. What's
more, the type library produced is just like any other COM type library produced today,
so it is very safe to use. To illustrate this technique, let's build a VB 6 project without
using the technique, then build a VB .NET project, and then change the original VB 6
project so that it uses this technique.
Original VB 6 Project
Let's suppose that you have a banking application. In this application you have two
classes: a checking class and a savings class. A good design for such an application
would be to separate the methods these two classes have in common into a single
interface called IAccount. The following code shows the definition of the IAccount
interface:
'VB 6 IAccount interface
Public Sub MakeDeposit(ByVal Amount As Currency)
End Sub
Public Property Get Balance() As Currency
End Property
In VB 6, interfaces are declared inside class modules. In this case, the class module
would be called IAccount. Interface methods do not have code, just the definition of the
methods. Also, the class's instancing property is normally set to 2--PublicNotCreatable, to let
client programs know that the class is not a creatable entity. Interfaces serve as a way to
communicate with the functionality of a class. After defining the interface, you would
implement it in a concrete class such as Checking and Savings. The following code
shows what the Checking class may look like:
'VB 6 Checking class
Implements IAccount
Private m_Balance As Currency

Public Sub IAccount_MakeDeposit(ByVal Amount As Currency)


m_Balance = m_Balance + Amount
End Sub

Public Property Get IAccount_Balance() As Currency


IAccount_Balance = m_Balance

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

End Property
The Checking class uses the Implements command to adopt the IAccount interface. To use
the interface, a client program would declare a variable as IAccount and use New to create
an instance of Checking as follows:
Dim Acct As IAccount
Set Acct = New Checking
Call Acct.MakeDeposit(5000)
Msgbox Acct.Balance
Notice that the client program uses the Checking class through the IAccount interface.
The client code above has a dependency on the IAccount interface and on the Checking
class. If your server project has a number of interfaces, then each client program would
have dependencies on one or more of those interfaces, and on one or more of the concrete
classes that implement them. If you were to change one of the methods in any of the
interfaces, then VB 6 would change the GUIDs to all the interfaces and every client
program would stop working. Let's see how VB .NET can help manage those GUIDs
more efficiently.
VB .NET Project
As a replacement for IDL, you can use VB .NET to get more control over your interfaces.
Plus, because VB .NET is similar to VB 6, you do not have to learn a different syntax.
Let's begin a VB .NET project; you should see the dialog box in Figure 1.

Figure 1. New project dialog box


Choose the class library project. You can name the new project BankInterfaces. When
you create a new library project, VB will create a class file named class1.vb. This file
contains some default code for the definition of a class: Class1. You can delete all the
default code and define your IAccount interface as follows:
'VB .NET Interface
Public Interface IAccount
Sub MakeDeposit(ByVal Amount As Decimal)
ReadOnly Property Balance() As Decimal
End Interface
Rename the class1.vb file to Account.vb. The next step is to assign GUIDs to the interface
and to the project. This is done using the GuidAttribute. An attribute is a class that can be
used to add information to an assembly, a module, an interface, a class, a field, a method,
or a parameter in a method. The following code shows the interface code with attributes:
Imports System.Runtime.InteropServices

<Guid("1393732E-8D27-431a-A180-8EDA0E4499E2")> _
Public Interface IAccount
Sub MakeDeposit(<MarshalAs(UnmanagedType.Currency)>
ByVal Amount As Decimal)
ReadOnly Property Balance() As
<MarshalAs(UnmanagedType.Currency)> Decimal
End Interface
As you can see, the code above declares the IAccount interface as it did before but uses
two attributes throughout the definition. The first attribute is the Guid attribute before the
declaration. The namespace-qualified name (or the full name) of this attribute is
System.Runtime.InteropServices.GuidAttribute. What enables you to omit the namespace name
of the attribute is the statement Imports System.Runtime.InteropServices at the beginning of the

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

file; what enables you to use the name Guid instead of GuidAttribute is the fact that when
you use an attribute class name you can omit the "Attribute" part of the name. Thus, the
class name GuidAttribute becomes Guid when used as an attribute.
The Guid attribute, at the interface level, will produce an interface ID (IID) when the type
library is generated. The GUID for the interface was not constructed by hand. To come
up with the numbers you must use a tool called guidgen.exe. This tool is automatically
installed when you install Visual Studio 6. Figure 2 shows the guidgen.exe program
interface.

Figure 2. guidgen.exe
There is a second instance of the Guid attribute in the code. However, it was the wizard
that added the second instance automatically. It is in the file AssemblyInfo.vb. If you
examine the code in that file you will find a line like the following:
<Assembly: Guid("8680B180-6BF8-4CCE-A4FC-E1A30ADA35FF")>
A full discussion of the term assembly is beyond the scope of this article, but for now
think of the assembly as the project. Putting the Guid attribute at the assembly level
enables you to assign the LIBID for the type library you will generate.
The second attribute that you see in the definition of the interface is the MarshalAs
attribute. The MarshalAs attribute is necessary because the datatype Decimal has a number
of possible interpretations in the COM world. (By default Decimal gets converted to a
wide character string.) The MarshalAs attribute enables you to specify the correct type
conversion. If you are wondering how in the world one figures out when to use MarshalAs
and when not to, it is not as hard as it first seems. Instead of declaring the interface in VB
.NET and exporting it for VB 6, declare the interface in VB 6 and import it into VB
.NET. There will be more information about this later in the article.
Once you have the source code in place you can build the project choosing Build from
the Project menu. The resulting DLL will be called BankInterface.DLL and you can find it in
the Project\Bin directory.
Creating the Type Library
When you build a VB .NET library project, you create what is known in the run-time as
an assembly. Assemblies are not COM servers, and that means that they do not have
embedded type libraries. However, the Microsoft .NET SDK ships with a tool called
tlbexp.exe. This tool can read an assembly and create a type library from the definitions
in the assembly. The best way to do this is to locate the DLL in a command prompt and
run the tlbexp.exe program using BankInterfaces.DLL as the command-line parameter. From
the command line enter the following:
tlbexp BankInterfaces.dll
Figure 3 shows the output generated from running tlbexp.

Figure 3. Running tlbexp to generate a type library


If you look at the files in the directory you will see that there is a new file called
BankInterfaces.tlb. To use this file in your VB 6 project you must first register the type
library with a command available in Visual Studio 6 called regtlib.exe. From the
command line enter the following:
regtlib BankInterfaces.tlb

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Once you register the type library file, you can incorporate it in your VB 6 project using
the Project Reference dialog. In VB 6 choose References from the Project menu, then
select BankInterfaces from the list as shown in Figure 4.
Figure 4. Project References dialog
The tlb file contains a declaration of the IAccount interface. With the BankInterfaces
reference in place, you can get rid of the IAccount class in the VB project. The Checking
and Savings classes can be used as before. You can then build the VB 6 COM server and
modify the client project slightly.
Reversing the Process
Earlier I mentioned that certain .NET datatypes have different interpretations in the COM
world, and that to specify the conversion type you must use the MarshalAs attribute. I also
mentioned that an easy way to know what datatypes required this attribute was to reverse
the process. Reversing the process means taking an existing type library and generating
an assembly that can be used in .NET. With this mechanism you can examine how VB
types translate to .NET and which types require special handling.

Reversing the process is done with a tool called tlbimp.exe, also included in the .NET
SDK. Tlbimp takes a type library file, or a DLL that contains a type library, and generates
an assembly that can be used from .NET. Using that approach, I've created a VB type
library that contains nearly every type that a developer may use in defining an interface,
and created a .NET assembly from it. The results are described below:
VB 6 Type .NET Type
Integer Short
Single Single
Byte Byte
Variant MarshalAs(UnmanagedType.Struct) Object
Long Integer
Double Double
Currency MarshalAs(UnmanagedType.Currency) Decimal
String MarshalAs(UnmanagedType.BStr) String
Boolean Boolean
Date Date
Object MarshalAs(UnmanagedType.IDispatch) Object
Array* <MarshalAs(UnmanagedType.SafeArray,
SafeArraySubType:=UnmanagedType.*enter type here*)>
*An example of an Array declaration is the following: Sub MyMethod(ByRef
strs() As String). The strs parameter in the MyMethod declaration is of type
SafeArray(Bstr). A SafeArray(Bstr) is an array of strings. To specify a SafeArray
parameter you use the MarshalAs attribute passing SafeArray as the type, then
adding SafeArraySubType:= the type of theSafeArray. In the case of the
MyMethod declaration, the second parameter to the attribute would be
SafeArraySubType:=UnmanagedType.Bstr.

New Client Version

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

The VB 6 client code can remain mostly as is, however, you must reference the new type
library as well, like you did in the server project. Also, to completely get rid of any
dependencies the client project has on the VB 6 generated GUIDs, you must change the
New command in the code to use CreateObject instead, as seen below. (Changing from New
to CreateObject eliminates the dependency on the CLSIDs.)
Dim Acct As IAccount
Set Acct = CreateObject("BankServer.Checking")
Call Acct.MakeDeposit(5000)
Msgbox Acct.Balance
With the VB .NET-generated type library and the CreateObject command you now have
full control over GUIDs. That means you no longer have to worry about how you set
your version compatibility project--no more worrying about Binary Compatibility,
Project Compatibility, or No Compatibility on the server side. It does mean, however,
that you must follow one important versioning guideline.
Versioning GUIDs
With full control comes more responsibility. While you are developing, it is not
necessary to follow COM rules to the letter. You may change, add, or delete a method
and keep the same GUID. Once you release the server and client to the outside world, it
is a different story--you must follow the COM versioning golden rule:
Any time you need to change a method in an interface you should create a new interface
and assign to the new interface a new GUID.
For example, let's suppose the requirements change and your company now needs an
extra parameter in the MakeDeposit method, like AmountAvailable. Instead of modifying the
existing method (which would break existing clients) you should add a new interface to
the VB .NET project called IAccount2 and then implement both IAccount and IAccount2
in the Checking class. The following code shows the definition of the IAccount2 interface
in the VB .NET project:
Imports System.Runtime.InteropServices

<Guid("1393732E-8D27-431a-A180-8EDA0E4499E2")> _
Public Interface IAccount
Sub MakeDeposit(<MarshalAs(UnmanagedType.Currency)>
ByVal Amount As Decimal)
ReadOnly Property Balance() As
<MarshalAs(UnmanagedType.Currency)> Decimal
End Interface

<Guid("1393732E-8D27-431a-A180-8EDA0E4499E2")> _
Public Interface IAccount2
Sub MakeDeposit(<MarshalAs(UnmanagedType.Currency)>
ByVal Amount As Decimal,
<MarshalAs(UnmanagedType.Currency)>
ByVal AmountAvailable As Decimal)
ReadOnly Property Balance() As
<MarshalAs(UnmanagedType.Currency)> Decimal
End Interface
Notice that the code uses a different GUID for the IAccount2 interface. Of course you
would have to save the project, rebuild it and re-export the type library with tlbexp.exe.
Then you would modify the server code as follows:
'VB 6 Checking class

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Implements IAccount
Implements IAccount2

Private m_Balance As Currency


Private m_Available As Currency

Public Sub IAccount_MakeDeposit(ByVal Amount As Currency)


m_Balance = m_Balance + Amount
End Sub

Public Property Get IAccount_Balance() As Currency


Balance = m_Balance
End Property

Public Sub IAccount2_MakeDeposit(ByVal Amount As Currency, ByVal


AmountAvailable As Currency)
m_Balance = m_Balance + Amount
m_Available = m_Available + AmountAvailable
End Sub

Public Property Get IAccount2_Balance() As Currency


IAccount2_Balance = IAccount_Balance
End Property

Notice that the code above implements both the IAccount interface and the IAccount2
interface. As a result you must implement the methods of each interface. Even though it
looks like you are duplicating code, this approach will guarantee that your existing client
code will continue to run smoothly. What's more, this approach will give you a really
good excuse to start using VB .NET to solve a very serious VB 6 problem.

VB .NET Language in a Nutshell

Appendix A
What's New and Different in VB .NET
This appendix is for readers who are familiar with earlier versions of
Visual Basic, specifically Version 6. In this appendix, we describe the
basic changes to the VB language, both in syntax and in functionality.
(Readers familiar only with Version 5 of Visual Basic will also benefit
from this chapter, although we discuss only the changes since Version 6.)
We also touch upon other changes to VB, such as error handling and
additional object-oriented programming support.
Language Changes for VB .NET

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

In this section, we outline the changes made to the Visual Basic language
from Version 6 to Visual Basic .NET. These language changes were made
to bring VB under the umbrella of the .NET Framework and allow a
Common Language Runtime for all languages in Visual Studio .NET. In
some sense, the changes made to the VB language were to bring the
language component of VB (as opposed to the IDE component) more in
line with the C# language (which is a derivative of C and C++).
Since we assume in this chapter that you are familiar with VB 6, we will
not necessarily discuss how VB 6 handles a given language feature, unless
the contrast is specifically helpful. You can assume that if a VB .NET
language feature is described in this chapter, there has been a change in its
behavior since VB 6.
Data Types
There have been fundamental changes to data types in VB .NET, which
we outline in this section. The most important change is that all of the
languages under the .NET umbrella (VB, C#, and Managed C++) now
implement a subset of a common set of data types, defined in the .NET
Framework's Base Class Library (BCL). We say subset because VB .NET
does not implement all of these data types. In any case, each data type in
the BCL is implemented either as a class or as a structure (which is similar
to a class) and, as such, has members. The VB .NET data types are
wrappers for the corresponding BCL data type. While this need not
concern the VB programmer, it can be used in some cases to expose a bit
more functionality from a data type. For more on data types, see Chapter
2.
Now let us consider the specifics.
Strings
As you may know, in VB 6, strings were implemented as a data type
known as the BSTR. A BSTR is a pointer to a character array that is
preceded by a 4-byte Long specifying the length of the array. In VB .NET,
strings are implemented as objects of the String class, which is part of the
.NET Framework's System namespace.
One consequence of this reimplementation of strings is that VB .NET does
not have fixed-length strings, as does VB 6. Thus, the following code is
illegal:
Dim Name As String * 30
Note, though, that strings in VB .NET are immutable. That is, although
you do not have to declare a string's length in advance, once a value is
assigned to a string, its length cannot change. If you change that string, the
.NET Common Language Runtime actually gives you a reference to a new
String object. (For more on this, see Chapter 2.)
Integer/Long data type changes
VB .NET defines the following signed-integer data types:
Short

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

The 16-bit integer data type. It is the same as the Int16 data type in the
Base Class Library.
Integer
The 32-bit integer data type. It is the same as the Int32 data type in the
Base Class Library.
Long
The 64-bit integer data type. It is the same as the Int64 data type in the
Base Class Library.
Thus, with respect to the changes from VB 6 to VB .NET, we can say:
• The VB 6 Integer data type has become the VB .NET Short data
type.
• The VB 6 Long data type has become the VB .NET Integer data
type.

Variant data type


VB .NET does not support the Variant data type. The Object data type is
VB .NET's universal data type, meaning that it can hold data of any other
data type. According to the documentation, all of the functionality of the
Variant data type is supplied by the Object data type.
We cannot resist the temptation to add that there are several penalties
associated with using a universal data type, including poor performance
and poor program readability. Thus, while VB .NET still provides this
opportunity through the Object data type, its use is not recommended
whenever it can be avoided.
The VarType function, which was used in VB 6 to determine the type of
data stored in a variant variable (that is, the variant's data subtype), now
reports the data subtype of the Object type instead. In addition, the
TypeName function, which can be used to return a string that indicates the
data type of a variable of type Object, is still supported.
Other data type changes
Here are some additional changes in data types:
• The Deftype statements (DefBool, DefByte, etc.), which were used to
define the default data type for variables whose names began with
particular letters of the alphabet, are not supported in VB .NET.
• The Currency data type is not supported in VB .NET. However, in
VB .NET, the Decimal data type can handle more digits on both
sides of the decimal point, and so it's a superior replacement. In
VB .NET, Decimal is a strong data type; in VB 6, it was a Variant
subtype, and a variable could be cast as a Decimal only by calling
the CDec conversion function.

• In VB 6, a date is stored in a Double format using four bytes. In


VB .NET, the Date data type is an 8-byte integer data type whose
range of values is from January 1, 1 to December 31, 9999.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Variables and Their Declaration


The changes in variable declarations and related issues are described here.
Variable declaration
The syntax used to declare variables has changed for VB .NET, making it
more flexible. Indeed, these are long awaited changes.
In VB .NET, when multiple variables are declared on the same line, if a
variable is not declared with a type explicitly, then its type is that of the
next variable with an explicit type declaration. Thus, in the line:
Dim x As Long, i, j, k As Integer, s As String
the variables i, j, and k have type Integer. (In VB 6, the variables i and j
would have type Variant, and only the variable k would have type Integer.)
When declaring external procedures using the Declare statement, VB .NET
does not support the As Any type declaration. All parameters must have a
specific type declaration.
Variable initialization
VB .NET permits the initialization of variables in the same line as their
declaration (at long last). Thus, we may write:
Dim x As Integer = 5
to declare an Integer variable and initialize its value to 5. Similarly, we
can declare and initialize more than one variable on a single line:
Dim x As Integer = 6, y As Integer = 9
Variable scope changes
In VB 6, a variable that is declared anywhere in a procedure has procedure
scope; that is, the variable is visible to all code in the procedure.
In VB .NET, if a variable is defined inside a code block (a set of
statements that is terminated by an End..., Loop, or Next statement), then the
variable has block-level scope; that is, it is visible only within that block.
For example, consider the following VB .NET code:
Sub Test( )
If x <> 0 Then
Dim rec As Integer
rec = 1/x
End If

MsgBox CStr(rec)
End Sub
In this code, the variable rec is not recognized outside the block in which
it is defined, so the final statement will produce an error.
It is important to note that the lifetime of a local variable is always that of
the entire procedure, even if the variable's scope is block-level. This
implies that if a block is entered more than once, a block-level variable
will retain its value from the previous time the code block was executed.
Arrays and array declarations
VB 6 permitted you to define the lower bound of a specific array, as well
as the default lower bound of arrays whose lower bound was not explicitly

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

specified. In VB .NET, the lower bound of every array dimension is 0 and


cannot be changed. The following examples show how to declare a one-
dimensional array, with or without an explicit size and with or without
initialization:
' Implicit constructor: No initial size and no initialization
Dim Days( ) As Integer

' Explicit constructor: No initial size and no initialization


Dim Days( ) As Integer = New Integer( ) {}

' Implicit constructor: Initial size but no initialization


Dim Days(6) As Integer

' Explicit constructor: Initial size but no initialization


Dim Days( ) As Integer = New Integer(6) {}

' Implicit constructor: Initial size implied by initialization


Dim Days( ) As Integer = {1, 2, 3, 4, 5, 6, 7}

' Explicit constructor, Initial size and initialization


Dim Days( ) As Integer = New Integer(6) {1, 2, 3, 4, 5, 6, 7}
Note that in the declaration:
Dim ArrayName(X) As ArrayType
the number X is the upper bound of the array. Thus, the array has size
X+1.
Multidimensional arrays are declared similarly. For instance, the following
example declares and initializes a two-dimensional array:
Dim X(,) As Integer = {{1, 2, 3}, {4, 5, 6}}
and the following code displays the contents of the array:
Debug.Write(X(0, 0))
Debug.Write(X(0, 1))
Debug.Writeline(X(0, 2))
Debug.Write(X(1, 0))
Debug.Write(X(1, 1))
Debug.Write(X(1, 2))

123
456
In VB .NET, all arrays are dynamic; there is no such thing as a fixed-size
array. The declared size should be thought of simply as the initial size of
the array, which is subject to change using the ReDim statement. Note,
however, that the number of dimensions of an array cannot be changed.
Moreover, unlike VB 6, the ReDim statement cannot be used for array
declaration, but only for array resizing. All arrays must be declared
initially using a Dim (or equivalent) statement.
Structure/user-defined type declarations
In VB 6, a structure or user-defined type is declared using the Type...End
Type structure.
In VB .NET, the Type statement is not supported. Structures are declared
using the Structure...End Structure construct. Also, each member of the

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

structure must be assigned an access modifier, which can be Public,


Protected, Friend, Protected Friend, or Private. (The Dim keyword is equivalent
to Public in this context.)
For instance, the VB 6 user-defined type:
Type RECT
Left As Long
Top As Long
Right As Long
Bottom As Long
End Type
is defined in VB .NET as:
Structure RECT
Public Left As Long
Public Top As Long
Public Right As Long
Public Bottom As Long
End Structure
Actually, the VB .NET Structure type is far more reaching than its VB 6
user-defined type predecessor. Indeed, structures have many properties in
common with classes; for instance, structures can have members
(properties and methods). We discuss structures in detail in Chapter 2.
Boolean and Bitwise Operators
Eqv and Imp, two infrequently used Boolean and bitwise operators that are
present in VB6, have been removed from VB .NET.
In VB6, Eqv is the logical equivalence operator. As a Boolean operator, it
returns True if both expressions are either True or False, but it returns False if
one is True while the other is False. As a bitwise operator, it returns 1 if
both bits are the same (that is, if both are 1 or both are 0), but it returns 0 if
they are different. In VB .NET, Eqv can be replaced with the equals
comparison operator for logical operations. However, for bitwise
operations, you'll have to resort to a bit-by-bit comparison, as the
following code fragment shows:
Public Function BitwiseEqv(x1 As Byte, X2 As Byte) _
As Long

Dim b1, b2, bRet As Byte


Dim iCtr as Integer

For iCtr = 0 to len(x1) * 8 - 1


b1 = x1 and 2^iCtr
b2 = x2 and 2^iCtr
if b1 = b2 then bRet += 2^iCtr
next

BitwiseEqv = bRet

End Function
In VB6, Imp is the logical implication operator. As a Boolean operator, it
returns True unless its first expression is True while the second is False. As a
bitwise operator, it returns 1 unless the bit in the first expression is 1 while

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

the bit in the second expression is 0. In VB .NET, Imp can be replaced


with a combination of the Not and Or operators for logical operations. For
example, the code fragment:
bResult = (Not bFlag1) Or bFlag2
is equivalent to the VB6 statement:
bResult = bFlag1 Imp bFlag2
For bitwise operations, a bit-by-bit comparison is again necessary, as the
following code fragment shows:
Public Function BitwiseImp(x1 As Byte, X2 As Byte) As Long

Dim b1, b2, bRet As Byte


Dim iCtr as Integer

For iCtr = 0 to len(x1)*8 - 1


b1 = Not(x1) and 2^iCtr
b2 = x2 and 2^iCtr
if b1 Or b2 then
bRet += 2^iCtr
end If
next

BitwiseImp = bRet

End Function

Changes Related to Procedures


VB .NET features a number of changes to the way in which procedures
are defined and called, most of which tend to make the language more
streamlined and consistent.
Calling a procedure
In VB 6, parentheses are required around arguments when making
function calls. When calling a subroutine, parentheses are required when
using the Call statement and proscribed when not using the Call statement.
In VB .NET, parentheses are always required around a nonempty
argument list in any procedure call--function or subroutine. (In subroutine
calls, the Call statement is optional.) When calling a parameterless
procedure, empty parentheses are optional.
Default Method of Passing Arguments
In VB 6, if the parameters to a function or subroutine were not explicitly
prefaced with the ByVal or ByRef keywords, arguments were passed to
that routine by reference, and modifications made to the argument in the
function or subroutine were reflected in the variable's value once control
returned to the calling routine. In VB .NET, on the other hand, if the
ByRef or ByVal keyword is not used in a parameter, the argument is
passed to the routine by value, and modifications made to the argument in
the function or subroutine are discarded once control returns to the calling
program.
Optional arguments

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

In VB 6, a procedure parameter can be declared as Optional without


specifying a default value. For optional Variant parameters, the IsMissing
function can be used to determine whether the parameter is present.
In VB .NET, an optional parameter must declare a default value, which is
passed to the procedure if the calling program does not supply an
argument for that parameter. The IsMissing function is not supported. The
following example shows an optional parameter declaration:
Sub Calculate(Optional ByVal Switch As Boolean = False)
Return statement
In VB .NET, the Return statement is used to return control to the calling
program from a function or subroutine. The GoSub statement is not
supported. Note that the Return statement is used to return a value from a
function.
The following function illustrates the Return statement:
Public Function Test( ) As Integer
If MsgBox("Return", MsgBoxStyle.YesNo) = MsgBoxResult.Yes Then
Return 0
Else
MsgBox("Continue")
Return 1
End If
End Function
Passing property parameters in procedures
Consider passing a property to a procedure by reference, as in:
Sub ShrinkByHalf(ByRef lSize As Long)
lSize = CLng(lSize/2)
End Sub

Call ShrinkByHalf(Text1.Height)
In VB 6, when passing the value of a property by reference, the property is
not updated. In other words, passing a property by reference is equivalent
to passing it by value. Hence, in the previous example, the property
Text1.Height will not be changed.
In VB .NET, passing a property by reference does update the property, so
in this case, the Text1.Height property will be changed. Note, however,
that the value of the property is not changed immediately, but rather when
the called procedure returns.
ParamArray parameters
In VB 6, if the ParamArray keyword is used on the last parameter of a
procedure declaration, the parameter can accept an array of Variant
parameters. In addition, ParamAarray parameters are always passed by
reference.
In VB .NET, ParamArray parameters are always passed by value, and the
parameters in the array may be of any data type.
Miscellaneous Language Changes

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

VB .NET includes several miscellaneous changes that include the format


of line numbers, the lack of support for the GoTo and GoSub statements,
and the replacement of the Wend keyword by End While.
Line numbers
Visual Basic .NET requires that every line number be followed
immediately by a colon (:). A statement can optionally follow the colon. In
VB 6, line labels, which were used in particular for error handling by the
On Error GoTo statement, had to be followed immediately by a colon, but
line numbers did not.
On GoTo
The On...GoSub and On...GoTo constructions are not supported. However,
VB .NET still supports the On Error GoTo statement.
While
The While...Wend construction loops through code while a specified
condition is True. VB .NET retains that construction, but replaces the Wend
keyword with the End While statement. The Wend keyword is not supported.
GoSub and Return statements
In VB .NET, the GoSub statement is not supported.
As remarked earlier, in VB .NET, the Return statement is used to return
control to the calling program from a function or subroutine. The VB 6
Exit Sub and Exit Function statements continue to be supported in VB .NET;
however, the advantage of the Return statement is that it allows you to
specify the function's return value as an argument to the Return statement.
Changes to Programming Elements
VB .NET has removed support for several programming elements because
the underlying .NET Framework class library and the Common Language
Runtime (CLR) contain equivalent functionality. Here are the victims and
their replacements. (We discuss the class library and CLR in Chapters and
.)
Constants
The Microsoft.VisualBasic.Constants class in the Base Class Library
defines a number of constants, such as the familiar vbCrLf constant, so
these can be used as always. However, some constants, such as the color
constants vbRed and vbBlue, are no longer directly supported. Indeed, the
color constants are part of the System.Drawing namespace's Color
structure, so they are accessed as follows:
Me.BackColor = System.Drawing.Color.BlanchedAlmond
In most cases, to access a particular constant that is not a field in the
Microsoft.VisualBasic.Constants class, you must designate the
enumeration (or structure) to which it belongs, along with the constant
name. For example, the vbYes constant in VB 6 continues to exist as an
intrinsic constant in VB .NET. However, it has a counterpart in the
MsgBoxResult enumeration, which can be accessed as follows:

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

If MsgBoxResult.Yes = MsgBox("OK to proceed?", ...


For a list of all built-in constants and enums, see Appendix D.
String Functions
The LSet and RSet functions have been replaced by the PadLeft and
PadRight methods of the System.String class. For instance, the following
code pads the name "Donna" with spaces on the left to make the total
string length equal to 15:
Dim sName As String = "Donna"
Msgbox(sName.PadLeft(15))
The String function has been removed from the language. In its place, we
simply declare a string and initialize it, using syntax such as:
Dim str As New String("A"c, 5)
which will define a string containing five As. Note the use of the modifier
c in "A"c to define a character (data type Char), as opposed to a String of
length 1. This is discussed in more detail in Chapter 2.
Emptiness
In VB 6, the Empty keyword indicates an uninitialized variable, and the
Null keyword is used to indicate that a variable contains no valid data. VB
.NET does not support either keyword, but uses the Nothing keyword in
both of these cases.
According to the documentation: "Null is still a reserved word in Visual
Basic .NET 7.0, even though it has no syntactical use. This helps avoid
confusion with its former meanings." Whatever.
In addition, the IsEmpty function is not supported in VB .NET.
Graphical Functionality
The System.Drawing namespace contains classes that implement graphical
methods. For instance, the Graphics class contains methods such as
DrawEllipse and DrawLine. As a result, the VB 6 Circle and Line methods
have been dropped.
Note that the VB 6 PSet and Scale methods are no longer supported and
that there are no direct equivalents in the System.Drawing namespace.
Mathematical Functionality
Mathematical functions are implemented as members of the Math class of
the System namespace. Thus, the VB 6 math functions, such as the
trigonometric functions, have been dropped. Instead, we can use
statements such as:
Math.Cos(1)
Note also that the Round function has been replaced by Round method of
the System.Math class.
Diagnostics
The System.Diagonstics namespace provides classes related to
programming diagnostics. Most notably, the VB 6 Debug object is gone,
but its functionality is implemented in the System.Diagnostics.Debug

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

class, which has methods such as Write, WriteLine (replacing Print),


WriteIf, and WriteLineIf. (You won't believe it, but there is still no
method to clear the Output window!)
Miscellaneous
Here are a few additional changes to consider:
• The VB 6 DoEvents function has been replaced by the DoEvents
method of the Application class of the System.Windows.Forms
namespace.

• The VB 6 IsNull and IsObject functions have been replaced by the


IsDBNull and IsReference methods of the Information class of the
Microsoft.VisualBasic namespace. Since this namespace is
implicitly loaded by VB as part of the project template when a
project is created in Visual Studio, no Imports statement is required,
and the members of its classes can be accessed without
qualification.
• Several VB 6 functions have two versions: a String version and a
Variant version. An example is provided by the Trim$ and Trim
functions. In VB .NET, these functions are replaced by a single
overloaded function. Thus, for instance, we can call Trim using
either a String or Object argument.
Obsolete Programming Elements
The following list shows some of the programming elements that have
been removed from Visual Basic .NET:
As Any
Required all parameters to have a declared data type.
Atn function
Replaced by System.Math.Atan.
Calendar property
Handled by classes in the System.Globalization namespace.
Circle statement
Use methods in the System.Drawing namespace.
Currency data type
Replaced by the Decimal data type.
Date function
Replaced by the Today property of the DateTime structure in the System
namespace.
Date statement
Replaced by the Today statement.
Debug.Assert method
Replaced by the Assert method of the Debug class of the
System.Diagonistics namespace.
Debug.Print method
Replaced by the Write and WriteLine methods of the Debug class of the
System.Diagonistics namespace.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Deftype statements
Not supported.
DoEvents function
Replaced by the DoEvents method of the Application class in
System.Windows.Forms namespace.
Empty keyword
Replaced by the Nothing keyword.
Eqv operator
Use the equal sign.
GoSub statement
Not supported.
Imp operator
A Imp B is logically equivalent to (Not A) Or B.
Initialize event
Replaced by the constructor method.
Instancing property
Use the constructor to specify instancing.
IsEmpty function
Not supported because the Empty keyword is not supported.
IsMissing function
Not supported because every optional parameter must declare a default
value.
IsNull function
Not supported. The Null keyword is replaced by Nothing.
IsObject function
Replaced by the IsReference function.
Let statement
Not supported.
Line statement
Use the DrawLine method of the Graphics class in the System.Drawing
namespace.
LSet statement
Use the PadLeft method of the String class in the System namespace.
Null keyword
Use Nothing.
On...GoSub construction
Not supported. No direct replacement.
On...GoTo construction
Not supported. No direct replacement. On Error... is still supported,
however.
Option Base statement
Not supported. All arrays have lower bound equal to 0.
Option Private Module statement
Use access modifiers in each individual Module statement.
Property Get, Property Let, and Property Set statements
Replaced by a new unified syntax for defining properties.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

PSet method
Not supported. No direct replacement. See the System.Drawing
namespace.
Round function
Use the Round method of the Math class of the System namespace.
RSet statement
Use the PadRight method of the String class in the System namespace.
Scale method
Not supported. No direct replacement. See the System.Drawing
namespace.
Set statement
Not supported.
Sgn function
Use Math.Sign.
Sqr function
Use Math.Sqrt.
String function
Use the String class constructor with parameters.
Terminate event
Use the Destroy method.
Time function and statement
Instead of the Time function, use the TimeOfDay method of the DateTime
structure of the System namespace. Instead of the Time statement, use the
TimeOfDay statement.
Type statement
Use the Structure statement.
Variant data type
Use the Object data type.
VarType function
Use the TypeName function or the GetType method of the Object class.
Wend keyword
Replaced by End While.
Structured Exception Handling
VB .NET has added a significant new technique for error handling. Along
with the traditional unstructured error handling through On Error Goto
statements, VB .NET adds structured exception handling, using the
Try...Catch...Finally syntax supported by other languages, such as C++. We
discuss this in detail in Chapter 7.
Changes in Object-Orientation
As you may know, Visual Basic has implemented some features of object-
oriented programming since Version 4. However, in terms of object-
orientation, the step from Version 6 to VB .NET is very significant.
Indeed, some people did not consider VB 6 (or earlier versions) to be a
truly object-oriented programming language. Whatever your thoughts may
have been on this matter, it seems clear that VB .NET is an object-oriented
programming language by any reasonable definition of that term.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Here are the main changes in the direction of object-orientation. We


discuss these issues in detail in Chapter 3.
Inheritance
VB .NET supports object-oriented inheritance (but not multiple
inheritance). This means that a class can derive from another (base) class,
thereby inheriting all of the properties, methods, and events of the base
class. Since forms are also classes, inheritance applies to forms as well.
This allows new forms to be created based on existing forms. We discuss
inheritance in detail in Chapter 3.
Overloading
VB .NET supports a language feature known as function overloading. The
idea is simple and yet quite useful. We can use the same name for
different functions (or subroutines), as long as the functions can be
distinguished by their argument signature. The argument signature of a
function (or subroutine) is the sequence of data types of the arguments of
the function. Thus, in order for two functions to have the same argument
signature, they must have the same number of arguments, and the
corresponding arguments must have the same data type. For example, the
following declarations are legal in the same code module because they
have different argument signatures:
Overloads Sub OpenFile( )
' Ask user for file to open and open it
End Sub

Overloads Sub OpenFile(ByVal sFile As String)


' Open file sFile
End Sub

Object Creation
VB 6 supports a form of object creation called implicit object creation. If
an object variable is declared using the New keyword:
Dim obj As New SomeClass
then the object is created the first time it is used in code. More
specifically, the object variable is initially given the value Nothing, and
then every time the variable is encountered during code execution, VB
checks to see if the variable is Nothing. If so, the object is created at that
time.
VB .NET does not support implicit object creation. If an object variable
contains Nothing when it is encountered, it is left unchanged, and no object
is created.
In VB .NET, we can create an object in the same statement as the object-
variable declaration, as the following code shows:
Dim obj As SomeClass = New SomeClass
As a shorthand, we can also write:
Dim obj As New SomeClass
If the object's class constructor takes parameters, then they can be
included, as in the following example:

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Dim obj As SomeClass = New SomeClass(argument1, argument2,...)


As a shorthand, we can also write:
Dim obj As New SomeClass(argument1, argument2,...)
For details on class constructors, see Chapter 3.
Properties
There have been a few changes in how VB handles properties, particularly
with respect to default properties and property declarations.
Default properties
As you know, you can use default properties in VB 6. For instance, if txt is
a textbox control, then:
txt = "To be or not to be"
assigns the string "To be or not to be" to the default Text property of the
textbox txt.
However, there is a price to pay for default properties: ambiguity. For
example, if txt1 and txt2 are object variables referencing two TextBox
controls, what does:
txt1 = txt2
mean? Are we equating the default properties or the object variables? In
VB 6, this is interpreted as equating the default properties:
txt1.Text = txt2.Text
and we require the Set statement for object assignment:
Set txt1 = txt2
In VB .NET, default properties are not supported unless the property takes
one or more parameters, in which case there is no ambiguity.
As Microsoft points out, default properties occur most commonly with
collection classes. For example, in ActiveX Data Objects (ADO), the
Fields collection of the Recordset object has a default Item property that
returns a particular Field object. Thus, we can write:
rs.Fields.Item(1).Value
or, since the default Item property is parameterized:
rs.Fields(1).Value
Although we may not be used to thinking of this line as using default
properties, it does.
Thus, in VB .NET, the line:
txt1 = txt2
is an object assignment. To equate the Text properties, we must write:
txt2.Text = txt1.Text
Since it is no longer needed, the Set keyword is not supported under VB
.NET, nor is the companion Let keyword.
This settles the issue of equating object variables. For object variable
comparison, however, we must use the Is operator, rather than the equal
sign, as in:
If txt1 Is txt2 Then
or:
If Not (txt1 Is txt2) Then
Property declarations

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

In VB 6, properties are defined using Property Let, Property Set, and Property
Get procedures. However, VB .NET uses a single property-declaration
syntax of the form shown in the following example. Note also that there is
no longer a need to distinguish between Property Let and Property Set because
of the changes in default property support.
Property Salary( ) As Decimal
Get
Salary = mdecSalary
End Get
Set
mdecSalary = Value
End Set
End Property
Note the use of the implicitly defined Value variable that holds the value
being passed into the property procedure when it is being set.
Note also that VB .NET does not support ByRef property parameters. All
property parameters are passed by value.

.NET Framework Essentials


By Thuan Thai & Hoang Lam
June 2001
0-596-00165-7, Order Number: 1657
320 pages, $29.95

Chapter 6
Web Services
Web Services allow access to software components through standard web
protocols such as HTTP and SMTP. Using the Internet and XML, we can
now create software components that communicate with others, regardless
of language, platform, or culture. Until now, software developers have
progressed toward this goal by adopting proprietary componentized
software methodologies, such as DCOM; however, because each vendor
provides its own interface protocol, integration of different vendors'
components is a nightmare. By substituting the Internet for proprietary
transport formats and adopting standard protocols such as SOAP, Web
Services help software developers create building blocks of software,
which can be reused and integrated regardless of their location.
In this chapter, we describe the .NET Web Services architecture and
provide examples of a web service provider and several web service
consumers.

Web Services in Practice


You may have heard the phrase "software as services" and wondered
about its meaning. The term service, in day-to-day usage, refers to what

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

you get from a service provider. For example, you bring your dirty
clothing to the cleaner to use its cleaning service. Software, on the other
hand, is commonly known as an application, either off-the-shelf, shrink-
wrapped, or a custom application developed by a software firm. You
typically buy the software (or in our case, build the software). It usually
resides on some sort of media such as floppy diskette or CD and is sold in
a shrink-wrapped package through retail outlets.
How can software be viewed as services? The example we are about to
describe might seem far-fetched; however, it is possible with current
technology. Imagine the following. As you grow more attached to the
Internet, you might choose to replace your computer at home with
something like an Internet Device, specially designed for use with the
Internet. Let's call it an iDev. With this device, you can be on the Internet
immediately. If you want to do word processing, you can point your iDev
to a Microsoft Word service somewhere in Redmond and type away
without the need to install word processing software. When you are done,
the document can be saved at an iStore server where you can later retrieve
it. Notice that for you to do this, the iStore server must host a software
service to allow you to store documents. Microsoft would charge you a
service fee based on the amount of time your word processor is running
and which features you use (such as the grammar and spell checkers). The
iStore service charges vary based on the size of your document and how
long it is stored. Of course, all these charges won't come in the mail, but
rather through an escrow service where the money can be piped from and
to your bank account or credit card.
This type of service aims to avoid the need to upgrade of your Microsoft
Word application. If you get tired of Microsoft Word, you can choose to
use a different software service from another company. Of course, the
document that you store at the iStore server is already in a standard data
format. Since iStore utilizes the iMaxSecure software service from a
company called iNNSA (Internet Not National Security Agency), the
security of your files is assured. And because you use the document
storage service at iStore, you also benefit from having your document
authenticated and decrypted upon viewing, as well as encrypted at storing
time.
All of these things can be done today with Web Services.
In fact, Microsoft has launched a version of the "software as service"
paradigm with its Passport authentication service. Basically, it is a
centralized authentication service that you can incorporate into your web
sites. For sites using the Passport authentication service, it's no longer
necessary to memorize or track numerous username/password pairs.
Recently, Microsoft also announced Project HailStorm, a set of user-
centric Web Services, including identification and authentication, email,
instant messaging, automated alert, calendar, address book, and storage.
As you can see, most of these are well-known services that are provided
separately today. Identification and authentication is the goal of the

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Passport project. Email might map to Hotmail or any other web-based


email services. Instant messaging and automated alert should be familiar
to you if you use MSN Messenger Service or AOL Instant Messenger. A
calendar and address book are usually bundled together with the web-
based email service. Consolidating these user-centric services and
exposing them as Web Services would allow the user to publish and
manage his own information.
A HailStorm customer can also control access permission to the data to
allow or restrict access to content. These services also allow other users,
organizations, and smart devices to communicate and retrieve information
about us. For example, how many times have you been on the road with
your mobile phone and want to get to your contact list in Outlook? Your
mobile phone should be able to communicate with your address book Web
Service to get someone's phone number, right? Or better yet, if your car
broke down in the middle of nowhere, you should be able to use your
mobile phone to locate the nearest mechanic. The user is in control of
what information is published and to whom the information will be
displayed. You would probably have it set up so that only you can access
your address book, while the yellow pages Web Service that publishes the
nearest mechanic shop to your stranded location would be publicly
accessible to all.
Currently, users store important data and personal information in many
different places. With HailStorm Web Services, information will be
centrally managed. For example, your mechanic might notify you when
it's time for your next major service. Or when you move and change your
address, instead of looking up the list of contacts you wish to send the
update to, HailStorm will help you publish your update in one action.
The potential for consumer-oriented and business-to-business Web
Services like HailStorm is great, although there are serious and well-
founded concerns about security and privacy. In one form or another,
though, Web Services are here to stay, so let's dive in and see what's
underneath.

Web Services Framework


Web Services combine the best of both distributed componentization and
the World Wide Web. It extends distributed computing to broader ranges
of client applications. The best thing is that it does it by seamlessly
marrying and enhancing existing technologies.
Web Services Architecture
Web Services are distributed software components that are accessible
through standard web protocols. The first part of that definition is similar
to that of COM/DCOM components. However, it is the second part that
distinguishes Web Services from the crowd. Web Services enable software
to interoperate with a much broader range of clients. While COM-aware
clients can understand only COM components, Web Services can be

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

consumed by any application that understands how to parse an XML-


formatted stream transmitted through HTTP channels. XML is the key
technology used in Web Services and is used in the following areas of the
Microsoft .NET Web Services framework:
Web Service wire formats
The technology enabling universal understanding of how to perform data
exchanges between the service provider and consumer; the format of data
for the request and response.
Web Service description in WSDL (Web Services Description Language)
The language describing how the service can be used. Think of this as the
instructions on the washing machine at the laundromat telling you where
to put quarters, what buttons to push, etc.
Web Service discovery
The process of advertising or publishing a piece of software as a service
and allowing for the discovery of this service.
Figure 6-1 depicts the architecture of web applications using Windows
DNA, while Figure 6-2 shows .NET-enabled web applications
architecture. As you can see, communication between components of a
web application does not have to be within an intranet. Furthermore,
intercomponent communication can also use HTTP/XML.
Web Services Wire Formats
You may have heard the phrase "DCOM is COM over the wire." Web
Services are similar to DCOM except that the wire is no longer a
proprietary communication protocol. With Web Services, the wire formats
rely on more open Internet protocols such as HTTP or SMTP.
A web service is more or less a component running on the web server,
exposed to the world through standard Internet protocols. Microsoft .NET
Web Services currently supports three protocols: HTTP GET, HTTP
POST, and SOAP (Simple Object Access Protocol), explained in the next
sections. Because these protocols are standard protocols for the Web, it is
very easy for the client applications to use the services provided by the
server.
HTTP GET and HTTP POST
As their names imply, both HTTP GET and HTTP POST use HTTP as
their underlying protocol. The GET and POST methods of the HTTP
protocol have been widely used in ASP (Active Server Pages), CGI, and
other server-side architectures for many years now. Both of these methods
encode request parameters as name/value pairs in the HTTP request. The
GET method creates a query string and appends it to the script's URL on
the server that handles the request. For the POST method, the name/value
pairs are passed in the body of the HTTP request message.
SOAP
Similar to HTTP GET and HTTP POST, SOAP serves as a mechanism for
passing messages between the clients and servers. In this context, the

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

clients are web services consumers, and the servers are the web services.
The clients simply send an XML-formatted request message to the server
to get the service. The server responds by sending back yet another XML-
formatted message. The SOAP specification describes the format of these
XML requests and responses. It is simple, yet extensible, because it is
based on XML.
SOAP is different than HTTP GET and HTTP POST because it uses XML
to format its payload. The messages being sent back and forth have a
better structure and can convey more complex information compared to
simple name/value pairs in HTTP GET/POST protocols. Another
difference is that SOAP can be used on top of other transport protocols,
such as SMTP in addition to HTTP.
Web Services Description (WSDL)
For web service clients to understand how to interact with a web service,
there must be a description of the method calls, or the interface that the
web service supports. This web service description document is found in
an XML schema called WSDL (Web Services Description Language).
Remember that type libraries and IDL scripts are used to describe a COM
component. Both IDL and WSDL files describe an interface's method calls
and the list of in and out parameters for the particular call. The only major
difference between the two description languages is that all descriptions in
the WSDL file are done in XML.
In theory, any WSDL-capable SOAP client can use the WSDL file to get a
description of your web service. It can then use the information contained
in that file to understand the interface and invoke your web service's
methods.
WSDL Structure
The root of any web service description file is the <definitions> element.
Within this element, the following elements provide both the abstract and
concrete description of the service:
Types
A container for datatype definitions.
Message
An abstract, typed definition of the data being exchanged between the web
service providers and consumers. Each web method has two messages:
input and output. The input describes the parameters for the web method;
the output describes the return data from the web method. Each message
contains zero or more <part> parameters. Each parameter associates with a
concrete type defined in the <types> container element.
Port Type
An abstract set of operations supported by one or more endpoints.
Operation
An abstract description of an action supported by the service. Each
operation specifies the input and output messages defined as <message>
elements.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Binding
A concrete protocol and data-format specification for a particular port
type. Similar to port type, the binding contains operations, as well as the
input and output for each operation. The main difference is that with
binding, we are now talking about actual transport type and how the input
and output are formatted.
Service
A collection of network endpoints--ports. Each of the web service wire
formats defined earlier constitutes a port of the service (HTTP GET,
HTTP POST, and SOAP ports).
Port
A single endpoint defined by associating a binding and a network address.
In other words, it describes the protocol and data-format specification to
be used as well as the network address of where the web service clients
can bind to for the service.
The following shows a typical WSDL file structure:
<definitions name="" targetNamespace="" xmlns:...>

<types>...</types>

<message name="">...</message>
...

<portType name="">
<operation name="">
<input message="" />
<output message="" />
</operation>
...
</portType>
...

<binding name="">
<protocol:binding ...>
<operation name="">
<protocol:operation ...>
<input>...</input>
<output>...</output>
</operation>
...
</binding>
...

<service name="">
<port name="" binding="">
<protocol:address location="" />
</port>
...
</service>
</definitions>

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

The <types> element contains physical type descriptions defined in XML


Schema (XSD). These types are being referred to from the <message>
elements.
For each of the web methods in the web service, there are two messages
defined for a particular port: input and output. This means if a web service
supports all three protocols: SOAP, HTTP GET, and HTTP POST, there
will be six <message> elements defined, one pair for each port. The naming
convention used by the Microsoft .NET autogenerated WSDL is:
MethodName + Protocol + {In, Out}
For example, a web method called GetBooks( ) will have the following
messages:
<message name="GetBooksSoapIn">...</message>
<message name="GetBooksSoapOut">...</message>
<message name="GetBooksHttpGetIn">...</message>
<message name="GetBooksHttpGetOut">...</message>
<message name="GetBooksHttpPostIn">...</message>
<message name="GetBooksHttpPostOut">...</message>
For each protocol that the web service supports, there is one <portType>
element defined. Within each <portType> element, all operations are
specified as <operation> elements. The naming convention for the port type
is:
WebServiceName + Protocol
To continue our example, here are the port types associated with the web
service that we build later in this chapter, PubsWS:
<portType name="PubsWSSoap">
<operation name="GetBooks">
<input message="GetBooksSoapIn" />
<output message="GetBooksSoapOut" />
</operation>
</portType>

<portType name="PubsWSHttpGet">
<operation name="GetBooks">
<input message="GetBooksHttpGetIn" />
<output message="GetBooksHttpGetOut" />
</operation>
</portType>

<portType name="PubsWSHttpPost">
<operation name="GetBooks">
<input message="GetBooksHttpPostIn" />
<output message="GetBooksHttpPostOut" />
</operation>
</portType>
We have removed namespaces from the example to make it easier to read.
While the port types are abstract operations for each port, the bindings
provide concrete information on what protocol is being used, how the data
is being transported, and where the service is located. Again, there is a
<binding> element for each protocol supported by the web service:
<binding name="PubsWSSoap" type="s0:PubsWSSoap">
<soap:binding transport="http://schemas.xmlsoap.org/soap/http"
style="document" />

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

<operation name="GetBooks">
<soap:operation soapAction="http://tempuri.org/GetBooks"
style="document" />
<input>
<soap:body use="literal" />
</input>
<output>
<soap:body use="literal" />
</output>
</operation>
</binding>

<binding name="PubsWSHttpGet" type="s0:PubsWSHttpGet">


<http:binding verb="GET" />
<operation name="GetBooks">
<http:operation location="/GetBooks" />
<input>
<http:urlEncoded />
</input>
<output>
<mime:mimeXml part="Body" />
</output>
</operation>
</binding>

<binding name="PubsWSHttpPost" type="s0:PubsWSHttpPost">


<http:binding verb="POST" />
<operation name="GetBooks">
<http:operation location="/GetBooks" />
<input>
<mime:content type="application/x-www-form-urlencoded" />
</input>
<output>
<mime:mimeXml part="Body" />
</output>
</operation>
</binding>
For SOAP protocol, the binding is <soap:binding>, and the transport is
SOAP messages on top of HTTP protocol. The <soap:operation> element
defines the HTTP header soapAction, which points to the web method. Both
input and output of the SOAP call are SOAP messages.
For both the HTTP GET and HTTP POST protocols, the binding is
<http:binding> with the verb being GET and POST, respectively. Because
the GET and POST verbs are part of the HTTP protocol, there is no need
for the extended HTTP header like soapAction for SOAP protocol. The only
thing we need is the URL that points to the web method; in this case, the
<soap:operation> element contains the attribute location which is set to
/GetBooks.
The only real difference between the HTTP GET and POST protocols is
the way the parameters are passed to the web server. HTTP GET sends the
parameters in the query string, while HTTP POST sends the parameters in
the form data. This difference is reflected in the <input> elements of the
operation GetBooks for the two HTTP protocols. For the HTTP GET

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

protocol, the input is specified as <http:urlEncoded />, whereas for the HTTP
POST protocol, the input is <mime:content type="application/x-www-form-
urlencoded" />.
Looking back at the template of the WSDL document, we see that the only
thing left to discuss is the <service> element, which defines the ports
supported by this web service. For each of the supported protocol, there is
one <port> element:
<service name="PubsWS">

<port name="PubsWSSoap" binding="s0:PubsWSSoap">


<soap:address
location="http://.../PubsWs.asmx" />
</port>

<port name="PubsWSHttpGet" binding="s0:PubsWSHttpGet">


<http:address
location="http://.../PubsWs.asmx" />
</port>

<port name="PubsWSHttpPost" binding="s0:PubsWSHttpPost">


<http:address
location="http://.../PubsWs.asmx" />
</port>

</service>
Even though the three different ports look similar, their binding attributes
associate the address of the service with a binding element defined earlier.
Web service clients now have enough information on where to access the
service, through which port to access the web service method, and how the
communication messages are defined.
Although it is possible to read the WSDL and manually construct the
HTTP[1] conversation with the server to get to a particular web service,
there are tools that autogenerate client-side proxy source code to do the
same thing. We show such a tool in "Web Services Consumers" later in
this chapter.
Web Services Discovery
Even though advertising of a web service is important, it is optional. Web
services can be private as well as public. Depending on the business
model, some business-to-business (B2B) services would not normally be
advertised publicly. Instead, the web service owners would provide
specific instructions on accessing and using their service only to the
business partner.
To advertise web services publicly, authors post discovery files on the
Internet. Potential web services clients can browse to these files for
information about how to use the web services--the WSDL. Think of it as
the yellow pages for the web service. All it does is point you to where the
actual web services reside and to the description of those web services.
The process of looking up a service and checking out the service
description is called Web Service discovery. There are two ways of

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

advertising the service: static and dynamic. In both of these, XML


conveys the locations of web services.
Static discovery
Static discovery is easier to understand because it is explicit in nature. If
you want to advertise your web service, you must explicitly create the
.disco discovery file and point it to the WSDL.[2] All .disco files contain a
root element discovery as shown in the following code sample. Note that
discovery is in the namespace http://schemas.xmlsoap.org/disco, which is referred
to as disco in this sample.
<?xml version="1.0" ?>
<disco:discovery xmlns:disco="http://schemas.xmlsoap.org/disco">
</disco:discovery>
Inside the discovery element, there can be one or more of contractRef or
discoveryRef elements. Both of these elements are described in the
namespace http://schemas.xmlsoap.org/disco/scl. The contractRef tag is used to
reference an actual web service URL that would return the WSDL or the
description of the actual web service contract. The discoveryRef tag, on the
other hand, references another discovery document.
This XML document contains a link to one web service and a link to
another discovery document:
<?xml version="1.0" ?>
<disco:discovery
xmlns:disco="http://schemas.xmlsoap.org/disco"
xmlns:scl="http://schemas.xmlsoap.org/disco/scl">
<scl:contractRef ref="http://yourWebServer/yourWebService.asmx?WSDL"/>
<scl:discoveryRef ref="http://yourBrotherSite/hisWebServiceDirectory.disco"/>
</disco:discovery>
This sample disco file specifies two different namespaces: disco, which is a
nickname for the namespace, specified at http://schemas.xmlsoap.org/disco; and
scl, which points to http://schemas.xmlsoap.org/disco/scl, where the schema for
the service discovery and service contract language is described. The
contractRef element specifies the URL where yourWebService WSDL can
be obtained. Right below that is the discoveryRef element, which links to the
discovery file on yourBrotherSite web site. This linkage allows for
structuring networks of related discovery documents.
Dynamic discovery
As opposed to explicitly specifying the URL for all web services your site
supports, you can enable dynamic discovery, which enables all web
services underneath a specific URL on your web site to be listed
automatically. For your web site, you might want to group related web
services under many different directories and then provide a single
dynamic discovery file in each of the directory. The root tag of the
dynamic discovery file is dynamicDiscovery instead of discovery.
<?xml version="1.0" ?>
<dynamicDiscovery xmlns="urn://schemas-dynamic:disco.2000-03-17" />
You can optionally specify exclude paths so that the dynamic mechanism
does not have to look for web services in all subdirectories underneath the

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

location of the dynamic discovery file. Exclude paths are in the following
form:
<exclude path="pathname" />
If you run IIS as your web server, you'd probably have something like the
following for a dynamic discovery file:
<?xml version="1.0" ?>
<dynamicDiscovery xmlns="urn://schemas-dynamic:disco.2000-03-17">
<exclude path="_vti_cnf" />
<exclude path="_vti_pvt" />
<exclude path="_vti_log" />
<exclude path="_vti_script" />
<exclude path="_vti_txt" />
</dynamicDiscovery>
Discovery setting in practice
A combination of dynamic and static discovery makes a very flexible
configuration. For example, you can provide static discovery documents at
each of the directories that contain web services. At the root of the web
server, provide a dynamic discovery document with links to all static
discovery documents in all subdirectories. To exclude web services from
public viewing, provide the exclude argument to XML nodes to exclude
their directories from the dynamic discovery document.
UDDI
Universal Description, Discovery, and Integration (UDDI) Business
Registry is like a yellow pages of web services. It allows businesses to
publish their services and locate web services published by partner
organizations so that they can conduct transactions quickly, easily, and
dynamically with their trading partner.
Through UDDI APIs, businesses can find services over the web that match
their criteria (e.g., cheapest fare), that offer the service they request (e.g.,
delivery on Sunday), and so on. Currently backed by software giants such
as Microsoft, IBM, and Ariba, UDDI is important to Web Services
because it enables access to businesses from a single place.
The System.Web.Services Namespace
Now that we have run through the basic framework of Microsoft .NET
Web Services, let us take a look inside what the .NET SDK provides us in
the System.Web.Services namespace.
There are only a handful of classes in the System.Web.Services
namespace:
WebService
The base class for all web services.
WebServiceAttribute
An attribute that can be associated with a Web Service-derived class.
WebMethodAttribute
An attribute that can be associated with public methods within a Web
Service-derived class.
WebServicesConfiguration

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Information needed for the Web Service runtime.


WebServicesConfigurationSectionHandler
Information needed for the Web Service runtime.
The two most important classes in the System.Web.Services namespace
for creating web services are the WebService base class and
WebMethodAttribute. We make use of these classes in the next section,
where we implement a Web Service provider and several Web Service
consumers.
WebService is the base class from which all web services inherit. It
provides properties inherent to legacy ASP programming such as
Application, Server, Session, and a new property, Context, which now
includes Request and Response.
The WebMethodAttribute class allows you to apply attributes to each
public method of your web service. Using this class, you can assign
specific values to the following attributes: description, session state
enabling flag, message name, and transaction mode. See the following
section for an example of attribute setting in C# and VB.
The WebServiceAttribute class is used to provide more attributes about
the web service itself. You can display a description of the web service, as
well as the namespace to which this web service belongs. In this book, we
do not discuss helper classes dealing with the runtime of web services.

Web Services Provider


In this section, we describe how to develop a web service, first from the
point of view of service providers and then of the consumers.
Web Services providers implement web services and advertise them so
that the clients can discover and make use of the services. Because web
services run on top of HTTP, there must be a web server application of
some sort on the machine that hosts the web services. This web server
application can be Microsoft Internet Information Services (IIS), Apache,
or any other program that can understand and process the HTTP protocol.
In our examples, we use Microsoft IIS, since that is the only web server
currently supported by .NET.
Web Service Provider Example
We will be building a web service called PubsWS to let consumers get
information from the sample Pubs database. All data access will be done
through ADO.NET, so make sure you've read Chapter 5 before attempting
the examples.
Creating a web service is a three-step process.
1. Create a new asmx file for the web service. This must contain the
<% webservice ... %> directive, as well as the class that provides the
web service implementation. To the Web Service clients, this asmx
file is the entry point to your Web Service. You need to put this in
a virtual directory that has the executescripts permission turned on.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

2. Inherit from the WebService class of the System.Web.Services


namespace. This allows the derived class to access all the normal
ASP objects exposed in the WebService base class. From this
point, you can use these ASP objects as if you were developing an
ASP-based application.[3] It is highly recommended that you
specify a namespace for your web service before publishing it
publicly because the default namespace, http://tempuri.org/, will
not uniquely identify your web service from other web services. To
do this, all you have to do is to tag the web service class with the
Namespace attribute, specifying your own namespace.

3. Tag the public methods with WebMethod attributes to make web


methods--public methods of a distributed component that are
accessible via the Web. You don't have to tag a method as
WebMethod unless you want that method to be published as a web
method.
The following C# code demonstrates a simple web service that exposes
four methods to Internet clients. We emphasize "Internet" because anyone
that can access this asmx file on the web server can access these methods,
as opposed to your COM component, which can be accessed only by
COM clients:
<%@ WebService Language="C#" Class="PubsWS.PubsWS" %>

namespace PubsWS
{
using System;
using System.Data;
using System.Data.OleDb;
using System.Web;
using System.Web.Services;

[WebService(Namespace="http://Oreilly/DotNetEssentials/")]
public class PubsWS : WebService
{
private static string m_sConnStr =
"provider=sqloledb;server=(local);database=pubs;uid=sa;pwd=;";

[WebMethod(Description="Returns a DataSet containing all authors.")]


public DataSet GetAuthors( )
{
OleDbDataAdapter oDBAdapter;
DataSet oDS;

oDBAdapter = new OleDbDataAdapter("select * from authors",


m_sConnStr);
oDS = new DataSet( );
oDBAdapter.Fill(oDS, "Authors");
return oDS;
}

[WebMethod]
public DataSet GetAuthor(string sSSN)

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

{
OleDbDataAdapter oDBAdapter;
DataSet oDS;

oDBAdapter = new OleDbDataAdapter(


"select * from authors where au_id ='"
+ sSSN + "'", m_sConnStr);
oDS = new DataSet( );
oDBAdapter.Fill(oDS, "SelectedAuthor");
return oDS;
}

[WebMethod(MessageName="GetBooksByAuthor",
Description="Find books by author's SSN.")]
public DataSet GetBooks(string sAuthorSSN)
{
OleDbDataAdapter oDBAdapter;
DataSet oDS;

oDBAdapter = new OleDbDataAdapter(


"select * from titles inner join titleauthor on " +
"titles.title_id=titleauthor.title_id " +
"where au_id='" + sAuthorSSN + "'", m_sConnStr);
oDS = new DataSet( );
oDBAdapter.Fill(oDS, "Books");
oDBAdapter = new OleDbDataAdapter("select * from authors " +
"where au_id='" + sAuthorSSN + "'", m_sConnStr);
oDBAdapter.Fill(oDS, "Author");

return oDS;
}

[WebMethod]
public DataSet GetBooks( )
{
OleDbDataAdapter oDBAdapter;
DataSet oDS;

oDBAdapter = new OleDbDataAdapter("select * from titles" ,


m_sConnStr);
oDS = new DataSet( );
oDBAdapter.Fill(oDS, "Books");
return oDS;
}

} // end PubsWS
}
If you are familiar with ASP, you may recognize the usage of the @
symbol in front of keyword WebService. This WebService directive
specifies the language of the web service so that ASP.NET can compile
the web service with the correct compiler. This directive also specifies the
class that implements the web service so it can load the correct class and
employ reflection to generate the WSDL for the web service.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Because PubsWS also uses ADO.NET's OLE DB provider for its data-
access needs, we have to add a reference to System.Data and
System.Data.OleDb, in addition to the System, System.Web, and
System.Web.Services namespaces.
Class PubsWS inherits from WebService with the colon syntax that should
be familiar to C++ or C# developers:
public class PubsWS : WebService
The four methods that are tagged with WebMethod attributes are
GetAuthors( ), GetAuthor( ), GetBooks( string), and GetBooks( ). In C#,
you can tag public methods with a WebMethod attribute using the []
syntax. In VB, you must use < >. For example, in VB, the second method
would be declared as:
Public Function <WebMethod( )> GetAuthor(sSSN As String) As DataSet
By adding [WebMethod] in front of your public method, you make the
public method callable from any Internet client. What goes on behind the
scenes is that your public method is associated with an attribute, which is
implemented as a WebMethodAttribute class. WebMethodAttribute has
six properties:
BufferResponse (boolean)
Controls whether or not to buffer the method's response.
CacheDuration
Specifies the length of time, in seconds, to keep the method response in
cache. The default is not to hold the method response in cache (0 seconds).
Description
Provides additional information about a particular web method.
EnableSession (boolean)
Enables or disables session state. If you don't intend to use session state
for the web method, you might want to disable this flag so that the web
server does not have to generate and manage session IDs for each user
accessing this web method. This might improve performance. This flag is
true by default.
MessageName
Distinguishes web methods with the same names. For example, if you
have two different methods called GetBooks (one method retrieves all
books while the other method retrieves only books written by a certain
author) and you want to publish both of these methods as web methods,
the system will have a problem trying to distinguish the two methods since
their names are duplicated. You have to use the MessageName property to
make sure all service signatures within the WSDL are unique. If the
protocol is SOAP, MessageName is mapped to the SOAPAction request
header and nested within the soap:Body element. For HTTP GET and HTTP
POST, it is the PathInfo portion of the URI (as in
http://localhost//PubsWS/PubsWS.asmx/GetBooksByAuthor).
TransactionOption
Can be one of five modes: Disabled, NotSupported, Supported, Required,
and RequiresNew. Even though there are five modes, web methods can
only participate as the root object in a transaction. This means both

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Required and RequiresNew result in a new transaction being created for


the web method. The Disabled, NotSupported, and Supported settings
result in no transaction being used for the web method. The
TransactionOption property of a web method is set to Disabled by default.
To set up these properties, pass the property name and its value as a name
= value pair:
[WebMethod(Description="Returns a DataSet containing all authors.")]
public DataSet GetAuthors( )
You can separate multiple properties with a comma:
[WebMethod(MessageName="GetBooksByAuthor",
Description="Find books by author's SSN.")]
public DataSet GetBooks(string sAuthorSSN)
Web.Config
If you set up your web services from scratch, you might also need to
provide the configuration file (web.config) in the same directory as your
asmx file. This configuration file allows you to control various application
settings about the virtual directory. The only thing we recommend
definitively is to set the authentication mode to None to make our web
services development and testing a little easier. When you release your
web services to the public, you would probably change this setting to
Windows, Forms, or Passport instead of None:
<configuration>
<system.web>
<authentication mode="None" />
</system.web>
</configuration>
The following list shows the different modes of authentication:
Forms
Basic Forms authentication is where unauthenticated requests are
redirected to a login form.
Windows
Authentication is performed by IIS in one of three ways: basic, digest, or
Integrated Windows Authentication.
Passport
Unauthenticated requests to the resource are redirected to Microsoft's
centralized authentication service. When authenticated, a token is passed
back and used by subsequent requests.

Web Services Consumers


Now that you have successfully created a web service, let's take a look at
how this web service is used by web clients.
Web Services clients communicate with web services through standard
web protocols. They send and receive XML-encoded messages to and
from the web services. This means any application on any platform can
access the web services as long as it uses standard web protocols and
understands the XML-encoded messages. As mentioned earlier, there are
three protocols that the web clients can employ to communicate with the

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

servers (web services): HTTP GET, HTTP POST, and SOAP. We


demonstrate next how to build client applications that utilize each of these
protocols. These web services-client applications are done in both VB6
and .NET languages, such as C# and VB.NET, to demonstrate the cross-
language/cross-platform benefits of Web Services. For example, you can
replace the example in VB6 with Perl running on Unix, and the web
services should still be serving.
HTTP GET Consumer
Let's look at how it is done using HTTP GET first, since it is the simplest.
In the examples that follow, we use localhost as the name of the web
server running the service and PubsWS as the virtual directory. If you have
deployed the sample web service on a remote server, you'll need to
substitute the name of the server and virtual directory as appropriate.
If you point your web browser at the web service URL
(http://localhost/PubsWS/PubsWS.asmx), it will give you a list of
supported methods. To find out more about these methods, click one of
them. This brings up a default web service consumer. This consumer,
autogenerated through the use of reflection, is great for testing your web
services' methods.[4] It uses the HTTP GET protocol to communicate with
the web service. This consumer features a form that lets you test the
method (see Figure 6-3), as well as descriptions of how to access the
method via SOAP, HTTP GET, or HTTP POST.
Here is the description of the GET request and response supplied by the
default consumer:
The following is a sample HTTP GET request and response. The placeholders shown
need to be replaced with

actual values.

GET /PubsWS/PubsWS.asmx/GetAuthor?sSSN=string HTTP/1.1


Host: localhost

HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
Content-Length: length

<?xml version="1.0" encoding="utf-8"?>


<DataSet xmlns="http://tempuri.org/">
<schema xmlns="http://www.w3.org/2001/XMLSchema">schema</schema>xml
</DataSet>
Using HTTP GET protocol, the complete URL to invoke the web method,
along with parameters, can be the following:
http://localhost/PubsWS/PubsWS.asmx/GetAuthor?sSSN=172-32-1176
Here is the response, including HTTP response headers and the raw XML
(note how the response includes the serialized schema and data from the
DataSet object):
Cache-Control: private, max-age=0
Date: Tue, 08 May 2001 20:53:16 GMT
Server: Microsoft-IIS/5.0

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Content-Length: 2450
Content-Type: text/xml; charset=utf-8
Client-Date: Tue, 08 May 2001 20:53:16 GMT
Client-Peer: 127.0.0.1:80

<?xml version="1.0" encoding="utf-8"?>


<DataSet xmlns="http://tempuri.org/">
<xsd:schema id="NewDataSet"
targetNamespace="" xmlns=""
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="NewDataSet" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="SelectedAuthor">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="au_id"
msdata:DefaultValue="NULL" type="xsd:string"
minOccurs="0" msdata:Ordinal="0" />
<xsd:element name="au_lname"
msdata:DefaultValue="NULL" type="xsd:string"
minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="au_fname"
msdata:DefaultValue="NULL" type="xsd:string"
minOccurs="0" msdata:Ordinal="2" />
<xsd:element name="phone"
msdata:DefaultValue="NULL" type="xsd:string"
minOccurs="0" msdata:Ordinal="3" />
<xsd:element name="address"
msdata:DefaultValue="NULL" type="xsd:string"
minOccurs="0" msdata:Ordinal="4" />
<xsd:element name="city"
msdata:DefaultValue="NULL" type="xsd:string"
minOccurs="0" msdata:Ordinal="5" />
<xsd:element name="state"
msdata:DefaultValue="NULL" type="xsd:string"
minOccurs="0" msdata:Ordinal="6" />
<xsd:element name="zip"
msdata:DefaultValue="NULL" type="xsd:string"
minOccurs="0" msdata:Ordinal="7" />
<xsd:element name="contract"
msdata:DefaultValue="NULL" type="xsd:boolean"
minOccurs="0" msdata:Ordinal="8" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<NewDataSet xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"
xmlns:updg="urn:schemas-microsoft-com:xml-updategram">
<updg:sync>
<msdata:unchanged>
<SelectedAuthor updg:id="f5237587-4918-44c6-b5cb-51a84e6af4e3"

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

xmlns="">
<au_id>172-32-1176</au_id>
<au_lname>White</au_lname>
<au_fname>Johnson</au_fname>
<phone>408 496-7223</phone>
<address>10932 Bigge Rd.</address>
<city>Menlo Park</city>
<state>CA</state>
<zip>94025</zip>
<contract>true</contract>
</SelectedAuthor>
</msdata:unchanged>
</updg:sync>
</NewDataSet>
</DataSet>

HTTP POST Consumer


In the section "HTTP GET Consumer," we saw the automatic creation of a
web services consumer just by hitting the URL of the web services,
http://localhost/PubsWS/PubsWS.asmx. It is now time for us to see how a
web client can use HTTP POST and SOAP to access a web service. This
time around, we are going write a C# web service consumer.
The Microsoft .NET SDK comes with a rich set of tools to simplify the
process of creating or consuming web services. We are going to use one of
these tools, wsdl, to generate source code for the proxies to the actual web
services:[5]
wsdl /l:CS /protocol:HttpPost http://localhost/PubsWS/PubsWS.asmx?WSDL
This command line creates a proxy for the PubsWS web service from the
WSDL (Web Services Description Language) document obtained from the
URL http://localhost/PubsWS/PubsWS.asmx?WSDL. The proxy uses
HTTP POST as its protocol to talk to the web service and is generated as a
C# source file.
The wsdl tool can also take a WSDL file as its input instead of a URL
pointing to the location where the WSDL can be obtained.
This C# proxy source file represents the proxy class for the PubsWS web
service that the clients can compile against. If you look at this generated
C# file, you will see that it contains a proxy class PubsWS that derives
from HttpPostClientProtocol class. If you use the /protocol:HttpGet or
/protocol:SOAP parameters, then the PubsWS derives from either the
HttpGetClientProtocol or SoapHttpClientProtocol class.
After generating the C# source file PubsWS.cs, we are faced with two
choices for how this proxy can be used. One way is to include this source
file in the client application project using Visual Studio.NET. The project
has to be a C# project if you choose this route. To make use of the proxy,
you also have to add to your project any references that the proxy depends
on. In this example, the necessary references for the proxy file are
System.Web.Services, System.Web.Services.Protocols,
System.Xml.Serialization, and System.Data.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

The other way to use the proxy is more flexible. You can compile the C#
source file into a dynamic link library (DLL) and then add a reference to
this DLL to any project you want to create. This way you can even have a
VB project use the DLL.
Below is the command line used to compile the C# proxy source into a
DLL. Notice that the three references are linked to PubsWS.cs so that the
resulting PubsWS.DLL is self-contained (type the entire command on one
line):
csc /t:library
/r:system.web.services.dll
/r:system.xml.dll
/r:system.data.dll
PubsWS.cs
Regardless of how you choose to use the proxy, the client application code
will still look the same. Consider the next two code examples containing
C# and VB code. For both languages, the first lines create an instance of
the proxy to the web service, PubsWS. The second lines invoke the
GetBooks web method to get a DataSet as the result. The remaining lines
bind the default view of the table Books to the data grid, add the data grid
to a form, and display the form. Note that these examples use the
Windows Forms API, which we'll discuss in Chapter 8.
Here is the C# web service client, TestProxy.cs :
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Data;

public class TestProxy


{

public static void Main( )


{

/* Create a proxy. */
PubsWS oProxy = new PubsWS( );

/* Invoke GetBooks( ) over SOAP and get the data set. */


DataSet oDS = oProxy.GetBooks( );

/* Create a data grid and connect it to the data set. */


DataGrid dg = new DataGrid( );
dg.Size = new Size(490, 270);
dg.DataSource = oDS.Tables["Books"].DefaultView;

/* Set the properties of the form and add the data grid. */
Form myForm = new Form( );
myForm.Text = "DataGrid Sample";
myForm.Size = new Size(500, 300);
myForm.Controls.Add(dg);

/* Display the form. */


System.Windows.Forms.Application.Run(myForm);

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

}
If you created the DLL as previously directed, you can compile this with
the following command:
csc TestProxy.cs /r:PubsWS.dll
This creates the executable TestProxy.exe, which gets a DataSet using a
SOAP call, and displays a data grid containing that dataset. Figure 6-4
shows the output of the C# client after obtaining the data from the
PubsWS web service via SOAP protocol.
Figure 6-4. C# web service client after calling GetBooks( )

Here is an excerpt from the VB web service client, TestProxy.vb :


Dim oProxy as PubsWS = New PubsWS( )
Dim oDS as DataSet = oProxy.GetBooks( )
DataGrid1.DataSource = oDS.Tables("Books").DefaultView
You can compile the VB web service client with this command (type the
entire command on one line):
vbc TestProxy.vb
/r:System.Drawing.dll
/r:System.Windows.Forms.dll
/r:System.Data.dll
/r:PubsWS.dll
/r:System.Web.Services.dll
/r:System.dll
/r:System.xml.dll

Non-.NET Consumers
This section shows how to develop non-.NET web service consumers
using HTTP GET, HTTP POST, and SOAP protocols. Because we cannot
just create the proxy class from the WSDL and compile it with the client
code directly, we must look at the WSDL file to understand how to
construct and interpret messages between the web service and the clients.
We trimmed down the WSDL file for our PubsWS web service to show
only types, messages, ports, operations, and bindings that we actually use
in the next several web service-client examples. In particular, we will have
our VB6 client access the following:
Web method Protocol

GetBooks( ) HTTP GET protocol

GetAuthor(ssn) HTTP POST protocol

GetBooksByAuthor(ssn) SOAP protocol


As a reference, here is the simplified version of the WSDL file while you
experiment with the VB6 client application:

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

<?xml version="1.0" encoding="utf-8"?>


<definitions xmlns:...
xmlns:s0="http://Oreilly/DotNetEssentials/"
targetNamespace="http://Oreilly/DotNetEssentials/" >

<types>
<!-- This datatype is used by the HTTP POST call -->
<s:element name="GetAuthor">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1"
name="sSSN" nillable="true" type="s:string" />
</s:sequence>
</s:complexType>
</s:element>
<!-- This datatype is used by the HTTP POST call -->
<s:element name="GetAuthorResponse">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1"
name="GetAuthorResult" nillable="true">
<s:complexType>
<s:sequence>
<s:element ref="s:schema" />
<s:any />
</s:sequence>
</s:complexType>
</s:element>
</s:sequence>
</s:complexType>
</s:element>

<!-- This datatype is used by the SOAP call -->


<s:element name="GetBooksByAuthor">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1"
name="sAuthorSSN" nillable="true" type="s:string" />
</s:sequence>
</s:complexType>
</s:element>
<!-- This datatype is used by the SOAP call -->
<s:element name="GetBooksByAuthorResponse">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1"
name="GetBooksByAuthorResult" nillable="true">
<s:complexType>
<s:sequence>
<s:element ref="s:schema" />
<s:any />
</s:sequence>
</s:complexType>
</s:element>
</s:sequence>
</s:complexType>

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

</s:element>

<!-- This datatype is used by the HTTP GET call -->


<s:element name="GetBooks">
<s:complexType />
</s:element>
<!-- This datatype is used by the HTTP GET call -->
<s:element name="GetBooksResponse">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1"
name="GetBooksResult" nillable="true">
<s:complexType>
<s:sequence>
<s:element ref="s:schema" />
<s:any />
</s:sequence>
</s:complexType>
</s:element>
</s:sequence>
</s:complexType>
</s:element>

<!-- This datatype is used by the HTTP GET/POST responses -->


<s:element name="DataSet" nillable="true">
<s:complexType>
<s:sequence>
<s:element ref="s:schema" />
<s:any />
</s:sequence>
</s:complexType>
</s:element>

</types>

<!-- These messages are used by the SOAP call -->


<message name="GetBooksByAuthorSoapIn">
<part name="parameters" element="s0:GetBooksByAuthor" />
</message>
<message name="GetBooksByAuthorSoapOut">
<part name="parameters" element="s0:GetBooksByAuthorResponse" />
</message>

<!-- These messages are used by the HTTP GET call -->
<message name="GetBooksHttpGetIn" />
<message name="GetBooksHttpGetOut">
<part name="Body" element="s0:DataSet" />
</message>

<!-- These messages are used by the HTTP POST call -->
<message name="GetAuthorHttpPostIn">
<part name="sSSN" type="s:string" />
</message>
<message name="GetAuthorHttpPostOut">
<part name="Body" element="s0:DataSet" />
</message>

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

<!-- SOAP port -->


<portType name="PubsWSSoap">
<operation name="GetBooks">
<documentation>Find books by author's SSN.</documentation>
<input name="GetBooksByAuthor"
message="s0:GetBooksByAuthorSoapIn" />
<output name="GetBooksByAuthor"
message="s0:GetBooksByAuthorSoapOut" />
</operation>
</portType>

<!-- HTTP GET port -->


<portType name="PubsWSHttpGet">
<operation name="GetBooks">
<input message="s0:GetBooksHttpGetIn" />
<output message="s0:GetBooksHttpGetOut" />
</operation>
</portType>

<!-- HTTP POST port -->


<portType name="PubsWSHttpPost">
<operation name="GetAuthor">
<input message="s0:GetAuthorHttpPostIn" />
<output message="s0:GetAuthorHttpPostOut" />
</operation>
</portType>

<!-- SOAP binding -->


<binding name="PubsWSSoap" type="s0:PubsWSSoap">
<soap:binding
transport="http://schemas.xmlsoap.org/soap/http"
style="document" />
<operation name="GetBooks">
<soap:operation
soapAction="http://Oreilly/DotNetEssentials/GetBooksByAuthor"
style="document" />
<input name="GetBooksByAuthor">
<soap:body use="literal" />
</input>
<output name="GetBooksByAuthor">
<soap:body use="literal" />
</output>
</operation>
</binding>

<!-- HTTP GET binding -->


<binding name="PubsWSHttpGet" type="s0:PubsWSHttpGet">
<http:binding verb="GET" />
<operation name="GetBooks">
<http:operation location="/GetBooks" />
<input>
<http:urlEncoded />
</input>
<output>
<mime:mimeXml part="Body" />

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

</output>
</operation>
</binding>

<!-- HTTP POST binding -->


<binding name="PubsWSHttpPost" type="s0:PubsWSHttpPost">
<http:binding verb="POST" />
<operation name="GetAuthor">
<http:operation location="/GetAuthor" />
<input>
<mime:content type="application/x-www-form-urlencoded" />
</input>
<output>
<mime:mimeXml part="Body" />
</output>
</operation>
</binding>

<!-- The whole Web Service and address bindings -->


<service name="PubsWS">

<port name="PubsWSSoap" binding="s0:PubsWSSoap">


<soap:address location="http://localhost/PubsWS/PubsWS.asmx" />
</port>

<port name="PubsWSHttpGet" binding="s0:PubsWSHttpGet">


<http:address location="http://localhost/PubsWS/PubsWS.asmx" />
</port>

<port name="PubsWSHttpPost" binding="s0:PubsWSHttpPost">


<http:address location="http://localhost/PubsWS/PubsWS.asmx" />
</port>

</service>

</definitions>
In both the HTTP GET and HTTP POST protocols, you pass parameters
to the web services as name/value pairs. With the HTTP GET protocol,
you must pass parameters in the query string, whereas the HTTP POST
protocol packs the parameters in the body of the request package. To
demonstrate this point, we will construct a simple VB client using both
HTTP GET and HTTP POST protocols to communicate with the PubsWS
web service.
Let's first create a VB6 standard application. We need to add a reference to
Microsoft XML, v3.0 (msxml3.dll ), because we'll use the XMLHTTP
object to help us communicate with the web services. For demonstrative
purposes, we will also use the Microsoft Internet Controls component
(shdocvw.dll ) to display XML and HTML content.
First, add two buttons on the default form, form1, and give them the
captions GET and POST, as well as the names cmdGet and cmdPost,
respectively. After that, drag the WebBrowser object from the toolbar onto
the form, and name the control myWebBrowser. If you make the

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

WebBrowser navigate to about:blank initially, you will end up with


something like Figure 6-5.
Figure 6-5. VB client form to test Web Services

Now all we need is some code similar to the following to handle the two
buttons' click events:
Private Sub cmdGet_Click( )
Dim oXMLHTTP As XMLHTTP
Dim oDOM As DOMDocument
Dim oXSL As DOMDocument

' Call the web service to get an XML document


Set oXMLHTTP = New XMLHTTP
oXMLHTTP.open "GET",_
"http://localhost/PubsWS/PubsWS.asmx/GetBooks", _
False
oXMLHTTP.send
Set oDOM = oXMLHTTP.responseXML

' Create the XSL document to be used for transformation


Set oXSL = New DOMDocument
oXSL.Load App.Path & "\templateTitle.xsl"

' Transform the XML document into an HTML document and display
myWebBrowser.Document.Write CStr(oDOM.transformNode(oXSL))
myWebBrowser.Document.Close

Set oXSL = Nothing


Set oDOM = Nothing
Set oXMLHTTP = Nothing
End Sub

Private Sub cmdPost_Click( )


Dim oXMLHTTP As XMLHTTP
Dim oDOM As DOMDocument
Dim oXSL As DOMDocument

' Call the web service to get an XML document


Set oXMLHTTP = New XMLHTTP
oXMLHTTP.open "POST", _
"http://localhost/PubsWS/PubsWS.asmx/GetAuthor", _
False
oXMLHTTP.setRequestHeader "Content-Type", _
"application/x-www-form-urlencoded"
oXMLHTTP.send "sSSN=172-32-1176"
Set oDOM = oXMLHTTP.responseXML

' Create the XSL document to be used for transformation


Set oXSL = New DOMDocument
oXSL.Load App.Path & "\templateAuthor.xsl"

' Transform the XML document into an HTML document and display
myWebBrowser.Document.Write oDOM.transformNode(oXSL)

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

myWebBrowser.Document.Close

Set oXSL = Nothing


Set oDOM = Nothing
Set oXMLHTTP = Nothing
End Sub
The two subroutines are similar in structure, except that the first one uses
the HTTP GET protocol and the second one uses the HTTP POST
protocol to get to the PubsWS web service. Let's take a closer look at what
the two subroutines do.
For the HTTP GET protocol, we use the XMLHTTP object to point to the
URL for the web method, as specified in the WSDL. Since the GetBooks
web method does not require any parameters, the query string in this case
is empty. The method is invoked synchronously because the async
parameter to XMLHTTP's open method is set to false. After the method
invocation is done, we transform the XML result using templateTitle.xsl
and display the HTML on the myWebBrowser instance on the form. Figure
6-6 displays the screen of our web services testing application after
invoking the GetBooks web method at URL
http://localhost/PubsWS/PubsWS.asmx/ through HTTP GET protocol.
For the HTTP POST protocol, we also point the XMLHTTP object to the
URL for the web method--in this case, method GetAuthor. Because this is
a POST request, we have to specify in the HTTP header that the request is
coming over as a form by setting the Content-Type header variable to
application/x-www-form-urlencoded. If this variable is not set, XMLHTTP by
default passes the data to the server in XML format.
Another difference worth noticing is that the GetAuthor method requires a
single parameter, which is the SSN of the author as a string. Since this is a
post request, we are going to send the name/value pair directly to the
server in the body of the message. Because the Content-Type header has
been set to application/x-www-form-urlencoded, the server will know how to
get to the parameters and perform the work requested. This time, we use
templateAuthor.xsl to transform the XML result to HTML and display it.
Figure 6-7 shows our application after invoking the GetAuthor web
method of PubsWS web service through HTTP POST protocol.
Figure 6-7. VB client form after calling GetAuthor

The following code is the XSL used to transform the XML result from the
GetBooks web method call to HTML to be displayed on the web browser
instance on the VB form:
<html version="1.0" xmlns:xsl="http://www.w3.org/TR/WD-xsl">
<head><title>A list of books</title></head>
<style>
.hdr { background-color=#ffeedd; font-weight=bold; }
</style>
<body>
<B>List of books</B>

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

<table style="border-collapse:collapse" border="1">


<tr>
<td class="hdr">Title</td>
<td class="hdr">Type</td>
<td class="hdr">Price</td>
<td class="hdr">Notes</td>
</tr>
<xsl:for-each select="//Books">
<tr>
<td><xsl:value-of select="title"/></td>
<td><xsl:value-of select="type"/></td>
<td><xsl:value-of select="price"/></td>
<td><xsl:value-of select="notes"/></td>
</tr>
</xsl:for-each>
</table>
</body>
</html>
Here is the XSL used to transform the XML result from the GetAuthor
web method call to HTML to be displayed on the web browser instance on
the VB form:
<html version="1.0" xmlns:xsl="http://www.w3.org/TR/WD-xsl">
<head><title>Selected author</title></head>
<STYLE>
.hdr { background-color:'#ffeedd';
text-align:'right'; vertical-align:'top';
font-weight=bold; }
</STYLE>
<body>
<B>Selected author</B>
<xsl:for-each select="//SelectedAuthor">
<table style="border-collapse:'collapse'" border="1">
<tr><td class="hdr">ID</td>
<td><xsl:value-of select="au_id"/></td></tr>
<tr><td class="hdr">Name</td>
<td><xsl:value-of select="au_fname"/>
<xsl:value-of select="au_lname"/></td></tr>
<tr><td class="hdr">Address</td>
<td><xsl:value-of select="address"/><br>
<xsl:value-of select="city"/>,
<xsl:value-of select="state"/>
<xsl:value-of select="zip"/></br></td></tr>
<tr><td class="hdr">Phone</td>
<td><xsl:value-of select="phone"/></td></tr>
</table>
</xsl:for-each>
</body>
</html>
We can also use SOAP protocol to access the web service. Because the
web service is exposed through HTTP and XML, any clients on any
platform can access the service as long as they conform to the
specification of the service. Again, this specification is the WSDL file. By
inspecting the WSDL file--specifically, the SOAP section--we can use

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

XMLHTTP again to communicate in SOAP dialog. Let's see how this can
be done.
Let's go back to the example of consumer web services using VB6 and
XMLHTTP. Add another button on the form, and call it cmdSOAP with
caption SOAP. This time, we will ask the web service to return all books
written by a particular author:
Private Sub cmdSOAP_Click( )
Dim oXMLHTTP As XMLHTTP
Dim oDOM As DOMDocument
Dim oXSL As DOMDocument

' Call the web service to get an XML document


Set oXMLHTTP = New XMLHTTP
oXMLHTTP.open "POST", "http://localhost/PubsWS/PubsWS.asmx", False

Dim sB As String

sBody = "" & _


"<soap:Envelope" & _
" xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance""" & _
" xmlns:xsd=""http://www.w3.org/2001/XMLSchema""" & _
" xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"">" & _
"<soap:Body>" & _
"<GetBooksByAuthor xmlns=""http://Oreily/DotNetEssential/"">" & _
"<sAuthorSSN>213-46-8915</sAuthorSSN>" & _
"</GetBooksByAuthor>" & _
"</soap:Body>" & _
"</soap:Envelope>"

oXMLHTTP.setRequestHeader "Content-Type", "text/xml"


oXMLHTTP.setRequestHeader "SOAPAction",
"http://Oreilly/DotNetEssentials/GetBooksByAuthor"

oXMLHTTP.send sB

Set oDOM = oXMLHTTP.responseXML

' Create the XSL document to be used for transformation


Set oXSL = New DOMDocument
oXSL.Load App.Path & "\templateAuthorTitle.xsl"

' Transform the XML document into an HTML document


myWebBrowser.Document.Write oDOM.transformNode(oXSL)
myWebBrowser.Document.Close

Set oXSL = Nothing


Set oDOM = Nothing
Set oXMLHTTP = Nothing
End Sub
This method is structurally similar to the ones used for HTTP GET and
HTTP POST; however, it has some very important differences. In SOAP,
you have to set the Content-Type to text/xml instead of application/ x-www-form-
urlencoded as for the HTTP POST. By this time, it should be clear to you
that only HTTP POST and SOAP care about the Content-Type because they

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

send the data in the body of the HTTP request. The HTTP GET protocol
does not really care about the Content-Type because all of the parameters are
packaged into the query string. In addition to the difference in format of
the data content, you also have to refer to the WSDL to set the SOAPAction
header variable to the call you want. Looking back at the SOAP section of
the WSDL, if you want to call the GetBooks(sAuthorSSN) method of the web
service, you will set the SOAPAction header variable to
http://Oreilly/DotNetEssentials/GetBooksByAuthor. On the other hand, if you
want to call the GetBooks( ) method instead, the SOAPAction variable has
to be set to http://Oreilly/DotNetEssentials/GetBooks. The reason the namespace
is http://Oreilly/DotNetEssentials/ is because we set it up as the attribute of the
PubsWS web service class.
After setting up the header variables, we pass the parameters to the server
in the body of the message. Whereas HTTP POST passes the parameters
in name/value pairs, SOAP passes the parameters in a well-defined XML
structure:
<soap:Envelope ...namespace omitted...">
<soap:Body>
<GetBooksByAuthor xmlns="http://Oreilly/DotNetEssentials/">
<sAuthorSSN>213-46-8915</sAuthorSSN>
</GetBooksByAuthor>
</soap:Body>
</soap:Envelope>
Both the SOAP request and response messages are packaged within a Body
inside an Envelope. With the previously specified request, the response
SOAP message looks like this:
<?xml version="1.0"?>
<soap:Envelope ...namespace omitted...>
<soap:Body>
<GetBooksByAuthorResult xmlns="http://Oreilly/DotNetEssentials/">
<result>
<xsd:schema id="NewDataSet" ...>

<... content omitted ...>

</xsd:schema>
<NewDataSet xmlns="">
<Books>
<title_id>BU1032</title_id>
<title>The Busy Executive's Database Guide</title>
<... more ...>
</Books>
<Books>
<title_id>BU2075</title_id>
<title>You Can Combat Computer Stress!</title>
<... more ...>
</Books>
<Author>
<au_id>213-46-8915</au_id>
<au_lname>Green</au_lname>
<au_fname>Marjorie</au_fname>
<phone>415 986-7020</phone>

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

<address>309 63rd St. #411</address>


<city>Oakland</city>
<state>CA</state>
<zip>94618</zip>
<contract>True</contract>
</Author>
</NewDataSet>
</result>
</GetBooksByAuthorResult>
</soap:Body>
</soap:Envelope>
Figure 6-8 shows the result of the test form after invoking the
GetBooksByAuthor web method using the SOAP protocol.
Figure 6-8. VB client form after calling GetBooksByAuthor

The XSL stylesheet used for transformation of the resulting XML to


HTML is included here for your reference. Notice that since
GetBooksByAuthor returns two tables in the dataset, author and books, we
can display both the author information and the books that this author
wrote.
<html version="1.0" xmlns:xsl="http://www.w3.org/TR/WD-xsl">
<head><title>A list of books</title></head>
<style>
.hdr { background-color=#ffeedd; font-weight=bold; }
</style>
<body>
<B>List of books written by
<I><xsl:value-of select="//Author/au_fname"/>
<xsl:value-of select="//Author/au_lname"/>
(<xsl:value-of select="//Author/city"/>,
<xsl:value-of select="//Author/state"/>)
</I>
</B>
<table style="border-collapse:collapse" border="1">
<tr>
<td class="hdr">Title</td>
<td class="hdr">Type</td>
<td class="hdr">Price</td>
<td class="hdr">Notes</td>
</tr>
<xsl:for-each select="//Books">
<tr>
<td><xsl:value-of select="title"/></td>
<td><xsl:value-of select="type"/></td>
<td><xsl:value-of select="price"/></td>
<td><xsl:value-of select="notes"/></td>
</tr>
</xsl:for-each>
</table>
</body>
</html>

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

As you can see, we can easily have any type of web service clients
accessing .NET web services. The clients to the web services need to
know how to communicate only in HTTP and understand the Web
Services Description Language (WSDL) to communicate with the server.
By the same token, we can also develop web services in any language and
on any platform as long as we adhere to the specification of WSDL.

Web Services and Security


This section demonstrates how to incorporate security into your web
service. We will do so in two ways: system security and application
security. System-level security allows for restricting access to the web
services from unauthorized clients. It is done in a declarative fashion,
whereas application-level security is more flexible. With system-level
security, you will most likely have the list of authorized clients' IP
addresses that you will let access your web service through the use of
some configuration-management tools. With application-level security,
you will incorporate the authentication into your web service, thus
providing a more flexible configuration.
System Security
Because web services communication is done through HTTP, you can
apply system-level security on web services just as you do for other web
pages or resources on your web site.
There are a number of different ways you can secure your web services.
For a B2B solution, you can use the IIS Administration Tool to restrict or
grant permission to a set of IP addresses, using the Internet Protocol
Security (IPSec) to make sure that the IP address in the TCP/IP header is
authenticated. When you rely only on the client to provide the IP in the
TCP/IP header, hackers can still impersonate other host IPs when
accessing your web services. IPSec authenticates the host addresses using
the Kerberos authentication protocol. You can also use a firewall to
restrict access to your web services for your partner companies. For a
business-to-consumer (B2C) scenario, you can take advantage of the
authentication features of the HTTP protocol.
To show how to use the authentication feature of the HTTP protocol to
secure your web services, let's revisit the example web service we have in
this chapter, PubsWS. All we have to do to secure PubsWS web service is
go to the IIS Admin Tool and choose to edit the File Security properties for
the PubsWS.asmx. Instead of keeping the default setting, which leaves this
file accessible to all anonymous users, we change this setting to Basic
Authentication. After this change, only users that pass the authentication can
make use of the web service.
For real-life situations, of course, we are not just going to use the Basic
Authentication method because it sends the username and password in
clear text through the HTTP channel. We would choose other methods,

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

such as Secure Sockets Layer (SSL) underneath Basic Authentication, so


that the data passed back and forth is secure. Available methods include:
Basic Authentication
Sends the username and password to the web server in clear text. IIS
authenticates the login against the database of users for the domain.
Basic over SSL Authentication
Similar to Basic Authentication, except that the username and password
are sent with Secure Sockets Layer (SSL) encryption.
Digest Authentication
Uses a hashing technique, as opposed to SSL encryption, to send client
credentials securely to the server.
Integrated Windows Authentication
Good for intranet scenarios only. Uses the login information of the client
for authentication.
Client Certificates Authentication
Requires each of the clients to obtain a certificate that is mapped to a user
account. The use of client-side digital certificates is not widespread at this
time.
Application Security
A less systematic way of securing your web services involves taking
security into your own hands. You can program your web services so that
all of their methods require an access token, which can be obtained from
the web service after sending in the client's username and password. The
client credentials can be sent to the server through SSL, which eliminates
the risk of sending clear-text passwords across the wire. Through this SSL
channel, the server returns an access token to the caller, who can use it to
invoke all other web service methods. Of course, all of the other web
methods that you publish have to have one parameter as the token. A
simple pseudocode example of a bank account web service can be as
follows:
Web Service Bank Account
Web Methods:
Login(user id, password) returns access token or nothing
Deposit(access token, account number, amount, balance) returns T/F
Withdraw(access token, account number, amount, balance) returns T/F
The only method that should be on SSL is the Login method. Once the
token is obtained, it can be used for other web methods. Of course, you
should be able to make sure that subsequent calls using this token are
coming from the same IP as the
Login( ) call. You can also incorporate an expiration timestamp on this
access token to ensure that the token only exists in a certain time frame
until a renewal of the access token is needed.
The Microsoft .NET Cryptographic Services can be very useful if you
choose this route. DES, RC2, TripleDES, and RSA encryption/decryption
algorithms are supported along with hashing methods such as SHA and

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

MD5. These implementations in the .NET library enable developers to


avoid low-level grunt work and focus on the application logic.

Summary
In this chapter, we've introduced you to the new paradigm of application--
the enterprise application. You are no longer restricted to homogeneous
platforms for implementing your solutions. With Microsoft Web Services,
your solutions can span many different platforms because the
communication between Web Services is done through standard Internet
protocols such as HTTP and XML. The distributed components in
Windows DNA with which you may be familiar are now replaced by Web
Services. Using Web Services as components in a distributed environment
allows for a heterogeneous system. Not only do the Web Services in your
system not have to be implemented in the same language, they don't even
have to be on the same platform. Because of this greater interoperability,
Web Services are very suitable for business-to-business (B2B) integration.

1. Current Microsoft .NET SOAP implementation runs on top of HTTP.


2. If you use Visual Studio.NET to create your web service, the discovery
file is created automatically.
3. You will have to get to the Request and Response objects through the
Context property of the WebService class.
4. A simply Reflection example can be found in the section .
5. wsdl.exe generates the proxy source code similar to the way IDL
compilers generate source files for DCOM proxies. The only difference is
that WSDL is the language that describes the interface of the software
component, which is XML-based.

ActiveX Data Objects (ADO)


OVERVIEW
What we are going to discuss in this article is Recordset object but you might think why
we have chosen to name the article as ActiveX Data Objects (ADO), this is because you
must have an idea of ADO technology in order to understand the bits and bytes of
Recordset object. So, we will briefly describe the ADO technology before jumping into
the details of Recordset object. The intended audience of this article includes VB as well
as ASP programmers, you must bear it in your mind that you can get more detailed
information about the ADO or any related technology on the Microsoft® site, the only
purpose of this article is to describe the new technology in simple and plain English so
that more people can access and read this article. I have noticed that many new
programmers prefer to search sites other than the Microsoft® site for the help on different
newer technologies, may be because these articles are more easily available to the
programmers and they don’t have to pay for these articles, moreover, the sample code is
provided free of cost unlike Microsoft® where only registered users can access the code

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

related to the newest technologies. So, instead of digging deep into the useless details of
why this article was written, let’s see, what’s so important about the ADO technology.

ActiveX® Data Objects


ADO is the object-based interface that provides a logical set of objects you can access from code.
These objects are:

Object Functionality

Connection Manages the connection with the data source

Command Defines the commands that will be executed against the data source

Recordset Contains the data that is retrieved from the data source

These objects present an interface in the form of properties and methods that can be queried and
manipulated. ADO was specifically developed to be small, lightweight, fast, and feature complete
– everything you need when you are programming either for the database applications or the
Internet.

An important thing in ADO is that the objects in this model are not dependant on one another.
This means that you can create instances of objects independently of one another, for example,
you can create a Recordset object without creating a connection object. Unlike the older
technologies, ADO is more flexible, ADO code is easier to write, read and maintain. ADO is built
on top of OLE DB and is capable of accessing any sort of data that is wrapped and exposed by an
appropriate OLE DB provider.

Connectionless Recordsets
Connectionless recordsets, persistant recordsets, creatable recordsets, call it whatever you want,
all these names refer to the same object and that is ADO Recordset object. The most important
feature provided by the ADO is the introduction of the principle that recordsets are creatable
objects. With ADO you can access any sort of structured data. Recordset is the most used object
in the ADO object library, it is used to temporarily store the set of records (known as recordset)
that is returned by a SQL query. Recordsets have a cursor that indicates the current pointer
position within the recordset. Whenever you employ ADO, you are using the recordsets to carry
data back and forth. Recordsets always contains data, but this data does not necessarily match a
table’s records.

Note that connectionless recordset is not same as the disconnected recordset. Making the
recordset structure externally creatable means that you can create a new recordset object anytime
and anywhere in your code, and you can use it without a connection to a database. A
disconnected recordset supports a static, client-side cursor that automates downloading the
records on the client side. You can have disconnected recordsets with RDO but you can’t have
connectionless recordsets.

A connectionless recordset is a recordset whose fields have been defined on the fly by the
application to match the structure of the information you want it to manage. Previously this
capability was reserved for the data object model such as ADO 1.x, RDO, or DAO.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

For reader’s convenience, we are including here an example that demonstrates the display of data
with a Recordset. There are two ways to do this, one is to create a connection object and then
create a recordset object, and the other way is to create a recordset object without explicitly
creating a connection object. Both ways are demonstrated below:

DISPLAYING DATA WITH A RECORDSET (USING A CONNECTION OBJECT)

Recordset is quite useful in real world applications. If anything, there are too many ways to do the
same thing. The example below uses an explicit Connection object and is written to be used in
Active Server Pages.

Set conn = server.createobject(“ADODB.Connection”)

Set objRec = server.createobject(“ADODB.Recordset”)

Conn.open “DSN=myDB;UID=sa;Password=;”

objRec.ActiceConnection = conn

objRec.open “select * from table1”

while not objRec.EOF

Response.write objRec(“fname”) & “ ”

Response.write objRec(“Address”) & “<br>”

ObjRec.MoveNext

Wend

ObjRec.Close

Conn.Close

DISPLAYING DATA WITH A RECORDSET (WITHOUT A CONNECTION OBJECT)

StrConnect = “DSN=myDB;UID=sa;Password=;”

Set objRec = server.createobject(“ADODB.Recordset”)

objRec.Open “select * from table1”, strConnect, adopenkeyset, adlockoptimistic

while not objRec.EOF

Response.write objRec(“fname”) &

Response.write objRec(“Address”) & “<br>”

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

objRec.MoveNext

wend

objRec.Close

To use the above code in Visual Basic, simply change the syntax of the statement in which the
objects are created like replace “server.createobject” with “createobject”, the above syntax is
specific to the ASP only.

Now that you have seen the examples demonstrating the usage of Recordset objects with the
database, let’s concentrate on the issue of connectionless recordsets or rather should I say
“Creatable Recordsets”. Below is shown the code that creates a brand new recordset that has no
relationship to an OLE DB data source. The code generates a recordset that reads drive
information through the FileSystem Scripting Object. So, you will learn not only how to create a
new recordset (connectionless recordset) but also, how to use the FileSystem Scripting Object.
The code shown below is written in VBScript. The ASP version is also provided with this article.
See the related documents.

CODE

'============================================================

'Name: ConnectionlessRS.vbs

'Description: Shows a connection less recordset with it’s own fields added.

'============================================================

'Constants

Const adUseClient = 3

const adCurrency = 6

const adBSTR = 8

'Local Variables

dim fso, rst, drives

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

'Creates the main objects of the script

set fso = CreateObject("scripting.filesystemobject")

set rst = CreateObject("ADODB.Recordset")

'Gets the collection of available drives

set drives = fso.Drives

'Prepares the Recordset structure: Root, Volume, Type

'FileSystem, FreeSpace

rst.CursorLocation = aduseclient

rst.Fields.Append "Root", adBSTR

rst.Fields.Append "Volume", adBSTR

rst.Fields.Append "Type",adBSTR

rst.Fields.Append "FileSystem",adBSTR

rst.Fields.Append "FreeSpace",adCurrency

rst.Open

'Fills the recordset out with drive information

for each drv in drives

rst.AddNew

if drv.isready then

rst.Fields("Root").value = drv.DriveLetter

rst.Fields("Volume").value = drv.VolumeName

rst.fields("Type").value = drv.DriveType

rst.Fields("FileSystem").value = drv.FileSystem

rst.Fields("FreeSpace").value = drv.FreeSpace/1024

end if

next

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

'Displays the recordset

s = ""

rst.movefirst

while not rst.EOF

for each fld in rst.Fields

s = s & Pad(rst.Fields(fld.name),14) & vbtab

next

s = s & vbcrlf

rst.MoveNext

wend

msgbox s

'============================================================

'Pad(str, numChars)

'Pads the specified string with the specified trailing blanks

'============================================================

Function Pad(str, numChars)

str = str & space(numChars)

Pad= Left(str,numchars)

end function

CODE DETAILS

Two objects are used in this code, one is the FileSystem Scripting Object and the other is the
Recordset object. Drives property of the FileSystem Object is used to get the collection of all the
drives in your computer. The next step is to create new fields of your recordset object. We have
used the client side cursor during the process. Append property of the fields collection is used to
add new fields in the recordset. Once the fields are appended, we scroll through the drives
collection and add new record against each drive, each record contains information about the
individual drive. Lastly, we display the drive information using the msgbox function. Trailing
blanks are added to the strings simply to display the information more clearly on the screen. To

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

display the same information in ASP, we use Response.Write method while navigating through
the recordset. Also, note that to use the above code in ASP, you will have to create the objects
using the “Server.CreateObject()” method.

I've been using ActiveX Data Objects (ADO) since version 1.5--that's a lot of versions
ago. Many changes have taken place in this time. With each version I've learned more
about ADO, stuff that you can't always read in books, or at least not all in one place.
I've put together a handful of these essential tips. Some are issues you should always
keep in mind; some are techniques you might not have known about; and a couple are
just classified as essential knowledge when developing with ADO.

1. Share Connection objects.


When you pass a connection string to a Command, Recordset, or Record object,
you are implicitly instructing ADO to create a Connection object each time:
Dim rec1 As ADODB.Record
Dim rec2 As ADODB.Record
Dim rec3 As ADODB.Record

Set rec1 = New ADODB.Record


rec1.Open "localstart.asp", _
"URL=http://localhost/"

Set rec2 = New ADODB.Record


rec2.Open "global.asa", _
"URL=http://localhost/"

Set rec3 = New ADODB.Record


rec3.Open "iisstart.asp", _
"URL=http://localhost/"

'
' do something here
'

rec1.Close
rec2.Close
rec3.Close

Set rec1 = Nothing


Set rec2 = Nothing
Set rec3 = Nothing
To save resources, you should use one Connection object and pass it to each
object that requires an active connection:
Dim con As ADODB.Connection

Dim rec1 As ADODB.Record


Dim rec2 As ADODB.Record
Dim rec3 As ADODB.Record

Set con = New ADODB.Connection


con.Open "URL=http://localhost/"

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Set rec1 = New ADODB.Record


rec1.Open "localstart.asp", con

Set rec2 = New ADODB.Record


rec2.Open "global.asa", con

Set rec3 = New ADODB.Record


rec3.Open "iisstart.asp", con

'
' do something here
'

rec1.Close
rec2.Close
rec3.Close
con.Close

Set rec1 = Nothing


Set rec2 = Nothing
Set rec3 = Nothing
Set con = Nothing
2. Read the ConnectionString property.
You can always read the ConnectionString property of any Connection object that is
open, including one returned from the Recordset, Command, or Record object’s
ActiveConnection property.
Dim com As ADODB.Command
Dim rst As ADODB.Recordset

Set com = New ADODB.Command

com.ActiveConnection = _
"Provider=Microsoft.Jet.OLEDB.4.0;" _
& "Data Source=NWind.mdb;"

com.CommandText = "SELECT * FROM Customers"

Set rst = com.Execute

MsgBox com.ActiveConnection.ConnectionString

rst.Close

Set rst = Nothing


Set com = Nothing
When the above code is run, you will get the following output displayed in a
message box:
Provider=Microsoft.Jet.OLEDB.4.0;
Password="";
User ID=Admin;
Data Source=NWind.mdb;

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Mode=Share Deny None;


Extended Properties="";
Jet OLEDB:System database="";
Jet OLEDB:Registry Path="";
Jet OLEDB:Database Password="";
Jet OLEDB:Engine Type=4;
Jet OLEDB:Database Locking Mode=0;
Jet OLEDB:Global Partial Bulk Ops=2;
Jet OLEDB:Global Bulk Transactions=1;
Jet OLEDB:New Database Password="";
Jet OLEDB:Create System Database=False;
Jet OLEDB:Encrypt Database=False;
Jet OLEDB:Don't Copy Locale on Compact=False;
Jet OLEDB:Compact Without Replica Repair=False;
Jet OLEDB:SFP=False
You can now parse this string to find out specific information about the
connection, such as if the database will be encrypted when it is compacted (Jet
OLEDB:Encrypt Database property).

3. Use dynamic properties.


The Properties collection of the Connection object can be used to set provider-
specific options, such as the OLE DB driver for SQL Server’s Prompt dynamic
property.
Dim con As ADODB.Connection

Set con = New ADODB.Connection

con.Provider = "SQLOLEDB"
con.Properties("Prompt") = adPromptAlways

con.Open

'
' the user will be prompted for the database
'

con.Close

Set con = Nothing


When this code is run, users will be prompted with a dialog that will allow them
to choose the database to log on to.

4. Choose your cursor location wisely.


When choosing your cursor location, you need to consider what services are
important for your connection.
If services from a data provider are what you seek, you will need to use a server-
side cursor. These are the services offered with the driver for a data source, and
they are usually very flexible. In addition, by keeping a server-side cursor you

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

don't always have to move all of the data to the client, as you would with a client-
side server.
On the other hand, local cursor services, such as the Microsoft Data Shaping
Service for OLE DB, offer services only available if you choose a client-side
cursor. For these services to be manipulated, they require data to reside on the
local machine, as with the Data Shaping Service.

You can use the Connection.CursorLocation property to set the location for your
cursor, but choose wisely.

5. Choose your cursor type wisely.


Equally as important as choosing the location of your cursor is choosing the type
of cursor you want to use.
There are four types of cursors, each with its own set of advantages and
disadvantages.
The Static cursor provides a snapshot of the data at a given moment.
Modifications, including additions and deletions from other users, are not
available in this type of cursor. The Static cursor is good for reporting, where you
need a nonchanging view of data, but it is not necessarily the fastest. Because
changes to data are not shown, a copy of the source at a given moment needs to be
created and maintained by the service provider for each connection using a Static
cursor.
A Forward Only cursor is identical to a Static cursor except you can only move
forward through the data, without moving back. This will improve the
performance, as compared to the Static cursor, but it still requires the data source
to maintain a temporary copy of itself so that changes by other users do not affect
your data.
The Dynamic cursor allows you to see changes and deletions by other users and
you are allowed to move freely throughout the Recordset. Unlike the Static or the
Forward Only cursors, the Dynamic cursor does not require that the data source
maintains a still image of the data, and thus, a Dynamic cursor can be faster than
the first two cursors.
The last cursor type is the Keyset cursor. The Keyset cursor is very similar to the
Dynamic cursor, except you can't see records added by other users. Records
deleted by other users also become inaccessible in a Keyset cursor. As with a
Dynamic cursor, changes by other users are also visible. A Keyset cursor can be
faster than a Dynamic cursor because it doesn't have to constantly look to see if
new records are added or deleted (because added records are not seen and deleted
records become inaccessible).
A cursor type for each season--take your pick.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

6. Create parameters manually.


Microsoft technical writers and anyone else who writes about ADO will beat you
silly with this one, so I don’t want to be any different.
When performance is an issue (and when is it not?), manually define your
parameters.
Dim con As ADODB.Connection
Dim com As ADODB.Command
Dim par As ADODB.Parameter
Dim rst As ADODB.Recordset

Set con = New ADODB.Connection

con.Open "Provider=SQLOLEDB;" _
& "Server=localhost;" _
& "Initial Catalog=Northwind;" _
& "User ID=sa;"

Set com = New ADODB.Command

Set com.ActiveConnection = con

Set par = com.CreateParameter _


("CategoryName", _
adVarWChar, _
adParamInput, _
15)

com.Parameters.Append par

Set par = com.CreateParameter _


("OrdYear", _
adVarWChar, _
adParamInput, _
4)

com.Parameters.Append par

com.CommandText = _
"EXECUTE SalesByCategory 'Produce', '1997'"

Set rst = com.Execute

'
' do something here
'

rst.Close
con.Close

Set com = Nothing


Set rst = Nothing
Set con = Nothing

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

By manually defining your parameters, ADO does not have to query the data
source to find out what the parameter list is for a stored procedure. This may not
matter if you are executing a single, stored procedure while the user is away from
her desk, but it will matter if you are executing dozens at a time, when the user is
waiting for her information.

7. Create buffers with Stream objects.


Stream objects can be used without a physical data source. You can use these
objects to create buffers in memory on the local machine. Simply instantiate the
Stream object and start writing to it.
Dim str As ADODB.Stream

Set str = New ADODB.Stream

str.Open

str.WriteText "This is a message that I "


str.WriteText "would like to keep in "
str.WriteText "memory.", adWriteLine

str.WriteText "This will be the second "


str.WriteText "line.", adWriteLine

MsgBox "The buffer has " & _


str.Size & " characters."

str.Position = 0

MsgBox "The buffer contains: " & str.ReadText

str.Close
You can also use binary data with the Write and Read methods instead of the text
methods WriteText and ReadText. After putting your data into the buffer, you can
use the SaveToFile method to persist the contents.

8. Check for warnings.


The Errors collection of the Connection object is used to not only report data-
provider errors with the execution of an operation, but also to indicate non-halting
warnings from the execution of an operation.

The Connection.Open, Recordset.CancelBatch, Recordset.Resync, and Recordset.UpdateBatch


methods as well as the Recordset.Filter property can all generate warnings.
To detect a data-provider warning (or error), call the Connection.Errors.Clear method
prior to using any of the above methods to begin an operation. After the operation
is complete, use the Count property of the Errors collection to determine if there
were any warnings.

9. Nest transactions.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

With the Jet OLE DB provider, you can nest transactions up to five levels. Using
multiple-level transactions gives you unprecedented control over your data.
Dim con As ADODB.Connection
Dim iLevel As Integer

Set con = New ADODB.Connection

con.CursorLocation = adUseClient

con.Open _
"Provider=Microsoft.Jet.OLEDB.4.0;" _
& "Data Source=NWind.mdb;"

con.BeginTrans

'
' change 1
'

con.BeginTrans

'
' change 2
'

con.BeginTrans

'
' change 3
'
iLevel = con.BeginTrans

'
' change 4
'

MsgBox "Level " & iLevel

con.CommitTrans

con.RollbackTrans

con.CommitTrans

con.CommitTrans

con.Close

Set con = Nothing


In the above example, changes 1 and 2 will be made while 3 and 4 will not.
Change 4 looked like it was going to make it, but the third-level transaction was
rolled back, causing all subsequent levels to be rolled back.

10. Don’t underestimate data shaping.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

My last tip for you is do not underestimate the power of the Microsoft Data
Shaping Service for OLE DB.
Data shaping allows you to create aggregations with multiple SQL statements to
create hierarchical recordsets, where individual fields can point to entire children
recordsets.
For instance, if you took two tables from the Biblio database, Publishers and
Titles, you could create the following SQL statement to join them in one
recordset.
SELECT Publishers.Name, Titles.Title
FROM Publishers
INNER JOIN Titles ON
Publishers.PubID=Titles.PubID
ORDER BY Publishers.Name, Titles.Title;
The first few records look like this:
Name (Pub) Title
-------------- -----------------------------
A K PETERS A Physical Approach to Col...
A K PETERS Colour Principles for C...
A SYSTEM PUBNS C Plus Plus Reference Card
A SYSTEM PUBNS C Reference Card
AA BALKEMA Planning With Linear Progr...
AARP Thesaurus of Aging Termin...
ABACUS Access 2.0 Programming Bible
ABACUS Advanced Access Programming
With data shaping, we can use the following statement to greatly reduce the size
of the returned data.
Three recordsets are created on the server and returned to the client as needed.
Name (Publisher)
------------------------------------
A K PETERS
A SYSTEM PUBNS
AA BALKEMA
AARP
ABACUS

Title
------------------------------------
A Physical Approach to Color...
Colour Principles for Computer...
C Plus Plus Reference Card
C Reference Card
Planning With Linear Programming
Thesaurus of Aging Terminology : ...
Access 2.0 Programming Bible
Advanced Access Programming
The above tables are brought to the client and passed to the data shaping cursor
service, where they are linked in a hierarchical fashion using Chapters as field
types to access child recordsets.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Dim con As ADODB.Connection


Dim rstPubs As ADODB.Recordset
Dim rstTitles As ADODB.Recordset

Dim sShape As String

Set con = New ADODB.Connection


Set rstPubs = New ADODB.Recordset

con.Provider = "MSDataShape"
con.Open _
"Data Provider=Microsoft.Jet.OLEDB.4.0;" _
& "Data Source=Biblio.mdb;"

sShape = "SHAPE {SELECT Name, PubID " _


&" FROM Publishers} " _
& " APPEND ({SELECT Title, PubID " _
&" FROM Titles} " _
&" As PubTitles " _
& " RELATE PubID TO PubID) "

rstPubs.Open sShape, con

Do Until (rstPubs.EOF)
Debug.Print rstPubs!Name
Set rstTitles = rstPubs("PubTitles").Value
Do Until (rstTitles.EOF)
Debug.Print " "_
& rstTitles!Title
rstTitles.MoveNext
Loop
rstPubs.MoveNext
Loop

rstPubs.Close
con.Close

Set rstPubs = Nothing


Set con = Nothing
When the above code is run, we see the following output:
A K PETERS
A Physical Approach to Color...
Colour Principles for Computer...
A SYSTEM PUBNS
C Plus Plus Reference Card
C Reference Card
AA BALKEMA
Planning With Linear Programming
AARP
Thesaurus of Aging Terminology : ...
ABACUS
Access 2.0 Programming Bible
Advanced Access Programming
Powerful stuff.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

VB & VBA Booklet

Booklet TOC
Implementing Custom Events p66-68 Chapter 4
Automation Examples p85-89 Chapter 5
Silent Reporting p106-107 Chapter 6
#Const Directive p113-117 Chapter 7
AddressOf Operator p121-123 Chapter 7
CallByName Function p142-146 Chapter 7
Declare Statement p214-218 Chapter 7
DoEvents Function p241-242 Chapter 7
Err.LastDLLError Property p259-261 Chapter 7
Filter Function p308-310 Chapter 7
GetObject p358-363 Chapter 7
WithEvents Keyword p576-577 Chapter 7

Implementing Custom Events


In the early versions of VB, programmers were limited to working with the built-in
events. In VB5, however, three simple keywords--Event, RaiseEvent, and WithEvents--were
added to the language to allow the programmer to define custom events or to trap events
in external objects that would otherwise be inaccessible.
Custom events applications
Custom events can be used for any of the following:
• To report the progress of an asynchronous task back to the client application from
an out-of-process ActiveX EXE component.
• To pass through events fired by the underlying control in an ActiveX custom
control.
• As a central part of a real-time multiuser application in an n-tier client-server
application. (Incidentally, events can't be fired from within a Microsoft
Transaction Server Context.)
• To receive notification of events fired in automation servers.
• To query the user and receive further input.

Custom event rules


The following are some of the rules and "gotchas" for defining custom events:
• Events can be declared and fired only from within object modules (i.e., Form,
User Control, and Class modules). You can't declare and fire events from a
standard code module.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

• Events can be handled or intercepted only from within object modules. You can't
handle any type of event from within a code module. This isn't really a limitation
because you can simply include a call to a function or sub within a code module
from within your event handler, to pass program control to a code module--just
like you would write code in form and control event handlers.

• The event declaration must be Public so that it's visible outside the object module;
it can't be declared as Friend or Private.
• You can't declare an object variable as WithEvents if the object doesn't have any
events.
• To allow the client application to handle the event being fired, the object variable
must be declared using the WithEvents keyword.
• VB custom events don't return a value; however, you can use a ByRef argument to
return a value, as you will see in the next section, "Creating a custom event."
• If your class is one of many held inside a collection, the event isn't fired to the
"outside world"--unless you have a live object variable referencing the particular
instance of the class raising the event.

Creating a custom event


To raise an event from within an object module, you first of all must declare the event in
the declarations section of the object module that will raise the event. You do this with
the Event statement using the following syntax:
[Public] Event eventname [(arglist)]
For example:
Public Event DetailsChanged(sField As String)
In the appropriate place in your code, you need to fire the event using the RaiseEvent
statement. For example:
RaiseEvent DetailsChanged("Employee Name")
That is all you need to do within the object module. Simply declare an event using Event,
and fire it using RaiseEvent.
The client code is just as simple. You declare an object variable using the WithEvents
keyword to alert VB that you wish to be informed when an event is fired in the object.
For example:
Private WithEvents oEmployee As Employee
This declaration should be placed in the Declarations section of the module. VB
automatically places an entry for the object variable name in the Object drop-down list at
the top left of your code window. When you select this, note that the events declared in
the object are available to you in the Procedure drop-down list at the top right of your
code window. You can then select the relevant event and its event handler. For example:
Private Sub oEmployee_DetailsChanged(sField As String)
MsgBox sField & " has been changed"
End Sub
In the earlier section "The Property Let procedure," we mentioned using a custom event
to fire a warning to the client as part of a data-validation procedure. Unfortunately,
though, events don't return a value. However, if you define one of the parameters of your

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

event to be ByRef, you can examine the value of the variable once the event has been
handled to determine the outcome of the event handling within the client application.
Here's a simple example:
Server code:
Public Event Warning(sMsg As String, ByRef Cancel As Boolean)

Public Property Let ClaimValue(dVal As Double)

Dim blnCancel As Boolean

If dVal > 10000 Then


RaiseEvent Warning("The Claim Value appears high", _
blnCancel)
If blnCancel Then
Exit Property
End If
End If

mdClaimValue = dVal

End Property
Client code:
Private WithEvents oServer As clsServer

Private Sub oServer_Warning(sMsg As String, _


Cancel As Boolean)
Dim iResponse As Integer
iResponse = MsgBox(sMsg & " is this OK?", _
vbQuestion + vbYesNo, _
"Warning")
If iResponse = vbNo Then
Cancel = True
Else
Cancel = False
End If

End Sub
As you can see, this is a powerful technology. However, it also demonstrates another
aspect of custom events that may not be desirable in certain circumstances: RaiseEvent is
not asynchronous. In other words, when you call the RaiseEvent statement in your class
code, your class code won't continue executing until the event has been either handled by
the client or ignored. (If the client has not created an object reference using the WithEvents
keyword, then it isn't handling the events raised by the class, and any events raised will
be ignored by that client.) This can have undesirable side effects, and you should bear it
mind when planning your application.
For more information on the custom event statements, see the entries for the Event, Friend,
Private, Public, RaiseEvent, and WithEvents statements in Chapter 7.

Automation Examples
So let's bring together all you've seen in this chapter with a few sample implementations
of OLE automation servers.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Using Word as a Report Writer from VB


This first application demonstrates how you can seamlessly use Microsoft Word to print
output from your VB program without the user knowing that you have actually used
Microsoft Word:
Private Sub cmdWordDoc_Click()

'create an error handler


On Error GoTo cmdWordDoc_Err

'create the local Early Bound object variables


Dim oWord As Word.Application
Dim oWordActiveDoc As Word.Document
Dim oWordSel As Word.Selection

'Create a new instance of Word


Set oWord = New Word.Application
'Create a new document object
Set oWordActiveDoc = oWord.Documents.Add
Set oWordSel = oWord.Selection

'Do some work with the Selection object


oWordSel.TypeText "This is some text from the VB app."
oWordSel.WholeStory
oWordSel.Font.Name = "Arial"
oWordSel.Font.Size = 12
oWordSel.Font.Bold = wdToggle

'Now print out the doc


oWordActiveDoc.PrintOut

'always tidy up before you leave


Set oWordSel = Nothing
Set oWordActiveDoc = Nothing

Set oWord = Nothing

Exit Sub

cmdWordDoc_Err:
MsgBox Err.Number & vbCrLf & Err.Description & vbCrLf _
& Err.Source

End Sub
Because this example uses early binding, you'll have to use the References dialog to add
a project reference to the Word 8 Object Model.
TIP: Note that this application appears seamless because the application's
Visible property is False by default. If you wanted to show the Word
application window in operation (which may be required while
debugging), simply set the property to True.

Using Email Within VB

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

This application demonstrates how you can work with a late bound object. The OLE
server in this instance is Windows MAPI. Using MAPI in this way uses Outlook sort of
through the back door; you don't actually create an instance of Outlook, but this sample
demonstrates how closely tied MAPI and Outlook are. In fact, the mail side of Outlook
isn't much more than a nice GUI to the Windows MAPI. If you are connected to an
Exchange server when this simple application runs, the mail is sent automatically;
otherwise, the mail is placed in Outlook's outbox, ready for you to send. You may also
have to change the profile name to match that on your own system.
The sample function shown below is called from a form containing a text box (txtDomain)
that holds the domain name of the recipients, and a list box (lstEmails) that holds the
individual addresses of the recipients. This example is in fact part of a working
application used several times a day to send test messages to new email accounts:
Private Function SendReturnEMail() As Boolean

' create an error handler


On Error GoTo SendReturnEMail_Err

'set the default return value


SendReturnEMail = False

'we're using late binding for this app


Dim objSession As Object
Dim objMessage As Object
Dim objRecipient As Object

'declare some other utility variables


Dim i As Integer
Dim sSubject As String
Dim sText As String
Dim sName As String

'set up the email message text


sText = "This is an automatic test message, " & _
vbCrLf & _
"Please reply to the sender confirming receipt."
'and the subject
sSubject = "Test Message"

'start with the top of the mapi hierarchy --


'the session object
Set objSession = CreateObject("mapi.session")
'use the local Outlook default profile
objSession.LogOn profilename:="Microsoft Outlook"

'this application will send a number of test messages


'to the members of a particular domain
For i = 0 To lstEmails.ListCount - 1
'build the addresses from the names in the list
'and the given domain name
sName = Trim(lstEmails.List(i)) & "@" & _
Trim(txtDomain.Text)
'now create a new message object
Set objMessage = objSession.outbox.messages.Add

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

'feed in the required property values for the


'message
objMessage.subject = sSubject
objMessage.Text = sText
'create a new recipient for this message
Set objRecipient = objMessage.Recipients.Add
'and set it's properties
objRecipient.Name = sName
objRecipient.Type = 1
'make sure the email address is resolved
objRecipient.resolve
'now send the message
objMessage.Send showdialog:=False
'tidy up this message
Set objRecipient = Nothing
Set objMessage = Nothing
'and go round again for the next one
Next i
'all done so off we go
objSession.logoff
'tidying up as always
Set objSession = Nothing
'set the success return value
SendReturnEMail = True

Exit Function

SendReturnEMail_Err:
MsgBox Err.Number & vbCrLf & Err.Description & vbCrLf _
& Err.Source

End Function

Output from VB to Excel


To finish with, here's an easy little application that places values from a VB application
into an Excel spreadsheet. There are project-level (early bound) references created to
both Excel and the ADODB 2.0 Reference Library. An ADO recordset has already been
created and is passed as a parameter to the OutputToExcel function. The function creates
an instance of a new Excel workbook and worksheet, then copies the values from the
ADO recordset into the worksheet. Excel's functionality is used to perform a simple
calculation on the data, the worksheet is saved, Excel is closed down, and all references
are tidied up.
This example illustrates the power of a glue language such as Visual Basic. Here VB is
acting as the glue between ADO, which is an ActiveX server, and Excel--controlling both
to produce a simple yet patently powerful and seamless application:
Private Function OutputToExcel(oADORec As ADODB.Recordset) _
As Boolean

On Error GoTo cmdExcel_Err

'set up the default return value


OutputToExcel = False

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

' Declare the Excel object variables


Dim oXLApp As Excel.Application
Dim oXLWBook As Excel.Workbook
Dim oXLWSheet As Excel.Worksheet

'start at the top of the model


Set oXLApp = New Excel.Application
'and work your way down
Set oXLWBook = oXLApp.Workbooks.Add
'until you get to the worksheet
Set oXLWSheet = oXLWBook.Worksheets.Add

oXLWSheet.Cells(1, 1).Value = oADORec!FirstValue


oXLWSheet.Cells(2, 1).Value = oADORec!SecondValue

' do some stuff in Excel with the values


oXLWSheet.Cells(3, 1).Formula = "=R1C1 + R2C1"

' save your work


oXLWSheet.SaveAs "vb2XL.xls"

'quit Excel
oXLApp.Quit

' always remember to tidy up before you leave


Set oXLWSheet = Nothing
Set oXLWBook = Nothing
Set oXLApp = Nothing

OutputToExcel = True

Exit Function

cmdExcel_Err:
MsgBox Err.Description & vbCrLf & Err.Number & _
vbCrLf & Err.Source

End Function

Silent Reporting: Logging the Error Event


Your efforts to resolve issues within an application are often frustrated by users not
reporting errors. The user simply clicks past the message box reporting the error and
continues. Either they forget or can't be bothered to contact the MIS department or the
software developer to report the issue. There is a way you can store information about the
error on the user's machine without having to go to the trouble of coding a file
open/write/close routine that itself could cause a fatal error within the error handler.
The App object includes a method called LogEvent whose operation depends on the
operating system being used. On NT the LogEvent method writes to the machine's event
log, whereas in Windows 9x a log file is created or an existing log file appended to.
Logging only takes place in compiled VB applications.
You can specify an event log file using the StartLogging method, which takes two
parameters, the log filename and the log mode. (The App object's LogPath and LogMode

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

properties, which you would expect to set before beginning logging, are read-only and
can only be set by calling the StartLogging method.)
WARNING: Note that the log mode constants were missing from Version
5 of VB, so you either have to enter their literal values, or you have to
define your own constants.
In Windows NT, if you call the StartLogging method but don't specify a log file, or in
Windows 95, if you don't call the StartLogging method at all, VB creates a file called
vbevents.log, which is placed in the Windows directory. To use event logging, you don't
necessarily need to use the StartLogging method.
The LogEvent method itself takes two parameters. The first is a string containing all the
detail you wish to store about the error or event. The second is an EventType constant,
which denotes an error, information, or a warning. In NT, this event type value displays
the correct icon in the event log, whereas in Windows 95, the word "Error,"
"Information," or "Warning" appears at the start of the item in the event log file.
TIP: In a previous section, "Error Handling in ActiveX Servers," you saw
how to force MsgBox prompts to be automatically written to an event log
by selecting the Unattended Application option. But which event log? The
MsgBox function doesn't take a parameter to specify an optional event log,
so VB will write the string contained within the Prompt parameter to the
default vbevents.log in Windows 9x or to the application event log in
Windows NT. However, you can place a call to the app object's
StartLogging method in the class's Initialize event, thereby specifying a
log file for all Msgbox and LogEvent calls.
Once you have an event log for your application, you can look back through the history
of the application any time you choose. If you are networked to the user's machine, you
can open the user's event log from your machine and detect problems without even
leaving your desk.

#Const Directive
Named Arguments
No
Syntax
#Const constantname = expression
constantname
Use: Required
Data Type: Variant (String)
Name of the constant.
expression
Use: Required
Data Type: Literal
Any combination of literal values, other conditional compilation constants defined
with the #Const directive, and arithmetic or logical operators except Is.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Description
Defines a conditional compiler constant. By using compiler constants to create code
blocks that are included in the compiled application only when a particular condition is
met, you can create more than one version of the application using the same source code.
This is a two-step process:
• Defining the conditional compiler constant. This step is optional; conditional
compiler constants that aren't explicitly defined by the #Const directive but that are
referenced in code default to a value of 0 or False.
• Evaluating the constant in the conditional compiler #If...Then statement block.
A conditional compiler constant can be assigned any string, numeric, or logical value
returned by an expression. However, the expression itself can consist only of literals,
operators other than Is, and another conditional compiler constant.
When the constant is evaluated, the code within the conditional compiler #If...Then block
is compiled as part of the application only when the conditional compiler constant
evaluates to True.
You may wonder why you should bother having code that is compiled only when a
certain condition is met, when a simple If...Then statement could do the same job. The
reasons are:
• You may have code that contains early bound references to objects that are
present only in a particular version of the application. You'd want that code
compiled only when you know it wouldn't create an error.

• You may wish to include code that executes only during the debugging phase of
the application. It's often wise to leave this code in the application even after the
application has been released, so that you can check back over a procedure if an
issue arises. However, you don't want the code to be executed in the final
application. The answer is to wrap your debugging code in a conditional
statement. You can then provide a conditional constant that acts as a switch to
turn debugging code on or off, as the example below demonstrates.
• Although most operations performed with conditional compilation can be
replicated with normal If...Then code blocks, conditional compilation reduces the
size of the compiled application and thereby the amount of memory required for
the application, making for a more efficient application.

Rules at a Glance
• Conditional compiler constants are evaluated by the conditional compiler #If...Then
statement block.
• You can use any arithmetic or logical operator in the expression except Is.
• You can't use other constants defined with the standard Const statement in the
expression.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

• According to the documentation, you can't use intrinsic functions in the


expression; e.g., #Const MY_CONST = Chr(13) is illegal. In most cases, VBA
displays a "Compile error : Variable not found" message if you try this. But there
are numerous exceptions. For example, the use of the Int function in the following
code fragment doesn't produce a compiler error, and in fact, successfully defines a
constant ccDefInt whose value is 3:
• #Const ccDefFloat = 3.1417
• #Const ccDefInt = Int(ccDefFloat)
• When using #Const, you can't use variables to assign the conditional constant a
value.
• Constants defined with #Const can be used only in conditional code blocks.

• Constants defined with #Const have scope only within the module in which they
are defined; i.e., they are private.
• You can place the #Const directive anywhere within a module.
• You can't use the #Const directive to define the same constant more than once
within a module. Attempting to do so produces a "Compile Error: Duplicate
Definition" error message.
• Interestingly, you can define the same constant both through the VB or VBA
interface (see the second item in the "Programming Tips & Gotchas" section) and
using the #Const directive. In this case, the constant defined through the interface
is visible throughout the application, except in the routine in which the #Const
directive is used, where the private constant is visible.
• The #Const directive must be the first statement on a line of code. It can be
followed only by a comment. Note that the colon, which combines two complete
sets of statements onto a single line, can't be used on lines that contain #Const.

Example
#Const ccDebug = 1 'evaluates to true

Function testValue(sValue as String)

sValue = UCase(sValue)
testValue = sValue

#If ccDebug Then


'this code only executes if ccDebug evaluates to true
Debug.Print sValue
#End If

End Function

Programming Tips & Gotchas

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

• Conditional compiler constants help you debug your code, as well as provide a
way to create more than one version of your application. You can include code
that operates only when run in debug mode. The code can be left in your final
version and won't compile unless running in the debugger. Therefore, you don't
need to keep adding and removing debugging code.

• You can also define conditional constants outside of the application's code. In the
VBA Editor, enter the conditional compiler constant into the Conditional
Compilation Arguments text box on the General tab of the Project Properties
dialog. You can reach it by selecting the Project Properties option (where Project
is the name that you've assigned to the project) from the Tools menu. In Visual
Basic, the Conditional Compilation Arguments text box is found on the Make
property sheet of the Project Properties dialog. It can be accessed by selecting the
Project Properties option (again, where Project is the name that you've assigned
to the project) from the Project menu. In Access, the Conditional Compilation
Arguments text box is found on the Advanced property sheet of the Options
dialog, which can be accessed by selecting the Options item from the Tools menu.
Conditional compiler constants defined in this way are public to the project.

Constants Defined Through the VB/VBA Interface


The rules for defining constants in the Conditional Compilation Arguments text box are
somewhat different than for constants defined in code using the #Const statement. The
value assigned through the VB/VBA interface must be an integer literal; it can't be an
expression formed by using multiple literals or conditional constants, along with one or
more operators, nor can it be a Boolean value (i.e., True or False). If multiple conditional
constants are assigned through the user interface, they are separated from one another by a
colon. For instance, the following fragment defines three constants, ccFlag1, ccFlag2, and
ccFlag3:
ccFlag1 = 1 : ccFlag2 = 0 : ccFlag3 = 1

• In many cases, failing to properly define a constant doesn't produce an error


message. When this happens (as it does, for instance, when you attempt to assign
a variable's value to a constant), the default value of the constant is False. As a
result, attempting to assign the value resulting from an invalid expression to a
constant can lead to the inclusion of the wrong block of code in the compiled
application.
• Although it may be obvious, it's important to remember that the constant defined
by #Const is evaluated at compile time, and therefore doesn't return information
about the system on which the application is running. For example, the intent of
the following code fragment is to test for a sound card and, if one is present, to
include code taking advantage of the system's enhanced sound capabilities:
If waveOutGetNumDevs > 0 Then
#Const ccSoundEnabled = True
Endif
...
#If ccSoundEnabled Then
' Include code for sound-enabled systems

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

#Else
' Include code for systems without a sound card
#End If
However, the code doesn't work as expected, since it includes or excludes the code
supporting a sound card based on the state of the development machine, rather than the
machine on which the application is running.
See Also
#If...Then...#Else Directive

AddressOf Operator
Named Arguments
No
Syntax
AddressOf procedurename
procedurename
Use: Required
The name of an API procedure.
Description
Passes the address of a procedure to an API function. There are some API functions that
require the address of a callback function as a parameter. (A callback function is a routine
in your code that is invoked by the routine that your program is calling: it calls back into
your code.) These callback functions are passed to the API function as pointers to a
memory address. In the past, calling functions that required callbacks posed a unique
problem to VB, since, unlike C or C++, it lacks a concept of pointers. However, the
AddressOf operator allows you to pass such a pointer in the form of a long integer to the
API function, thereby allowing the API function to call back to the procedure.
Rules at a Glance
• The callback function must be stored in a code module; attempting to store it in a
class or a form module generates a compile-time error, "Invalid use of AddressOf
operator."
• The AddressOf operator must be followed by the name of a user-defined function,
procedure, or property.
• The data type of the corresponding argument in the API function's Declare
statement must be As Any or As Long.
• The AddressOf operator can't call one VB procedure from another.

Example
The following example uses the EnumWindows and GetWindowText API calls to return a
list of currently open windows. EnumWindows requires the address of a callback function

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

as its first parameter. A custom function, EnumCallBackProc, is the callback function


that populates the lstWindowTitles list box.
When the cmdListWindows command button is clicked, the list box is cleared, and a call
to the EnumWindows API function is made, passing the AddressOf the EnumCallBackProc
function and a reference to the list box control. EnumWindows then calls back to
EnumCallBackProc, passing it the window handle of an open window and the reference
to the list box. EnumCallBackProc then uses the GetWindowText API function to return
the text in the titlebar of the window, passing it the window handle, a string buffer, and
the length of that buffer. EnumCallBackProc is called by the API function as many times
as is required, depending upon the number of open windows. The first portion of the
example code must be stored in a code module, while the cmdListWindows_Click event
handler can be stored in the form module containing the cmdListWindows button.
Option Explicit

Public Declare Function EnumWindows Lib "User32" _


(ByVal lpEnumFunc As Any, _
ByVal lParam As Any) As Long

Public Declare Function GetWindowText Lib "User32" _


Alias "GetWindowTextA" _
(ByVal hWnd As Long, _
ByVal lpString As String, _
ByVal cch As Long) As Long

Function EnumCallBackProc(ByVal hWnd As Long, _


ByVal lParam As ListBox) As Long

On Error Resume Next

Dim sWindowTitle As String


Dim lReturn As Long

sWindowTitle = String(512, 0)

lReturn = GetWindowText(hWnd, sWindowTitle, 512)

If lReturn > 0 Then


lParam.AddItem sWindowTitle
End If

EnumCallBackProc = 1

End Function

Private Sub cmdListWindows_Click()

Dim lReturn As Long

lstWindowTitles.Clear
lReturn = EnumWindows(AddressOf EnumCallBackProc, _
lstWindowTitles)

End Sub

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Programming Tips & Gotchas


• Debugging calls containing AddressOf is at best very difficult and most of the time
downright impossible.

• It's possible to pass an AddressOf pointer from one VB procedure to another by


creating a wrapper for the callback function. To do this, however, you must
declare the pointer as either Long or Any. The following snippet shows how you
could add such a wrapper function to the example used above:
Private Sub cmdListWindows_Click()

Dim lReturn As Long

lReturn = DoWindowTitles(AddressOf EnumCallBackProc, _


lstWindowTitles)
End Sub

Private Function DoWindowTitles(CallBackAddr As Long, _


lstBox As ListBox) As Long

'other stuff here


lstBox.Clear
DoWindowTitles = EnumWindows(CallBackAddr, lstBox)

End Function
• Because you can't pass an error back to the calling Windows API function from
within your VB callback function, you should use the On Error Resume Next
statement at the start of your VB callback function.

See Also
Declare Statement

CallByName Function (VB6)


Named Arguments
No
Syntax
CallByName(object, procedurename, calltype, _
[argument1,..., argumentn])
object
Use: Required
Data Type: Object
A reference to the object containing the procedure being called.
procedurename
Use: Required
Data Type: String
The name of the procedure to call.
calltype

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Use: Required
Data Type: vbCallType constant
A constant that indicates the type of procedure being called. vbCallType constants
are listed in the next table.
arguments
Use: Optional
Data Type: Variant
Any number of variant arguments, depending on the argument list of the
procedure to call.
Constant Value Description

vbGet 2 The called procedure is a Property Get

vbLet 4 The called procedure is a Property Let


The called procedure is a method; this can be a Sub or a
vbMethod 1
Function within object
vbSet 8 The called procedure is a Property Set

Return Value
Depends on the return value (if any) of the called procedure.
Description
Provides a flexible method for calling a public procedure in a VB object module. Since
procedurename is a string expression, rather than the hard-coded name of a routine, it's
possible to call routines dynamically at runtime with a minimum of coding.
Rules at a Glance
• The return type of CallByName is the return type of the called procedure.
• procedurename isn't case sensitive.

Programming Tips & Gotchas


• At last, VB allows you to create a call to a procedure using a string. This means
that the call can be flexible at runtime.
• The only drawback to the current implementation of CallByName is that the
parameters to pass to the called function must be entered individually. This means
that, when coding the CallByName function, you need to know in advance how
many parameters are needed. You could work around this by coding your
functions to accept only Variant arrays so that you only need to pass a single
parameter.
• Late binding is necessarily used to instantiate objects whose procedures are
invoked by the CallByName function. Consequently, the performance of
CallByName is inferior to that of method invocations in early bound objects. This

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

degradation of performance is especially acute if CallByName is invoked


repeatedly inside a looping structure.

Example
The following example takes CallByName and the amendments to CreateObject to their
logical conclusion: a variable procedure call to a variable ActiveX server in a variable
location. In this example, the SQL Server pubs database is used as the source of the data.
Two ActiveX objects on two separate machines are used to create two different
recordsets: one from the Authors table, the other from the Titles table. However, nowhere
in the program are the names of the ActiveX DLLs, the procedures, or the remote servers
mentioned.
The middle tier of this application uses the registry to store these names, allowing fast
alteration of the application without touching a single line of code or creating
incompatibilities between components. The repercussions of this approach to enterprise-
wide programming are wide-reaching, and the prospects very exciting.
Only when dealing with the user interface of the client component are the names of the
required datasets and fields specified. The Form_Load event calls a standard function to
populate combo box controls with the required data:
Private Sub Form_Load()

PopulateCombo cboAuthors, "Authors", "au_lname"


PopulateCombo cboTitles, "Titles", "title"

End Sub
The PopulateCombo function calls a GetRecordset function in the first middle tier of the
model, passing in the recordset name required (either Authors or Titles in this case) and a
search criteria string that is concatenated into the embedded SQL script to refine the
recordset. GetRecordset returns an ADO recordset that populates the desired combo box:
Private Function PopulateCombo(oCombo As ComboBox, _
sRecords As String, _
sField As String) As Boolean

Dim adorRecords As ADODB.Recordset


Dim sSearch As String

If sRecords = "Authors" Then


sSearch = "contract = 1 AND state = 'CA'"
Else
sSearch = ""
End If

Set adorRecords = oAdmin.GetRecordset(sRecords, sSearch)

Do While Not adorRecords.EOF


oCombo.AddItem adorRecords(sField)
adorRecords.MoveNext
Loop

adorRecords.Close
Set adorRecords = Nothing

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

End Function
The GetRecordset method that sits on a central machine interrogates the registry (using
the GetSetting function) to determine the names of the ActiveX server, the machine, and
the procedure to call. I've also coded an alternative method of obtaining these names
using a Select Case statement (which is commented out in the code sample). Finally, the
CreateObject function obtains a reference to the appropriate ActiveX server on the
appropriate machine and a call is made to the function in that server to obtain the correct
recordset:
Public Function GetRecordset(sRecords As String, _
sCriteria As String _
) As ADODB.Recordset

Dim sServer As String


Dim sLocation As String
Dim sMethod As String

Dim oServer As Object

sServer = GetSetting(App.Title, sRecords, "Server")


sLocation = GetSetting(App.Title, sRecords, "Location")
sMethod = GetSetting(App.Title, sRecords, "GetMethod")

' An alternative method of obtaining the names of the


' elements of the remote procedure call is to hard-code
' them into the application as follows:
' Select Case sRecords
' Case Is = "Titles"
' sServer = "TestDLL.Titles"
' sLocation = "NTSERV1"
' sMethod = "GetTitles"
' Case Is = "Authors"
' sServer = "Test2DLL.Authors"
' sLocation = "NTWS2"
' sMethod = "getAuthors"
' Case Else
' Set GetRecordset = Nothing
' Exit Function
' End Select

Set oServer = CreateObject(sServer, sLocation)

Set GetRecordset = CallByName(oServer, _


sMethod, _
VbMethod, _
sCriteria)

End Function
The code to create the recordsets in TestDLL.Titles and Test2DLL.Authors isn't shown here, as
it's straightforward database access code.
Now, imagine for a moment that the organization using this application wanted a minor
alteration in the way the Authors recordset was presented to the client (a different sort
order, for example). You can now make a change to the procedure, calling it
getAuthorsRev ; compile a completely new ActiveX server; and place it on the remote

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

server. Then with two quick edits of the registry, all the clients in the organization would
instantly access the new procedure with a minimum of fuss, no loss of component
compatibility, zero downtime, and an almost seamless transition.
See Also
Call Statement

Declare Statement
Named Arguments
No
Syntax
Syntax for subroutines
[Public | Private] Declare Sub name Lib "libname" _
[Alias "aliasname"] [([arglist])]
Syntax for functions
[Public | Private] Declare Function name Lib "libname"
[Alias "aliasname"] [([arglist])] [As type]
Public
Use: Optional
Keyword used to declare a procedure that has scope in all procedures in all
modules in the application.
Private
Use: Optional
Keyword used to declare a procedure that has scope only within the module in
which it's declared.
Sub
Use: Optional
Keyword indicating that the procedure doesn't return a value. Mutually exclusive
with Function.
Function
Use: Optional
Indicates that the procedure returns a value. Mutually exclusive with Sub.
name
Use: Required
Data Type: String
Any valid procedure name within the DLL or code library. If the aliasname
argument is used, name represents the name the function or procedure is called in
your code, while aliasname represents the name of the routine as found in the
external library.
Lib
Use: Required
Keyword indicating that the procedure is contained within a DLL or other code
library.
libname
Use: Required
Data Type: String

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

The name of the DLL or other code library that contains the declared procedure.
Alias
Use: Optional
Keyword whose presence indicates that name is different from the procedure's
real name within the DLL or other code library.
aliasname
Use: Optional
Data Type: String
The real name of the procedure within the DLL or code library.
arglist
Use: Optional
Data Type: Any
A list of variables representing the arguments that are passed to the procedure
when it's called. (For details of the arglist syntax and elements, see the entries for
the Sub statement or Function statement.)
type
Use: Optional
Data type of the value returned by a function. (For further details see the Function
statement entry.)
Description
Used at module level to declare references to external procedures in a dynamic-link
library (DLL).
Rules at a Glance
• You can place a Declare statement within a code module, in which case it can be
public or private, or within the declarations section of a form or class module, in
which case it must be private.
• Leaving the parentheses empty and not supplying an arglist indicates that the Sub
or Function procedure has no arguments.
• The number and type of arguments included in arglist are checked each time the
procedure is called.
• The data type you use in the As clause following arglist must match that returned
by the function.

Example
Option Explicit

Declare Function GetVersion Lib "kernel32"() As Long

Public Function WhereAmI() As Boolean

Dim lWinVersion As Long


Dim lWinMajVer As Long
Dim lWinMinVer As Long
Dim sSys As String

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

lWinVersion = GetVersion()

lWinMajVer = lWinVersion And 255


lWinMinVer = (lWinVersion And 65280) / 256

If lWinVersion And &H80000000 Then


sSys = "Windows 95"
Else
sSys = "Windows NT"
End If

Msgbox "Platform: " & sSys & vbCrLf & _


"Version: " & lWinMajVer & "." & lWinMinVer

Programming Tips & Gotchas


• If you don't specify a Public or Private keyword, the visibility of the external
procedure is public by default. However, if the routine is declared in the
declarations section of a form or a class module, a compiler error ("Constants,
fixed length strings, arrays, and Declare statements not allowed as Public
members of object modules") results.
• Using an alias is useful when the name of an external procedure would conflict
with a Visual Basic keyword or with the name of a procedure within your project,
or when the name of the procedure in the code library isn't allowed by the Visual
Basic DLL naming convention. In addition, aliasname is frequently used in the
case of functions in the Win32 API that have string parameters, where the
"official" documented name of the function is used in code to call either of two
"real" functions, one an ANSI and the other a Unicode version. For example:
• Declare Function ExpandEnvironmentStrings _
• Lib "kernel32" Alias "ExpandEnvironmentStringsA" _
• (ByVal lpSrc As String, ByVal lpDst As String, _
ByVal nSize As Long) As Long
defines the documented Win32 function ExpandEnvironmentStrings to a VB
application. However, although calls to the function take the form:
lngBytes = ExpandEnvironmentStrings(strOriginal, _
strCopy, len(strCopy)
the actual name of the function as it exists in Kernel32.dll is
ExpandEnvironmentStringsA. (Windows API functions ending in A are the ANSI
string versions, and those ending in W (for W ide) are the Unicode string
versions.)
• You can use the # symbol at the beginning of aliasname to denote that aliasname
is in fact the ordinal number of a procedure within the DLL or code library. In this
case, all characters following the # sign that compose the aliasname argument
must be numeric. For example:
• Declare Function GetForegroundWindow Lib "user32" _
Alias "#237" () As Long
• Remember that DLL entry points are case sensitive. In other words, either name
or, if it's present and doesn't represent a routine's ordinal position, aliasname must

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

correspond in case exactly to the routine as it's defined in the external DLL.
Otherwise, VB displays runtime error 453, "Specified DLL function not found." If
you aren't sure how the routine name appears in the DLL, use QuickView to
browse the DLL and scan for its export table.
• libname can include an optional path that identifies precisely where the external
library is located. If the path isn't included along with the library name, VB by
default searches the current directory, the Windows directory, the Windows
system directory, and the directories in the path, in that order.
• If the external library is one of the major Windows system DLLs (like
Kernel32.dll or Advapi32.dll ), libname can consist of only the root filename,
rather than the complete filename and extension.
• In some cases, a single parameter to an API function can accept one of several
data types as arguments. This is particularly common when a function accepts a
pointer to a string buffer if an argument is to be supplied and a null pointer if it
doesn't; the former is expressed in Visual Basic by a string argument and the latter
by a 0 passed to the function by value. It's also the case whenever an API function
designates a parameter's data type as LPVOID, which indicates a pointer to any
data type. To handle this, you can define separate versions of the DECLARE
statement, one for each data type to be passed to the function. (In this case, name
designates the name by which a particular API function is referenced in your
program, while the ALIAS clause designates the name of the routine as it exists in
the DLL.) A second alternative, rather than having to "strongly type" a parameter
in arglist, is to designate its data type as As Any, indicating that the routine accepts
an argument of any data type. While this provides you with a flexible way of
partly overcoming the mismatch between VB and C data types, you should use it
with caution, since it suspends Visual Basic's normal type checking for that
argument.
• Windows NT was built from the ground up using Unicode (two-byte) strings;
however, it also supports ANSI strings. OLE 2.0 was built to use Unicode strings
exclusively. Visual Basic from Version 4 onwards uses Unicode strings internally,
but passes ANSI strings into your program. What does all this mean for you?
Well, Windows NT and OLE 2.0 API calls that have string parameters require
them to be passed as Unicode strings. Unfortunately, although Visual Basic uses
Unicode strings internally, it converts strings passed to these DLLs back into
ANSI. The remedy is to use a dynamic array of type Byte. Passing and receiving
arrays of bytes circumvents Visual Basic's Unicode-ANSI conversion.

To pass a string to a Unicode API function, declare a dynamic byte array, assign
your string to the array, and concatenate a terminating null character (vbNullChar)
to the end of the string, then pass the first byte of the array (at element 0) to the
function, as the following simple snippet shows:
Dim bArray() As Byte
bArray() = "My String" & vbNullChar
someApiCall(bArray(0))

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

• One of the most common uses of the Declare statement is to make routines in the
Win32 API accessible to your programs. For more information on calling the
Win32 API from Visual Basic, see Dan Appleman's The Visual Basic
Programmer's Guide to the Win32 API, published by Ziff-Davis Press.

See Also
Sub Statement, Function Statement, StrConv Function

DoEvents Function
Named Arguments
No
Syntax
DoEvents()
Return Value
In VBA, DoEvents returns 0; in the retail version of VB, it returns the number of open
forms.
Description
Allows the operating system to process events and messages waiting in the message
queue. For example, you can allow a user to click a Cancel button while a processor-
intensive operation is executing. In this scenario, without DoEvents, the click event
wouldn't be processed until after the operation had completed; with DoEvents, the Cancel
button's Click event can be fired and its event handler executed even though the
processor-intensive operation is still executing.
Rules at a Glance
Control is returned automatically to your program or the procedure that called DoEvents
once the operating system has processed the message queue.
Example
The following example uses a UserForm with two command buttons to illustrate how
DoEvents interrupts a running process:
Option Explicit
Private lngCtr As Long
Private blnFlag As Boolean

Private Sub CommandButton1_Click()

blnFlag = True

Do While blnFlag
lngCtr = lngCtr + 1
DoEvents
Loop
MsgBox "Loop interrupted after " & lngCtr & _
" iterations."

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

End Sub

Private Sub CommandButton2_Click()

blnFlag = False

End Sub

Programming Tips & Gotchas


• You may consider using the retail version of VB to create standalone ActiveX
EXEs that handle very intensive or long processes. These can then be called from
your VBA code. This allows you to pass the responsibility of time slicing and
multitasking to the operating system.
• Make sure that during the time you have passed control to the operating system
with DoEvents, the procedure calling DoEvents isn't called from another part of
the application or from another application, since the return from DoEvents may
be compromised. For the same reason, you must not use the DoEvents function
within VB in-process ActiveX DLLs.
• While DoEvents can be essential for increasing the responsiveness of your
program, it should at the same time be used judiciously, since it entails an
enormous performance penalty. For example, the following table compares the
number of seconds required for a simple For...Next loop to iterate one million times
when DoEvents isn't called, on the one hand, and when it's called on each iteration
of the loop, on the other:

without DoEvents 0.3 seconds

with DoEvents 49.8 seconds

• If most of a procedure's processing occurs inside a loop, one way of avoiding far-
too-frequent calls to DoEvents is to call it conditionally every hundred or
thousand iterations of the loop. For example:
• Dim lngCtr As Long
• For lngCtr = 0 To 1000000
• If lngCtr / 1000 = Int(lngCtr / 1000) Then
• DoEvents
• End If
• Next

• Err.LastDLLError Property
• Syntax
• Err.LastDLLError
• Description
• A read-only property containing a long data type representing a system error
produced within a DLL called from within a VB program.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

• Rules at a Glance
o Only direct calls to a Windows system DLL from VB code assign a value
to LastDLLError.

o The value of the LastDLLError property depends upon the particular DLL
being called. Your code must be able to handle the various codes that can
be returned by the DLL you are calling.
o Don't forget that a failed DLL call doesn't itself raise an error within your
VB program. As a result, the Err object's Number, Description, and Source
properties aren't filled.

Programming Tips & Gotchas


o The LastDLLError property is used only by system DLLs, such as
kernel32.dll, and therefore errors that occur within DLLs you may have
created in VB won't be assigned.
o Obtaining accurate documentation about the return values of system DLLs
can be a challenging experience. Most useful information can be found by
studying the API documentation for Visual C++. However, you can use
the Windows API FormatMessage to return the actual Windows error
message string from within Kernel32.DLL, which incidentally is also in
the correct language. The following is a brief example you can use in your
applications to display the actual Windows error description:
Option Explicit
Declare Function FormatMessage Lib "kernel32" _
Alias "FormatMessageA" _
(ByVal dwFlags As Long, lpSource As Any, _
ByVal dwMessageId As Long, _
ByVal dwLanguageId As Long, _
ByVal lpBuffer As String, ByVal nSize As Long, _
Arguments As Long) As Long
Public Const FORMAT_MESSAGE_FROM_SYSTEM = &H1000
Public Const FORMAT_MESSAGE_IGNORE_INSERTS = &H200

Function apiErrDesc(lErrCode As Long) As String

Dim sErrDesc As String


Dim lReturnLen As Long
Dim lpNotUsed As Long

sErrDesc = String(256, 0)
lReturnLen = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM _
Or FORMAT_MESSAGE_IGNORE_INSERTS, _
lpNotUsed, lErrCode, 0&, sErrDesc, _
Len(sErrDesc), ByVal lpNotUsed)

If lReturnLen > 0 Then


apiErrDesc = Left$(sErrDesc, lReturnLen)
End If

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

End Function
Here's a snippet demonstrating how you can use this utility
function:
lReturn = SomeAPICall(someparams)
If lReturn <> 0 then
Err.Raise Err.LastDLLError & vbObjectError, _
"MyApp:Kernel32.DLL", _
apiErrDesc(Err.LastDLLError)
End If
Note that some API calls return 0 to denote a successful function
call; others return 0 to denote an unsuccessful call. You should also
note that some API functions don't appear to set the LastDLLError
property. In most cases, these are functions that return an error
code. You could therefore modify the snippet above to handle
these cases:
lReturn = SomeAPICall(someparams)
If lReturn <> 0 then
If Err.LastDLLError <> 0 Then
Err.Raise Err.LastDLLError & vbObjectError, _
"MyApp:Kernel32.DLL", _
apiErrDesc(Err.LastDLLError)
Else
Err.Raise lReturn & vbObjectError, _
"MyApp:Kernel32.DLL", _
apiErrDesc(lReturn)
End If
End If

See Also
Err Object, Chapter 6

Filter Function (VB6)


Named Arguments
No
Syntax
Filter(SourceArray, FilterString[, Switch[, Compare]])
SourceArray
Use: Required
Data Type: String or Variant
An array containing values to be filtered.
FilterString
Use: Required
Data Type: String
The string of characters to find in the source array.
Switch
Use: Optional
Data Type: Boolean

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

A Boolean (True or False) value. If True, the default value, Filter includes all
matching values in result; if False, Filter excludes all matching values (or, to put it
another way, includes all nonmatching values).
Compare
Use: Optional
Type: Constant of vbCompareMethod Enumeration
An optional constant (possible values are vbBinaryCompare, vbTextCompare,
vbDatabaseCompare) that indicates the type of string comparison to use. The default
value is vbBinaryCompare.
Return Value
A String array of the elements filtered from SourceArray.
Description
Produces an array of matching values from an array of source values that either
match or don't match a given filter string. In other words, individual elements are
copied from a source array to a target array if they either match or don't match a
filter string.
Rules at a Glance
o The default Switch value is True.
o The default Compare value is vbBinaryCompare.
o vbBinaryCompare is case sensitive; that is, Filter matches both character and
case. In contrast, vbTextCompare is case insensitive, matching only character
regardless of case.
o The returned array is always base 0, regardless of any Option Base setting.

Programming Tips & Gotchas


o The Filter function ignores zero-length strings ("") if SourceArray is a
string array and ignores empty elements if SourceArray is a variant array.
o The array you declare to assign the return value of Filter must be a
dynamic, single-dimension String array or a variant.

o Although the Filter function is primarily a string function, you can also
filter numeric values. To do this, specify a SourceArray of type Variant
and populate this array with numeric values. Although FilterString
appears to be declared internally as a string parameter, a String, Variant,
Long, or Integer can be passed to the function. Note, though, that the
returned string contains string representations of the filtered numbers. For
example:
o Dim varSource As Variant, varResult As Variant
o Dim strMatch As String
o
o strMatch = CStr(2)

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

o varSource = Array(10, 20, 30, 21, 22, 32)


o varResult = Filter(varSource, strMatch, True, _
vbBinaryCompare)
In this case, the resulting array contains four elements: 20, 21, 22, and 32.
o The Filter function is an ideal companion to the Dictionary object. The
Dictionary object is a collection-like array of values, each of which is
stored with a unique string key. The Keys method of the Dictionary object
allows you to produce an array of these Key values, which you can then
pass into the Filter function as a rapid method of filtering the members of
your Dictionary, as the following example demonstrates.

Example
Dim sKeys() As String
Dim sFiltered() As String
Dim sMatch As String
Dim blnSwitch As Boolean
Dim oDict As Dictionary

Set oDict = New Dictionary

oDict.Add "One Microsoft Way", "Microsoft"


oDict.Add "31 Harbour Drive", "AnyMicro Inc"
oDict.Add "The Plaza", "Landbor Data"
oDict.Add "999 Pleasant View", "Micron Co."

sKeys = oDict.Keys
sMatch = "micro"
blnSwitch = True
'find all keys that contain the string "micro" - any case
sFiltered() = Filter(sKeys, sMatch, blnSwitch, _
vbTextCompare)
'now iterate through the resulting array
For i = 0 To UBound(sFiltered)
Set oSupplier = oDict.Item(sFiltered(i))
With oSupplier
Debug.Print oSupplier.Address1
End With
Set oSupplier = Nothing
Next i

GetObject Function
Named Arguments
Yes
Syntax
GetObject([pathname] [, class])
pathname
Use: Optional
Data Type: Variant (String)
The full path and name of the file containing the ActiveX object.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

class
Use: Optional
Data Type: Variant (String)
The class of the object (see next list).
The class argument has these parts:
Appname
Use: Required
Data Type: Variant (String)
The name of the application.
Objecttype
Use: Required
Data Type: Variant (String)
The class of object to create, delimited from Appname by using a point (.). For
example, Appname.Objecttype.
Return Value
Returns a reference to an ActiveX object.
Description
Accesses an ActiveX server held within a specified file.
Rules at a Glance
o Although both pathname and class are optional, at least one parameter
must be supplied.
o In situations in which you can't create a project-level reference to an
ActiveX object, you can use the GetObject function to assign an object
reference from an external ActiveX object to an object variable.
o GetObject is used when there is a current instance of the ActiveX object;
to create the instance, use the CreateObject function.

o If you specify pathname as a zero-length string, GetObject returns a new


instance of the object--unless the object is registered as single instance, in
which case the current instance is returned.
o If you omit the pathname, the current instance of the object is returned.
o An error is generated if pathname isn't specified, and no current instance
of the object can be found.
o The object variable you use within your program to hold a reference to the
ActiveX object is dimensioned as type Object. This causes the object to be
late bound; that is, your program knows nothing of the type of object nor
its interface until the object has been instantiated within your program. To
assign the reference returned by GetObject to your object variable, you
must use the Set statement:
o Dim myObject As Object

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Set myObject = GetObject("C:\OtherApp\Library.lib")


o The details of how you create different objects and classes are determined
by how the server has been written; you need to read the documentation
for the server to determine what you need to do to reference a particular
part of the object. There are three ways you can access an ActiveX object:
The overall object library. This is the highest level, and it gives
you access to all public sections of the library and all its public
classes:
GetObject("C:\OtherApp\Library.lib")
A section of the object library. To access a particular section of the
library, use an exclamation mark (!) after the filename, followed by
the name of the section:
GetObject("C:\OtherApp\Library.lib!Section")
A class within the object library. To access a class within the
library, use the optional Class parameter:
GetObject("C:\OtherApp\Library.lib", "App.Class")

Programming Tips & Gotchas


o Pay special attention to objects registered as single instance. As their type
suggests, there can be only one instance of the object created at any one
time. Calling CreateObject against a single-instance object more than once
has no effect; you still return a reference to the same object. The same is
true of using GetObject with a pathname of ""; rather than returning a
reference to a new instance, you obtain a reference to the original instance
of the object. In addition, you must use a pathname argument with single-
instance objects (even if this is ""); otherwise an error is generated.

o You can't use GetObject to obtain a reference to a class created with


Visual Basic.
o When possible, you should use early binding in your code. For more
details on early and late binding, see Chapter 4. You can use GetObject in
early binding, as in:
o Dim objExcel As Excel.Application
Set objExcel = GetObject(, "Excel.Application")
The following table shows when to use GetObject and CreateObject :
Task Use

Create a new instance of an OLE server CreateObject

Create a subsequent instance of an already instantiated


CreateObject
server (if the server isn't registered as single instance)

Obtain a further reference to an already instantiated server GetObject

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

without launching a subsequent instance

Launch an OLE server application and load an instance of


GetObject
a subobject

Instantiate a class created with VB CreateObject

Instantiate a class registered on a remote machine CreateObject

See Also
CreateObject Function, Set Statement

WithEvents Keyword
Named Arguments
No
Syntax
Dim|Private|Public WithEvents objVarname As objectType
objVarName
Use: Required
Data Type: String
The name of any object variable that refers to an object that exposes events.
objectType
Use: Required
Data Type: Any object type other than the generic Object
The ProgID of a referenced object.
Description
The WithEvents keyword informs VB that the object being referenced exposes
events. When you declare an object variable using WithEvents, an entry for the
object variable is placed in the code window's drop-down Object List, and a list of
the events available to the object variable is placed in the code window's drop-
down Procedures List. You can then write code event handlers for the object
variable in the same way that you write other more common event handlers such
as Form_Load.
Rules at a Glance
o An object variable declaration using the WithEvents keyword can be used
only in an object module such as a Form or Class module.

o An object variable declaration using the WithEvents keyword should be


placed only in the Declarations section of the object module.
o Any ActiveX object or class module that exposes events can be used with
the WithEvents keyword. WithEvents is valid only when used to declare an
object variable.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

o You can't use WithEvents when declaring the generic Object type.
o Unlike other variable declarations, the As keyword is mandatory.
o There is no limit to the number of object variables that can refer to the
same object using the WithEvents keyword; they all respond to that object's
events.
o You can't create an array variable that uses the WithEvents keyword.

Example
The following example demonstrates how to trap and respond to the events within
an ADO recordset. An object variable is declared using the WithEvents keyword in
the declarations section of a form module. This allows you to write event-
handling code for the ADO's built-in events, in this case the FetchProgress event.
(The FetchProgress event allows you to implement a Progress Bar control that
shows progress in populating the recordset.)
Private WithEvents oADo As ADODB.Recordset

Private Sub oADo_FetchProgress(ByVal Progress As Long, _


ByVal MaxProgress As Long, _
adStatus As ADODB.EventStatusEnum, _
ByVal pRecordset As ADODB.Recordset)

ProgressBar1.Max = MaxProgress
ProgressBar1.Value = Progress

End Sub

Programming Tips & Gotchas


o Placing the object variable declaration that uses the WithEvents keyword in
a procedure doesn't add the object variable name to the module's Object
List. In other words, the events fired from the object would have scope
only in the procedure and therefore can't be handled.
o Even if you declare the object variable using the Public keyword, the events
fired by the object have scope only in the module in which the object
variable has been declared.
o Because you can't use WithEvents to declare a generic Object type,
WithEvents can be used only with early bound object references. In other
words, objects must have been added to the project using the References
dialog. Without this prior knowledge of the object's interface, VB has no
chance of knowing how to handle events from the object.

o If the object you are referencing doesn't expose any public events, you will
generate a compile-time error, "Object doesn't source Automation Events."
o You can't handle any type of event from within a code module. This isn't
really a limitation, because to pass program control to a code module, you

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

can simply call one of its functions or procedures from your event handler,
just as you would from a form or control's event handler.
o For information about generating your own custom events in form and
class modules, see the "Implementing Custom Events" section in Chapter
4.
These are the questions I most want answered from Developer.com, my emails, and other
sources. Some I don't have time to dig into deeply enough to give a good answer. Some I
have seen solutions for but don't remember where. Others I have no clue about.
If you know how to accomplish these tasks, please email me! Better still, if you can, zip
up an example program and send that as a binary attachment (no uuencodings please).
Click the items in the list to see peoples' solutions.
1. How can I retrieve a metafile from a resource file?

2. How can I create a floating toolbar?


3. How can I access the History list of an embedded browser?
4. How can I load fonts in VB? (By this I mean install a new font on the system, not
just use different fonts)

5. How can I call an ActiveX exe from Visual C++?


6. How can I pass a Form object from an In-Process DLL back to the main
Application?
7. How can I open a password protected database?

8. How can I open an Access 95 database that is password protected using VB?
9. How can I use ADO in a multi-user environment?
10. How can I list the programs in the task bar?
11. How can I change the color of a title bar?
12. How can I make a gradient color title bar?
13. How can I install multiple programs in one setup program?

14. How can I empty the wastebasket?


15. How can I delete temporary internet files?
16. How can I print a preformated file directly to printer?
If you know how to accomplish these tasks, please email me! Better still, if you can, zip
up an example program and send that as a binary attachment (no uuencodings please).
ANSWERS

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

1. How can I retrieve a metafile from a resource file?


David Otero
You have to add the metafile as a custom resource, then when you need the file, u have to
do something like this:
Dim bData() As Byte

bData() = LoadResData(101, "CUSTOM")


Open App.path & "\file.wmf" For Binary As 45
Put #45, , bData
Close 45
You have to rerplace "101" with the number of the resource, "CUSTOM" with the name
of the type of resource, and "file.wmf" with the name that you want to use.

2. How can I create a floating toolbar?


Robert Terblanche
My solution is quite simple. In fact, it's so simple I'm wondering if I haven't missed
something or maybe misunderstood your question.
All I did was to add a form to my application, and set the BorderStyle to 4 - Fixed
ToolWindow. On this form I created the toolbar that I would normally have created on
my main application form. When I show the form, I set the owner to the main form by
saying:
frmToolbar.Show, Me
That's it. Nothing to it. The Toolbar will now always be on top of my application, but not
on top of other applications. The toolbar I used is just a simple example, using an aligned
picturebox with command buttons on it, but you can of course place any toolbar in the
toolbar form that you wish. You can of also totally remove the toolbar form's title bar,
and add all sorts of fancy rearrangement of buttons when the toolbar form resizes.

An elegant solution. Note that for MDI applications, the toolbox must be a child of the
MDI form, not a child form. See also:
• Give a window a toolbar style title bar (2K) Intermediate
• The book Custom Controls Library which shows how to build a tooolbox that you
could put inside Robert's toolbox form.

4. How can I load fonts in VB?


Robert Terblanche says you can install a font by simply copying it into the Fonts
directory.
He also found some code somewhere on the net that installs a font in a more "official"
way (If you recognize it, tell me where it came from originally so I can give credit to the
author).

5. How can I call an ActiveX exe from Visual C++?

My company is producing software for CAD-users, so we do most of our programming


in C++ but we intend to create the UI in VB. I therefore face the challenge of linking VB
and C++ programs. As long as this only consists in putting a control that is written in the

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

"foreign" language this is (often) painless. But as soon as you have to do something more
complicated or there are problems it seems to be rather difficult to get answers to
questions you might have...
A couple of scenarios are possible
1. The Server is a control:
This is very often painless. Just put the control on a form in the VC++ resource
editor. There is one caveat though, the containers VB offers (Forms, picture
controls and the like) offer a richer "environment" in which the control lives and
with which it can communicate. In VB parlance this environment is called
Extender and Ambient properties. They do not exist for non VB containers or
offer a reduced set of properties one can call. If such a control is put on a VC++
form (aka resource), the IDE will create a class with the necessary interface glue
as soon as you create a member variable for the control the first time. If you
change the interface of the control (the public stuff), things become messy,
because the IDE just keeps on using the old "interface glue", which of course has
become out of date. I have yet to find a way how to remedy this situation without
scanning the MFC created files and deleting all references to the control.
I created a mini MFC application "RRMfcControll" that hosts a simple VB
created control, you can set its properties and react to its events.
[This is the way I would probably try to do it. I should have thought of this. --
Rod]
2. The server is a DLL or a an EXE

To incorporate an ActiveX server that is not a control (it also works for controls
by the way) MFC offers the #import compiler directive. Using it you get all
needed interface glue created and wrapped into a set of smart pointers which are
very easy to use. You just call your server properties in a very VB like manner.
The nice thing is, that you can change your interface in VB as often and
thoroughly as you like. The glue will always be up to date since it is recreated
when ever MFC detects an change in the file dates. However, there is a down side
too. You do not get a interface map created, so there is no "automatic"
communication with your server. You have to do it yourself. As mentioned
before, this is a trivial task as long as the communication is VC++ -> VB. Vice
versa matters are a bit more complicated. Raising an event in VB that is serviced
by a peer actually means calling an function from VB that this peer is exposing.
This calls for a two way communication to be built up between peer and VB.
MFC generates this two way channel automatically for you when it hosts a
control on a resource by creating an message map and an event sink map. Both
these maps are actually wrappers around functions that subclass the windows
involved. Since nobody creates them for us we have to do it for ourselves. Luckily
subclassing comes naturally to C++ and is easy to implement.
I have included two samples demonstrating the communication of C++ with an
ActiveX server. RRConBarBone is a minimal console application calling an

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

ActiveX server RRMfcTwoWays demonstrates the use of a "hand made" message


map to build up a two way communication.
My hand crafted "message map" is of course a much simplified version of what is
going on when two com objects talk to each other. I tried to understand Don Box's
"Essential Com" but failed. That is why I can offer no better solution.

6. How can I pass a Form object from an In-Process DLL back to the main
Application?
Create a property in your DLL that returns a Variant. Set the Property to return the Form
object.
Public Property Get DLLForm() as Variant
Set DLLFrom = Form1
End Property
This trick will work with any object that normally gives that annoying message that it
can't be passed back from a DLL.

7. How can I open a password protected database?


8. How can I open an Access 95 database that is password protected using VB?

Dim Dbpath as String


Dim DB as database
DBPassword as String

DBEngine.SystemDB = DBPath & "\MyDB.mdw"


DBPassword = "database password"
Set DB = DBEngine.Workspaces(0).OpenDatabase( _
DBPath & "\MyDB.mdb", False, False, ";pwd=" & DBPassword)

For both questions this will work:


dim db as database
set db = opendatabase(mydatabase,false,false,";pwd=mypassword")
Another solution:
Data1.DatabaseName = "C:\mydb.mdb"
Data1.Connect = ";pwd=thepassword"
Data1.Recordsource = "My Table"
Data1.Refresh

9. How can I use ADO in a multi-user environment?


This topic deserves a book. I have been dealing with this for a few months. I have a
specific answer to a specific question that drove me nuts for a couple of weeks. I am in
the middle of a project that I have to get done right now so don't have time to work all
that up. If there is enough interest, I will consider doing this in the next few months.
The question is: In an N-tiered environment (based on Lhotka's VB 6 Business Objects
book) where I have an Activex server that uses ADO to talk to an Access .mdb via Jolt
3.51 how do you get reliable error messages returned when two users try to update the
same record. Here is the solution (I understand it also works with SQL Server 7.0):

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

If I use a clientside cursor, with batchoptimistic locking and batchupdate I can get
reliable locking results that I can trap. Now when the second client tries to .UpdateBatch
(assuming they get past the chgcount check) I get the following error consistently.
Case -2147217864 'The specified row could not be located for updating: Some values
may have been changed since it was last read.
In the context of my app this does not make a lot of sense. That is I have the PersistLayer
as an activex server running on the same machine as the MS Access .mdb. So I don't
really see a reason to have to use clientside cursors. Also I am only dealing with one
record in this example so the Batch part does not make since either. But hey at this point
if it works I'll use it.
What I am concluding is that with ADO using Jolt 3.51 you cannot get reliable (same
errors, same place) with serverside cursors. A friend has also demonstrated this technique
works with SQL Server 7.0. He has not gone to the trouble of seeing if the situations that
don't seem to work (that I think should using serverside cursors) will work with SQL
Server 7.0.

10. How can I list the programs in the task bar?


Click here to download an example program. It seems mostly correct, though in my
initial tests it missed a couple tasks.

12. How can I make a gradient color title bar?


Subclass and use an owner draw window. Click here to download an example program.

13. How can I install multiple programs in one setup program?


When you are using the auto setup maker in VB, near the end of the process before it
starts compiling (the last place where you can check things on and off), you can click on
an "add" button, this will compress any other program you add to it. The only drawback
is that when it is installed the programs you added manually will be in the same directory
as the main program.

The source code for the setup program should be in your VB directory. On my computer
it is at:
D:\Program Files\Microsoft Visual Studio\VB98\Wizards\PDWizard\Setup1\Setup1.vbp
You can change the Form_Load event of the form Setup.frm to alter the setup procedure,
or you can edit the file Setup.LST to change the files that will be installed.
The Setup1.vbp application is just another VB project, so you can change it to install
seven and a half applications, run dos programs, draw fractals and beep incessantly
through the PC speaker. ...If that is what you want.
So you have full control over the setup process.
Steve Squires
I made a multiple application setup file by writing down each project's references after
running the setup wizard. After finishing the list, I made a setup for one of the programs
and then added the files I wrote down. I set the proper directories for these files using the
"file details" button. It might be a hassle if you have lots of references to include, but it
works for me.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

14. How can I empty the wastebasket?


Ross Overstreet
Although maybe not the most reliable way:
In Windows, a hidden directory exists which contains the data from the recycle bin. The
"Recycle Bin" on the desktop is only a link to its contents. My "Recycle Bin" directory is
"c:\recycled\" so the command Kill "c:\recycled\*.*" works for me.

Will Swift
Use the SHEmptyRecycleBin function.
Private Sub ClearRecycleBin( _
Optional ByVal RootPath As String, _
Optional ByVal hwndParent As Long, _
Optional ByVal NoConfirmation As Boolean, _
Optional ByVal NoProgress As Boolean, _
Optional ByVal NoSound As Boolean)
Dim nOptions As Long
Const SHERB_NOCONFIRMATION = &H1
Const SHERB_NOPROGRESSUI = &H2
Const SHERB_NOSOUND = &H4

If NoConfirmation Then nOptions = SHERB_NOCONFIRMATION


If NoProgress Then nOptions = nOptions Or SHERB_NOPROGRESSUI
If NoSound Then nOptions = nOptions Or SHERB_NOSOUND

On Error Resume Next


SHEmptyRecycleBin hwndParent, RootPath, nOptions
End Sub

16. How can I print a preformated file directly to printer?


There are two Microsoft references that address this. The best one is Q154078:
HOWTO: Send Raw Data to a Printer Using the Win32 API from Visual Basic, which
has an example using the WIN API to print directly to a printer device.
But the other one is more important -- if you use the FileCopy solution to copy to a
printer device name that contains spaces, it will fail under NT in VB5 and VB6. This is
illustrated in Q252607: PRB: The CopyFile Method Does Not Work for Printer
DeviceName with Spaces on NT. To use FileCopy you'd have to remove any spaces from
the NT printer devicename, therefore it's not a robust solution.
Use the API solution -- it's better!

When you make use of the Printer object, VB will control said printer through the
installed printer driver. This leads to the attempted translation of data that does not need
translating, causing the problems which you have no doubt already experienced.
The trick is to open the printer as a file, and then simply copy the data from your
preformatted file to the printer "file". VB thinks it is a file and does not do any
translation. If the printer is connected to your LPT1 port, you can open the port itself as a
file, e.g.
Open MyFile For Input As #1
Open "LPT1" For Output As #2

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

TempString = Input(LOF(#1), #1)

Print #2, TempString;

Close
Some operating systems will require a colon after LPT1, so you might have to open
"LPT1:" instead of "LPT1". If you want to print to a printer connected to a serial port,
you can open "COM1" or "COM2" or whatever (or "COM1:"). If you want to print to a
remote printer, then I'm stuck. I'm afraid I haven't tried it yet.

I have had mixed results with this technique. My printer starts to print, and then stops.
This may just be a configuration issue on my system.
I got the same results with a network printer specifying the printer file name like this:
Open "//Beauty/Digital" For Output As #2
Here the printer named Digital is being served by the computer Beauty. Presumably this
will work, but I have that problem mentioned above.

One fundamental error (hopefully just a typo) is that the slashes should be backslashes,
i.e. \\PrintServer\Printer, which may work on its own, but if it doesn't, the answer is to
use a virtual port. For example, assign your remote printer to a virtual port, such as LPT2:
or LPT3: through a DOS window, like this...
C:>NET USE LPTn: \\PrintServer\Printer ...
then simply open LPTn: as a file and proceed as Robert suggests for the local printer. For
example:
Open "LPT2:" For Output As #1
Print #1, "Hello, world!"
Close #1
Use caution when working with LPT1 if using Windows NT since LPT1 is a physical
port; that is "actual hardware," which NT protects with absolute security permissions. In
some, if not all, versions of NT, a failed attempt at directly accessing LPT1 results in a
hardware lockout which renders LPT1 unavailable from that point on, unless and until
you reboot the OS.

This used to be a similar problem when I was trying to print preformatted files to a
printer over a Novell network. The solution was to copy the file to the relevant captured
port using the DOS binary switch 'COPY /b'.
In VB, the 'Filecopy sourcefile, destfile' method does the same. The reason Robert
Terblanche is having problems is that the Input# and Print# methods try to interpret the
data on-route (it doesn't say so, but it does).
The following simple code works perfectly:
Private Sub Form_Load()
FileCopy "c:\tims.prn", "\\Server\HP"
End Sub
Where "c:\tims.prn" is the preformatted file and "\\Server\HP" is our networked HP
printer.
NOTE: The files that are preformatted in this way are usually produced by a software
package that has a default printer selected, then the file is printed with the user selecting
'Print to File' option. The .PRN file produced will then contain all the formatting codes

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

relevant to the selected printer. Sending this ouput file to another printer will confuse the
*?*?*?* out of the destination printer if it doesn't understand the control codes.

I had this problem on a network. Admin had disabled MS-DOS commands and VB exe's.
We found that notepad can print with the /p flag on the command line. These execute
notepad/write(wordpad) and print the file you provide.
notepad.exe /p filepath
write.exe /p filepath
This uses winword(word) to execute a macro
"PathToWinword\winword.exe" "Path to Doc" /mFilePrintDefault
(DefaultPrintFile) but the program doesn't shut down unlike notepad/write which do. I
have not tried FilePrint (/mFilePrint). I had to do this when using oracle forms 4.5 (a
really complicated/frustrating program).

Dale Thorn:
In the Dr. Dobb's web site (ddj.com), under Programmer's Resources, under BASIC,
there are some utilities that do a great job of printing.
If you have a preformatted file that's coded for a particular printer, use PRNT, which
simply spools characters to the printer.
For various plain-text files, PSET decides the best fit to the paper you use, and optimizes
the printing as much as possible to fit.
Note: These are DOS programs called from a SHELL command. Of course, you can
import the code into VB and make them VB programs.
I've used PSET extensively on every combination of network and PC, from handheld and
serial port to NT and network. As long as the drivers etc. are in place, it works perfectly.
Performance Tuning
This page lists some Visual Basic performance tuning tips. If you have other tips to share,
let me know. I will update this page as tips are contributed.
The book Bug Proofing Visual Basic contains some performance optimization tips. More
importantly, it explains when to optimize and when not to optimize. The condensed
version is:
First make it work correctly, then worry about making it work quickly.

There are many ways to speed up a Visual Basic program. Unfortunately most produce
only a small benefit. Even if a program uses huge collections, converting them into arrays
will probably only save you a few percent in run time. On the other hand, rewriting the
program's main algorithms can reduce the run time by factors of hundreds.
Here is a list of some of the many things you can do to improve performance. The ones at
the top of the list are more likely to make a noticeable difference.

• Paul Sheldon:
I have found the technique described in the tip "Hide controls while sending them
data" can in some cases give funny results, so I prefer to use the

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

LockWindowUpdate API Call. You call it once passing the hwnd of the control,
and all repaint messages will be blocked for that control (giving the same effect as
hiding it), you then call it again with 0 as the parameter to restart the repaint
messages. Obviously it is good to also put the reset in your error handling.
• Seth:
Hide controls while you send data to them. For example, suppose you want to
display 1,000 strings in a ListBox. Set the control's Visible property to False, set
the strings, and then set Visible back to True. The reason this makes it faster is
because the ListBox doesn't have to "repaint" itself every time a string of data is
sent. The ListBox only has to paint itself once, not every single time an item is
added.
• Constantijn Enders:
Split IF functions. Visual Basic doesn't has an option like complete boolean
evaluation. If the first expression is false it will still evaluate the second one even
if the result will be useless.
• Private Sub Command1_Click()
• ' Slow code
• If f1 And f2 Then
• Debug.Print "True"
• End If

• ' Faster because f2 not executed when f1 is false
• If f1 Then
• If f2 Then
• Debug.Print "True"
• End If
• End If
End Sub
And if possible put the fastest function at the top line If both function have the
same speed put the function that is most of the time false at the top
• Constantijn Enders:
Use option Compare Text at the top of the module. This will eliminate the need
for UCase$ functions. You can still use StrComp(s1, s2, vbBinaryCompare).
• Constantijn Enders:
Parse results directly to controls. If CheckPassword is a function which result a
boolean:
• If CheckPassword = True Then
• cmdLogon.Enabled = True
• Else
• cmdLogon.Enabled = False
End If
Is slower than:
cmdLogon.Enabled = (CheckPassword = True)
Or even better:
cmdLogon.Enabled = CheckPassword

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

• Constantijn Enders:
Cache the results of a function just as you would a property.
• For i = 1 To 10
• Command(i).Enabled = CheckPassword
Next
Is slower than:
bEnabled = CheckPassword
For i = 1 to 10
Command(i).Enabled = bEnabled
Next
Because in the first routine the CheckPassword function is executed 10 times.
• Carman Thornton:
In addition to using integer operations whenever possible. Use \ for division
instead of / (it's faster). Use * .5 instead of / 2. Example: 11 * .5 = 5.5 is faster
than 11 / 2 = 5.5. Assembler instruction "MUL" is faster than "FDIV".
[Click here to download a test program comparing different calculations. You
may be surprised at the results. -- Rod]
• Mike Carver says:
Whenever possible don't use square roots. For example:
If a * a + b * b = 4 Then ...
is much faster than
If Sqr(a * a + b * b) = 2 Then ...
• Smidge sent me this important technique. I have used this one, too, and it can
make an enormous difference, depending on your application.
If you have to do anything with repetative calculations really fast (ie: Circles or
anything dealing with Trig functions), it may help out a lot to create a table of
values for whatever resolution you need for the data.
For example, precalculating the x, y coordinates of a circle about a point every
two degrees (or use radians, which are actually better for this) is often good
enough and much faster than using the SIN, COS and TAN functions.

• Rewrite the program in C++ or Delphi. (This is rarely an option, but it is the
ultimate solution when you REALLY need more performance, so I am listing it
anyway.)
• Upgrade to Visual Basic 5 or 6. Compiled native Visual Basic executables are a
lot slower than C++ or Delphi executables, but they are much faster then the non-
compiled programs produced by Visual Basic 4 and earlier versions. (This is
another expensive option, but easier than learning a new language.)
• Profile your application. Use a performance monitoring tool to see exactly where
the program is spending most of its time. Visual Basic 4 and 5 come with one.
Visual Basic 6 does if you buy the enterprise edition. Don't waste your time
optimizing code that is already fast enough.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

• Decompress graphics. Set the Picture property for a Form or PictureBox to a .bmp
file, not a compressed JPEG or GIF file. Those files are stored in the program in
compressed form so the program needs extra time to display them.
• Preload forms. Load all the forms you will use when the program starts. Then they
can display faster when you need them.
• Use arrays instead of collections. Arrays are much faster. Use collections only if
you need their special features like keyed lookup.
• Preallocate arrays so they are big enough and you don't have to ReDim them later.
• If you need to set all entries in an array to zero, use ReDim to reallocate it. This
takes longer than leaving the array alone (the previous tip), but is faster than
setting the entries one at a time.
• To set entries to zero in a fixed-sized array (allocated using Dim), use the Erase
statement. This destroys dynamically allocated arrays, but resets fixed-sized
arrays. (Thanks to BwetS).
• Use the MemCopy or RtlMoveMemory API functions to copy arrays instead of
copying their entries one at a time.
• Use specific data types instead of Variants. Always declare a variable's data type.
If you don't, it default to variant.
• Use specific object types rather than declaring a variable to be of type Object. Be
as specific as possible. For example, Object is bad, Control is better, TextBox is
best.
• Do not empty a collection by removing its elements. Destroy it by setting it to
Nothing.
• Declare and allocate objects in separate lines. The statement "Dim obj As New
MyClass" is actually slower than "Dim obj As MyClass" and "Set obj = New
MyClass" on two separate lines (try it).

• Use integer operations whenever possible. Use \ for division instead of / (it's
faster).
• Use Len to test for zero-length strings. For example, If Len(my_string) = 0 Then
... This is faster than using If my_string = "" Then...
• Use With for a long series of object references used several times. This executes
faster than if you repeat the series of objects in each statement.
• Use as few string operations as possible, they are slow.
• Order Select Case statements so the most commonly used value comes first.
• Call sub and function with by ref parameters when possible. Adriano Ghezzi

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

[Note that this makes the routine more prone to accidental side effects so be
careful--Rod]
• Set form to nothing when you never need. Adriano Ghezzi [This saves memory
and may save lots of time if you have so many forms that you must page. If you
only have a few forms, it will be faster to keep them always loaded and just hide
them--Rod]
• Perceived performance is as important as actual performance. Imagine clicking on
a button, and nothing happens for 10 seconds. That will be a very long 10
seconds. Add a progress bar, and the user won't even notice the 10 seconds.
Robert Terblanche.
• When you use a lot of images several times in an application. Put them on one
form and load them when needed from that form. Jan Cromwijk [This makes all
the images load when that form is loaded so they are ready to go when you need
them--Rod]
• If you need to do a lot of string/file processing, use mid$ (and trim$ etc.) rather
than mid as the latter treats the data type as a variant as opposed to a string, which
can be up to 3 times slower (I think you can use the $ sign with mid, trim, left and
right). Steven R. Hamby.

• To make the application seem faster, display its first form as quickly as possible.
Use Show in the form's Load event handler to make it appear before performing
long startup calculations.
• Put as little code as possible in Form_Load event handlers so the forms load
quickly.
• If the initial form taks a long time to load, display a splash screen immediately
and remove it only when the first form is loaded (Advanced Visual Basic
Techniques shows how to make different kinds of interesting splash screens).
• Group subroutines in modules. When one routine calls another, the other routine's
module is loaded. If one routine calls many others from different modules, all the
modules must be loaded. If all the routines are in the same module, they will all be
loaded at once.
• Do not waste memory. Sometimes you can make a program faster using more
memory, but sometimes more memory can slow things down. In particular, if you
use so much memory that the program cannot fit in real memory all at once, the
system will page. That can slow the program enormously.
• Set AutoRedraw to False to reduce memory usage. Set AutoRedraw to True to
make redrawing faster for complicated drawings.
• Set ClipControls to False (read the help for more information).

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

• Use Move to position controls instead of setting the Left and Top properties.
• Hide a control if you need to change a bunch of its appearance properties. Make it
visible again when you are done modifying it.
• Use a temporary variable to refer to a complex expression multiple times. For
example, suppose you need to set several values in the
SelectedEmployee.NextOfKin.HomeInformation.Address object. Instead of
referring to this long expression several times, use:
• Dim addr As AddressInfo

• Set addr = SelectedEmployee.NextOfKin.HomeInformation.Address
• addr.Street = txtStreet.Text
• addr.City = txtCity.Text
• addr.State = txtState.Text
• addr.Phone = txtPhone.Text
• Cache properties you use multiple times. If the program needs to refer to
txtLastName.Left several times, save that value in a variable and refer to the
variable instead. Accessing variables is much faster than accessing properties.
• Use Line (x1, y1)-(x2, y2), , B to draw a box instead of using Line four times.
• Use Image controls instead of PictureBoxes if possible. Image controls take less
memory.
• Use Frame controls to hold other controls instead of PictureBoxes. Frame controls
take less memory.
• Use control arrays for controls that are unimportant. For example, many forms
contain a lot of uninteresting labels. Put them all in a control array. A control
array containing 10 controls usees less memory than 10 individual controls.
• Perform long, low-prioirity calculations in the background using a Timer.
• Use comments and meaningful variable names. Long comments and variable
names, and blank lines do not add to the compiled program's size so there is no
harm in using them.
• Do not line number every line because line numbers increase the program's size.
• Remove unused variables and code since they remain in the program and take up
memory.
• Use DoEvents to allow the user to perform other actions while your long process
is running. This can reduce the user's frustration even if it doesn't make the
program move faster. (John Dye)
• Use the FindFirstFile, FindNextFile, and FindClose API functions to quickly
search directories. Thanks to Nikolaos D. Dimopoulos.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

[Note that using API functions is often but not always faster. It is always more
complicated and sometimes riskier than using VB--Rod]
• UCase$ and LCase$ let you perform case insensitive comparisons. The following
API functions are faster:
• Declare Function CharLower Lib "user32" _
• Alias "CharLowerA" (ByVal lpsz As String) As String
• Declare Function CharUpper Lib "user32" _
• Alias "CharUpperA" (ByVal lpsz As String) As String
Thanks to Nikolaos D. Dimopoulos.
• Use a temporary variable to refer to a complex expression multiple times. For
example, suppose you need to set several values in the
SelectedEmployee.NextOfKin.HomeInformation.Address object. Instead of
referring to this long expression several times, use:
• Dim addr As AddressInfo

• Set addr = SelectedEmployee.NextOfKin.HomeInformation.Address
• addr.Street = txtStreet.Text
• addr.City = txtCity.Text
• addr.State = txtState.Text
• addr.Phone = txtPhone.Text
The With command speeds things up in the same way, so this could be:
With SelectedEmployee.NextOfKin.HomeInformation.Address
.Street = txtStreet.Text
.City = txtCity.Text
.State = txtState.Text
.Phone = txtPhone.Text
End With
Thanks to Mark Focas.
• Use ByRef to pass values instead of ByVal. When you use ByRef, the program
passes the (small) address of the value. When you use ByVal, it must make a new
copy of the value and pass that. Generally it is faster to pass the address instead of
a copy.
However, when you are making an out-of-process call, ByVal is faster. With out-
of-process calls, Visual Basic must repackage the value anyway to send it to the
other process. If you use Byref, it must then unpackage the returned result and that
takes extra time. Thanks to Kevin B. Castleberry.

• Use * instead of ^ to take simple integer powers. For example, use A = B * B


instead of A = B ^ 2. The first is faster. Thanks to Michalis Vlastos.
• If you need to build a long string, build it in pieces and then join the pieces when
they are all finished. For example, suppose subroutines AddText1, AddText2, etc.
append text to a string. Then the following code:
• Dim txt As String

• txt = AddText1(txt)

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

• txt = AddText2(txt)
• txt = AddText3(txt)
takes longer than this code:
Dim txt As String
Dim txt1 As String
Dim txt2 As String
Dim txt3 As String

AddText1(txt1)
AddText2(txt2)
AddText3(txt3)
txt = txt1 & txt2 & txt3
In the first code, the AddText subroutines must manipulate long strings. In the
second example they work with relatively short strings.
• Save intermediate results in mathematical calculations. For example, this code:
• Xsquare = x * x
• Ysquare = y * y
• a = 2 * Xsquare + 3 * Ysquare
• b = 3 * Xsquare + 5 * Ysquare
• If a + b > 50 Then ...
is faster than this version:
If 2 * x * x + 3 * y * y + _
3 * x * x + 5 * y * y > 50 _
Then ...
Thanks to Michalis Vlastos.
• Cade Roux has some words of wisdom about Visual Basic's optimizations.
When I moved to VB5 from VB4, I immediately started compiling everything to
native code for speed. For large interactive applications which are not processor
bound, I have found the size of the executable for the compiled version causes it
to load much slower and execute slower due to the large executable size, and
probably larger working set. I had a 10MB exe go down to 4MB by switching
back to P-Code. The compile time is vastly shorter as well, resulting in quicker
test-cycles. We no longer compile to native code at all, even on smaller
applications.
[Database bound applications may show the same effect. Any program that spends
a lot of time waiting for some slow process (the user, a database, a modem, etc.)
will not be limited by the code speed. In those cases, you will get smaller
executables and possibly better performance if you do not compile. -- Rod]
• From Chris Collura:
When looping to a final value, do not put the function returning the count in the
looping logic.
i=1
Do While i <= SlowFunction()
total = total + i
Loop
Runs slower than

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

i_max = SlowFunction()
i=1
Do While i <= i_max
total = total + i
Loop
[Note: For loops do not evaluate their bounds every time through the loop. When
the For statement starts, the system calculates the upper bound and saves it. It
does not recalculate it every time through the loop. Therefore this code is
reasonably fast:
For i = 1 To SlowFunction()
total = total + i
Next i
Rod]
Fifty Ways to Improve Your Visual Basic
Programs

If you are fairly new to Visual Basic, here are 50 things you can do today to make your
VB code more effective and easier to maintain. Although there are usually exceptions to
any rule, my intent is to help you learn from my experience programming with VB. I'll go
into great detail on some items, while others will take the form of general advice. If you
find that I am being vague, it is probably for one reason: I want you to research it
yourself. And honestly, I don't have the space to discuss everything that I would like to
cover in detail.

I don't expect you to agree with everything presented here. Programmers can be
passionate about their beliefs as to what is good or bad. But hopefully, there is a tidbit or
two you can take with you and use to improve your VB programs.

I originally submitted many of the topics discussed in this article as a book idea to
O'Reilly. But shortly after I sent in my proposal, .NET appeared on the scene; thus, doing
a whole book was no longer a viable option. Many of these topics can be adapted for
.NET, but some will not be applicable. Regardless, this discussion is targeted at the
beginning Visual Basic programmer who is already writing code.
No doubt you will look at some of these topics and say, "Well, of course you should do
that!" or "Who the - does that?". Every single one of these topics is something that I do,
or something I have seen done in corporate America, where I work as a contract software
architect and developer. With that said, let's get to the tips, which are broken down into
the following categories: The Basics; Variables and Declarations; Function Design;
Classes, Objects, and Object Models; The User Interface; and Miscellaneous.

The Basics
1. Use error handling everywhere. Whether you are writing a function or simply
accessing a basic property, define an error handler using On Error GoTo. This
should look similar to the following:

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

2. Private Sub Encryptolizer()


3.
4. On Error GoTo Errs
5.
6. 'Code goes here
7.
8. Exit Sub
9. Errs:
10. 'Call error handler here
11. End Sub
Even if your function, property, or sub only contains a single line of code, put that
error handler in there. The drawback to this is a substantially larger executable.
The benefit is code that will eventually have little to no errors. The error handler
itself can be a standard function located in a module or in a class that encapsulates
all of the required functionality of the handler.
However you decide to implement it, make the error handler as robust as possible.
And try this: Next time you start a project, write the error-handling code first, so
that it is in place. If you treat your exception-handling code as the most important
part of an application (it is actually) instead of as a footnote to be tacked on at the
end of the development cycle, you will be amazed at how strong your application
will be by the time it goes to testing.
12. Never use On Error Resume Next. Using the On Error Resume Next statement not only
violates tip 1, it also makes code difficult to debug. This statement should be
avoided at all costs. It breeds the most hard-to-find and unpredictable bugs you
could ever imagine. And for those of you who know assembly language, try
disassembling code with an On Error Resume Next statement embedded in it. What
you will find is a massive jumble of spaghetti code that is injected into the
function by the compiler in order to implement this functionality. Imagine a 9-
year-old programmer who likes GoSub and GoTo and you get the idea.
13. Indent, use white space, and comment your code. This is a simple suggestion
meant to make your code more readable. First, all code should be indented once
(four spaces) within the block where it resides. Second, all code should have at
least one line of white space separating it from the block where it resides. Third,
declare all variables on their own line so that they can have comments associated
with them. It can also be beneficial for large blocks of code to have a comment on
the End statement (End If, Loop, and so forth) of the block that points to the start of
the block. Well-formatted code looks like this (squint your eyes and pretend that
the function does something):
14. Public Function CrispAndClean() As Boolean
15.
16. On Error Goto Errs
17.
18. Dim x As Integer 'x coordinate
19. Dim y As Integer 'y coordinate
20. Dim z As Integer 'z coordinate
21.
22. If (x > 10) Then
23.
24. If (z < 20) Then

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

25.
26. 'code starts here
27.
28. End If
29.
30. Else
31.
32. x = x * 10
33. y = y * 20
34. z = z * 30
35.
36. End If 'If (x > 10) Then
37.
38. Exit Sub
39.
40. Errs:
41. 'Error handler here
42.
43. End Function
44. Know the language. There are many great functions hidden away in VB that you
may not know about. Spend time learning the language. The VB Help file is a
great place to start. Go to the index, start at the top, and read about any keyword
you don't know. After you have done that, get a solid desktop reference. Hands
down, VB & VBA in a Nutshell: The Language is the best language reference on
the market. Believe it or not, that is my unbiased opinion. I owned a copy before I
ever wrote a single word for O'Reilly.

This topic comes up for a reason. Once I was working on a project with a really
great programmer who was in the process of writing a really great string parser.
Imagine his surprise when I showed him the Split function. Amazingly enough, it
could do in one line of code what his was doing in over 20. Needless to say, he
chucked his code out the window.
Believe it or not, I meet people all the time who have never heard of the Split, Join,
InStrRev, or Filter functions.

45. Avoid legacy BASIC. Do your part to rid the world of legacy BASIC. Avoid
using DefInt, GoSub, QBColor, REM, and Let. And unless you are writing an
application with accessibility considerations, consider avoiding Beep as well.
Users find it annoying. Also, avoid declaring data types with suffixes instead of
the name of the data type:
46. Dim name$ 'This looks like my old Atari 800 code
47. Dim num%
48. Dim bignum&
I mention the declaration syntax for reasons of clarity. A good rule of thumb is
that you should always be clear, versus, well, not being clear. Don't be afraid to
type. And don't put more than one statement on the same line:
If x = 4 Then: x = 5: y = 6
Nobody likes reading code like this.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

49. Don't sacrifice maintainability for speed. There are many things that can be
done in VB to make an application run faster. Global variables, undocumented
pointer functions, and loop unrolling are just a few. Unless you are maintaining
your own code for your own software company avoid these things and try to make
your code as readable as possible. If you work in the corporate world this is very
important. The person who inherits your code when you've moved on to greener
pastures will praise your name instead of curse you. Believe me, the folks who
pay you don't care how slick you can code. Ultimately, they want something that
will not only work, but is also flexible and easy to maintain. If you are doing
something that requires intimate knowledge of the black arts of coding, don't do
it.

Variables and Declarations


7. Always use Option Explicit. By default, VB is set for Sloppy Mode. In Sloppy
Mode, variables don't have to be declared before they are used. Why is this bad?
It leads to hard-to-find bugs, should you happen to misspell a variable name:
8. Public Function Vague()
9.
10. position = (x * y) + 5
11.
12. 'more code here
13.
14. If (position > 10) Then
15.
16. 'This code will never be executed
17. 'because "position" is misspelled
18.
19. End If
20.
21. End Function
To take VB out of Sloppy Mode, open the Tools/Options dialog from the menu.
On the first tab, named Editor, check the option that says "Require Variable
Declaration." If you are doing this after you've already started a project, make
sure any forms, classes, or modules you've already created have Option Explicit at
the top of the file.
22. Don't use Hungarian notation. Gasp! I am going to go against popular opinion
on this and argue against using Hungarian notation in any way, shape, or form.
This is my opinion, but I think well-named variables are better. I am actually a
recent convert to the dark side. I used to use Hungarian notation religiously, but I
have since been shown the error of my ways.

The first reason I will give you is that the design guidelines for the up-and-coming
.NET Framework say not to use it. It would be an easy cop-out to say that this is
the reason this tip exists; that I am just preparing you for the future. And anyway,
everything in .NET is an object. You can't really go around putting an "o" in front
of everything can you?
But there are other reasons:

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

o VB is not coded outside of the VB IDE. I am sure there are a few


renegades out there who use vi for Windows, but for the other 99 percent
of us, the IntelliSense built into the IDE works just great for telling you
the type for all your variables.
o Another argument for using Hungarian notation is: "When I print out my
code to read it, I want to be able to see what I am dealing with." This is a
valid argument. On the other hand, some people prefer looking at well-
named variables instead of a bunch of gobbledygook.

o What if the data type changes? You have explicitly tied the variable name
to its type. In addition to having to redeclare your variables to the new
type, you have to change every single place where the variable was used to
use the new Hungarian prefix. Think about the repercussions of this. Some
of you out there will say that global Find and Replace was created for
things like this. But I say that this is a clue that Hungarian notation is
lacking.
23. If you are going to use Hungarian notion anyway, maintain style. Use the
same style that is being used in the project. If everyone else is prefixing integers
with an n, follow suit. If you are the only one on the project, at least make sure
your style is consistent. Read this Microsoft document for more information.

24. Don't use hard-coded values. Always avoid using literal values. Even if you
know a value will never change, you should at least use a constant.
25. Don't use global variables. Global variables lead to global problems, especially
when you are working on a large project with several developers. However, in
some circumstance a global variable will be necessary. There are just those times
when you need to get something done instead of designing the application
properly (yes, this is a barb). In these cases, create accessor functions to get to the
global variable instead of using it directly:
26. 'Global
27. Public GlobalNumber As Integer
28.
29. 'Accessor functions
30. Public Function Get_GlobalNumber() As Integer
31. Get_GlobalNumber = GlobalNumber
32. End Function
33.
34. Public Function Set_GlobalNumber(ByVal number As Integer)
35. GlobalNumber = number
36. End Function
It is especially important to do this if your global variable needs to be validated in
any way. But unfortunately, there is no way to force other developers to use the
accessor methods. When faced with a situation like this, ask yourself why
GlobalNumber isn't a private member of a class as it should be.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

37. Think small. What does this mean exactly? It means several things. One, it
means that you should strive to limit the scope of all your variables as much as
possible. In the ideal situation every variable is private.
Two, make your class interfaces as small as possible. Accomplish as much as you
can with the fewest number of public functions that have the smallest number of
parameters (tip 15 is the exception).

38. Expose as little as possible. The more that is hidden from the outside world, the
more flexible your code will be. This is most true when you are dealing with
objects. The less you expose to the outside world, to your clients, the more you
can change on the inside world without breaking anything. This topic is closely
related to an important object-oriented concept called encapsulation.

Function Design
14. Procedures should be designed to complete a single task. Avoid writing
procedures that try to accomplish too much. Failing to follow this rule will make
your code less modular, which ultimately could lead to code that is harder to
manage or extend.
15. Pass as much information as possible when using remote objects. If you are
using remote objects such as ActiveX DLL's in a COM+ setting or an ActiveX
EXE, design your objects so they can be initialized from a single method call.
You want to minimize calls across the network to use as few calls as possible,
otherwise your performance will suffer greatly.
16. Functions and Subs have one entry, and they should also have one exit. Avoid
using Exit Function and Exit Sub. Use a GoTo to jump to your cleanup block (see next
tip), or see if you can restructure your code differently. As a general rule, your
code should flow from top to bottom. Exit Loop and Exit For are fine because they
do not disrupt program flow. I think you will find that your code will be easier to
debug if you know there is only one place a function can exit.
17. Use GoTo for function cleanup after error conditions. In error conditions it is
valid to use GoTo in order to jump to the program exit. (I didn't make this up, I
started doing this years ago, after reading Writing Solid Code.)
Using GoTo allows you to implement generic error handling and allows the
function to place any cleanup code in a single location, rather than repeating it
throughout the routine. Here's some pseudo-code:
Public Function GetData() As Boolean
On Error Goto Errs

'Allocate resources that need cleaning up


Dim conn As ADODB.Connection
conn.Open(.)

Dim rs As ADODB.Recordset
Set rs = conn.Execute(yadayadayada)

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

If (something bad happens) Then


Goto Cleanup
End If

If (another bad thing happens) Then


Goto Cleanup
End If

'More code here

Cleanup:
rs.Close
Set rs = Nothing
conn.Close
Set conn = Nothing

Exit Sub

Errs:
'Call error handler here

End Function
Using GoTo is bad, but only if you don't know what you're doing. This is one of
the few valid reasons to ever use them.
18. Use ByVal for function parameters. I am not going into much detail on this topic
because it is actually bigger than it looks. There are reasons for calling ByVal that
deal with performance, but the reason this tip exists is simple: If you do not pass
ByVal, you are modifying the variable that was passed in. This variable will
remain modified once the function exits, which can cause problems for the
beginning programmer.
19. Default functions to failed condition upon entry. After you declare your local
variables, the first line of code in a function should either set the return value to a
default value or a False condition. There are usually a lot more things that can go
wrong, but only a few things that will satisfy a True result.

Yes, VB will automatically set a Boolean value to False, but do it yourself


anyway. It states your intent clearly. And seriously, this topic would not exist if I
didn't see the following all the time (I literally saw this last week):
Public Function Foo() As Boolean

If (success) Then
If (more success) Then
If (greater success) Then
Foo = True
Else
Foo = False
End If
Else
Foo = False
End If

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Else
Foo = False
End If

End Function
The first clue something is wrong is that Foo is set to False three times. If you
assume a False condition from the onset, you can rewrite the function to look like
this:
Public Function Foo() As Boolean

Foo = False

If (success) Then
If (more success) Then
If (greater success) Then
Foo = True
End If
End If
End If

End Function
Here, each condition is set only once.
Classes, Objects, and Object Models
20. If you want to use a UDT (user-defined type), think about a class instead.
This is true if you find yourself writing module-level functions that take the UDT
as a parameter. In this case, what you actually want is a class so that you can
encapsulate your data along with the functions that manipulate that data. UDTs
should only be used to group small pieces of related data. This data should only
consist of primitive data types (Integer, Long, Double) and not object references.

21. Understand when to use properties versus methods. Properties describe an


object's state: like a color, a name, or a quantity. A function describes a behavior:
Open, Close, Release. Properties are nouns. Methods are verbs.
22. Model "Has A" relationships through containment. Containment or
aggregation is when one object contains another (usually as a private member of
the class). For instance, a Car class could contain an Engine class. A Boat class
could contain an Engine class, too.

23. Model "Is A" relationships with interfaces. Define common object behaviors
with an interface. For instance, if you were writing objects for an online retailer
you might have a Visa class, a Discover class, and a MasterCard class. While
each might need to be validated differently, all cards need to be authorized, billed,
and credited. You should define these behaviors using an interface.
24. Build classes by implementing interfaces. This allows classes to grow over time
yet keeps them backwards compatible. Keep your interfaces as small as possible,
and build your objects through the use of interface composition.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

25. Implement several interfaces to define object behavior. Interfaces should


represent a service that your object provides. These services are not necessarily
intrinsic to the object itself. For instance, an IPlay interface could be implemented
by VCR, DVD, or CDPlayer. Each of these classes handles a different media in a
different way, but the interface (play, pause, stop, etc.) is the same.

26. Never break an interface--no matter what! This is pretty self-explanatory.


Once an interface has been used, don't change it. Apply this rule to published
classes as well, and you will do even better.

27. Use Implements in a way that supports polymorphism. Consider the credit card
classes discussed in tip 23. Basing each class on an interface, would allow you to
write code that could manipulate these objects polymorphously:
28. Dim v As Visa
29. Set v = New Visa
30.
31. .
32. .
33. .
34. Call BillOrder(Visa)
35. .
36. .
37. .
38.
39. Public Sub BillOrder(ByVal pCreditCard As ICreditCard)
40.
41. If pCreditCard.Bill Then
42. 'Ship the order
43. End If
44.
45. End Function
Why is this great? Well, what if you add more credit card types, or even a gift
certificate? You would not have to rewrite the BillOrder method every time you
added a new type of payment.
46. Free object references. Be aware of the objects you are using and set them equal
to Nothing as soon as they are no longer needed. Don't necessarily wait for the
cleanup code for your function to finish. If you can free an object before, do it.
47. Restrict input to class methods with enumerations. Use an Enum when a
method requires numerical data that must be within a certain range.

48. Design your object model on paper first. I am not necessarily saying to go out
and learn UML and start drawing object diagrams. Write out the methods you
think the class should have. Draw pictures and use a pencil. Sure, this is
unorthodox, but the idea here is to contemplate instead of diving head first into
the code without thinking first. UML is a helpful tool, though.

49. Be aware of how your object looks in an object browser. This topic is tied to
tip 8 somewhat. Go take a look at ADO in the Object Browser and you will notice
that none of the parameters for any of the Public methods use Hungarian notation.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

I once worked with a guy who used to name his functions using Hungarian
notation, too. Since every one of his functions returned a Boolean, IntelliSense
was pretty much rendered useless because everything began with a "b". Keep in
mind how the object browser and IntelliSense will present your code to other
developers.

The User Interface


32. Read "Official Guidelines for User Interface Developers and Designers."
This is another Microsoft document worth reading, which deals with user
interfaces.
33. Separate the business logic from the user interface. Forms should only contain
data validation and persistence code. Business logic and database access code
should be in another tier.
34. Persist your forms. Persistence means that the user interface should save its state
between invocations. It should default to whatever settings the user last selected.
35. Minimal is better. The simpler the UI the better. Don't distract the user by
placing all of the latest and greatest controls on a form, or non-standard UI
elements like purple buttons and baby-blue list boxes.
36. The user interface should be able to convey the result of all input. Whenever
the user does something, make sure the UI conveys it.
37. Don't report normal conditions to the user with dialogs. The less popup
windows a user has to look at the better. Try to use other visual queues to get your
user's attention. Dialogs interrupt the user and he or she will find it annoying after
awhile.

38. Don't require the user to enter information that can be obtained
automatically.
Do as much as possible for the user. For instance, if 9 times out of 10 the current
month is used, make sure your date text box already has the current date in it. Do
the most, with the fewest number of clicks and the smallest amount of keyboard
strokes.

39. Forget that MDI windows exist. Enough said. If you have no idea what an MDI
window is, you are off to a good start. Most people don't do it right anyway, so it
is not worth the risk.
40. Work to eliminate tedium. Make sure the user can get related information as
quickly as possible (but not at the expense of the user interface).

41. Visually show the progress of an operation and provide the user with the
means to cancel it. Visual cues are very important to users, but they hate feeling
like they are committed to an operation. Always give users a way to bail out,
especially if the procedure is time consuming.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

42. Don't forget about the keyboard. Assume there is no mouse. Provide the means
to do everything (if possible) from the keyboard. This is somewhat tied to tip 40.
Keyboard shortcuts are a must, especially on applications that will be used for any
considerable amount of time.
43. Use popular applications as a model. I like to use Word or SQL 2000 as a
model for user interface design. You might use a different application. It's up to
you. Look at professional applications and model yours after them. The sad truth
is that most programmers are not very good at designing user interfaces. There is
no shame in stealing someone else's ideas.
44. Don't display errors that should have been prevented. A user shouldn't have to
look at a dialog box stating that your application has performed a divide by zero
error. For one thing, this error never should have made it this far.
45. Eliminate the possibility of errors by restricting input. Prepopulate form fields
whenever possible, and limit what a user can do in the UI. Try to prevent the user
from entering as much free form data as possible. For instance, if you have
numerical data between 1 and 1000 make sure the MaxLength property of the
textbox is set to 4. If you have numerical data between 1 and 20, use a dropdown
with the numbers 1 to 20 in it.
Provide lists through dropdown combos instead of allowing users to enter
information into a textbox. Where I am working at this moment, there is a
reporting application that asks the user to enter in the "account types" they wish to
filter on. Why this is not presented through a multiselect listbox, I don't know; the
account types are stored in a database.
The point to this topic is that the more you can restrict the input of the user, the
less validation code you will have to write.
46. User interface should be consistent. All visual elements of your UI should be
the same in terms of style. One form should not vary wildly from another.

Miscellaneous
47. Avoid Option Base. Arrays should start with element 0 because most people are
used to using them that way. Using Option Base makes looking at your code
somewhat of a guessing game.
48. Avoid ReDim. This is because ReDim can be an expensive operation because every
time you do this you are actually allocating a new block of memory. ReDim
Preserve is even more expensive because an additional memory copy is used to
move all of the elements from the old block of memory to the new block. Use of
these commands here and there is OK if you are aware of the penalties involved. I
bring this up because I have seen ReDim used in loops so many times (I can't count
that high) that is unbelievable. Think about how you could use a collection in
situations where you are using ReDim in a loop.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

49. Avoid the operator precedence guessing game. Use parenthesis to guarantee the
outcome equations and logical branches. Yes, even professional programmers can
get tripped up over operator precedence. I avoid having to think about it by using
parenthesis around every operation:
50. x = (((x * y) + z) / q)
51. Consistency is the cornerstone of style. Whatever you decide to do code wise,
do it consistently. Develop your style. This will do more for you than any tip that
someone gives you. If you take nothing else with you from this article but this tip,
then you are off to a good start. You might have noticed that consistency has been
discussed during several of the tips here. It really is the foundation of good
programming.

J. P. Hamilton is an independent software developer who lives and works in Houston,


Texas, perhaps the largest underrated city in the known universe. He was born and raised
on the 6502 processor (long live Atari) and punk rock, but now devotes much of his time
to the .NET Framework. Currently, when he is not programming, he is reading about
programming, writing about programming, or dreaming about programming. But he
hopes to start building a Japanese garden at his home this fall when the weather cools
down and the mosquitoes are dead.

My Top 15 VB Tips and Tricks

1. Use vbCrLf in Embedded SQL Scripts


Visual Basic contains quite a few intrinsic constants -- that is, constants that are now part
of the VBA language but that you used to have to define in your code either explicitly or
by adding the Constant.Bas file to your project. One of these is vbCrLf, which equates to
the carriage return/line feed -- or Chr$(13) & Chr$(10) - character combination.
But why would I suggest that you include this constant in an embedded SQL script? After
all, the code means nothing to SQL Server; it could care less that you want a carriage
return and line feed at the end of each line. The answer: debugging, plain and simple.
Quite often, you'll find yourself creating embedded SQL scripts that run to 10, 20 or more
lines. For example,
sSQL = "SELECT * FROM anytable" _
& " WHERE userid = " & lUser _
& " AND color = '" & sColor "'"
If you define the SQL statement in this way, the SQL script is readable when viewed in
the procedure, but what about when you want use the Immediate window to see the value
of the sSQL variable at run time?
You enter
? sSQL
in the Immediate window, and a long, unbroken string is shown, running miles off into
the distance.
However, if you simply suffix each line with vbCrLf, like this:
sSQL = "SELECT * FROM anytable" & vbCrLf _

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

& "WHERE userid = " & lUser & vbCrLf _


& "AND color = '" & sColor "'"
then you can quickly and easily read your completed SQL code to find the problem.
The previous example illustrates another benefit of vbCrLf: if you don't use it, you must
remember to start (or end) each line with a space; otherwise,
sSQL = "SELECT * FROM anytable" & _
& "WHERE userid = " & lUser
becomes
"SELECT * FROM anytableWHERE userid = 1"
and your code will generate a SQL syntax error.
2. Use #If ccDebug to View Hidden Automation Servers
One of the problems with testing and debugging a remote automation server is that the
server application typically remains hidden, doing its work in the background. Hence,
when something goes wrong, you frequently can't tell what has happened or where in
your code your application is being derailed.
Consequently, when you're developing, testing, and debugging your application, it's a
good idea to make sure that otherwise hidden automation server remain visible. You an
do this by using code like the following to make sure that a new instance of an
automation server (in this case Microsoft Word) is visible:
Dim objWord as Word.Application
Set objWord = New Application

#If ccDebug Then


objWord.Visible = True
#End If
You can define the project-level conditional compiler constant ccDebug in either of two
ways:
• By including the line
• #Const ccDebug = 1
in the declarations section of a form or code module.
• By using the Make tab of the Project Properties dialog (select the Project
Properties option from the VB Project menu) and entering the following in the
Conditional Compilation Arguments text box:
• ccDebug = 1
Then, whenever your code runs in the design-time environment, you automation server
will be visible. When you are ready to create the final executable, you should either set
the ccDebug conditional compilation constant to 0 or, if you've used the Conditional
Compilation Arguments text box, remove its definition.
3. Implement an IsSaved (or IsDirty) Property
I often find this technique very useful, but it takes a little extra work when you're creating
your object classes. On balance, despite this up front investment of time, it saves a good
deal of programming time by making it easy to determining when an object has changed
and therefore needs to be saved.
To implement an IsSaved or IsDirty property, each Property Let procedure of a particular
object must contain a line of code to determine if the value to be assigned to the property

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

is different than its current value (which should be stored in a private member variable).
If it is different, then the private member variable that represents the IsSaved property is
set to False. For example:
Property Let CustStreetAddr(sVal as String)
If msCustStreetAddr <> sVal Then
msCustStreetAddr = sVal
mbIsSaved = False
End If
End Property
(Of course, you can also implement this the other way round by having an IsDirty
property that returns True when the object needs to be saved.)
Back at the client end you can check to see if the object needs saving quickly and easily
as follows:
If Not myObject.IsSaved Then
SaveTheObject
End If
On the server object, this is implemented as a simple Property Get procedure:
Property Get IsSaved() As Boolean
IsSaved = mbIsSaved
End Property
Another neat addition to this is to define an object event called something like
ObjectChanged. Then the event can be fired whenever some attribute of the object
changes:
Property Let CustStreetAddr(sVal As String)
If msCustStreetAddr <> sVal Then
msCustStreetAddr = sVal
mbIsSaved = False
RaiseEvent ObjectChanged()
End If
End Property
On the client form, you can then implement an event handler for the ObjectChanged
event that enables the Save button when the object needs to be saved:
Sub MyObject_ObjectChanged()
cmdSave.Enabled = Not myObject.IsSaved
End Sub
This code enables the Save button when the object is not saved and disables the button
when the object has been changed.
I should add a major qualification to this tip: don't update your object property based on
the Change event handler of a text box. The Change event is fired for each keystroke that
the text box receives. Therefore typing a word like "Stupid" into the text box will fire off
6 Change events - and the final result is that the text box could contain the same word
that it originally started with, so that in fact its contents haven't changed at all despite the
firing of six unnecessary events.
4. Implement a For Each...Next Statement against a Collection Class
Most of the time, we take for granted the For Each...Next loop, which iterates the members
of an array or a collection. It's the fastest, most efficient method of visiting all the
members of the collection or array, and we could care less that, as it enumerates the
collection, the unseen code is actually generating new references to members of the
collection with each iteration of the loop. However, as the provider of a collection class,

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

it is up to you to provide an interface that the For Each...Next statement can work with.
This may sound a little daunting, but you'll be pleasantly surprised how easy it is to
implement a property that enumerates members of the collection within your class. First
of all, you must create a Property Get procedure called NewEnum with the type of IUnknown.
Its syntax is always the same:
Public Property Get NewEnum() As IUnknown
Set NewEnum = mCol.[_NewEnum]
End Property
where mCol is the name of your private collection object variable.
Second, set the Procedure ID for this Property Get procedure to -4. To do this, select the
Procedure Attributes option from the Tools menu, then click the Advanced button on the
Procedure Attributes dialog. Enter a Procedure ID of -4. You should also check the "Hide
this member" option to prevent the property from appearing in the IntelliSense drop
down.
5. Implement an Exists Method within a Collection Class
One of my long standing gripes about the Collection object is the complete absence lack
of an easy method to determine whether the member you're looking for exists within the
collection. Therefore, when I'm writing a wrapper class for a collection, I always include
my own.
However, I add a little more to the method than simply determining if the member exists
in the collection. If the member is not found within the collection, I attempt to add it to
the collection. This way, I can simplify the code at the client end by always calling the
Exists method prior to assigning the member to a local object variable. Therefore, I know
that if the Exists method returns true, I can safely go on to assign the member to the local
object variable.
The code for a typical Exists method is shown below:
Public Function Exists(sDomainName As String) As Boolean

On Error GoTo Exists_Err

Dim oTemp As Domain


Set oTemp = mcolDomains.Item(sDomainName)
Exists = True
Set oTemp = Nothing
Exit Function

TrytoGet:
If Not LoadDomain(sDomainName) Then
Exit Function
Else
Exists = True
End If
Exit Function

Exists_Err:
If Err.Number = 5 Then
Resume TrytoGet
Else
'further error handling here for other error types
End If

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

End Function
As you can see, the idea is to test for the presence of a particular member of the collection
by attempting to assign it to a temporary local object variable. If the member is not
present, then error 5 ("Invalid Procedure Call or Argument") is raised. Trapping this,
program flow proceeds to the LoadDomain function, which attempts to load the member
into the collection.
The new VB6 Dictionary object (found in the Scripting Runtime Library) contains its
own built-in Exists property. The custom Exists method for the collection object can
therefore be cut down dramatically but still achieve the same results, as the following
method illustrates:
Public Function Exists(sDomainName As String) As Boolean
If mdicWebsites.Exists(sDomainName) Then
Exists = True
Else
Exists = GetWebsite(sDomainName)
End If
End Function
Whether you're using the (now old fashioned) Collection object or the (new and fast)
Dictionary object, your client code is identical, as the following fragment shows:
Private Sub cboDomainName_Click()
If moWebsites.Exists(cboDomainName.Text) Then
Set moWebsite = moWebsites.Website(cboDomainName.Text)
End If
End Sub

6. Implement a Fast Reset Method within a Form


There are many occasions where you need to reset the controls on a form, either in
readiness for a new record, or perhaps when a record has been displayed and the user
clicks the button to enter a new record.
You could, of course, reference each control on the form individually to reset its display.
However, I tend to whip through every control on the form and perform the same
function on each. Of course, not all controls have the same properties, so I preface my
procedure with an On Error Resume Next statement that tells my code to ignore any errors
and continue with the next line of code:
On Error Resume Next
For Each oControl In Controls
oControl.Text = "" 'mainly for text box controls
oControl.ListIndex = -1 'reset combo and list boxes.
Next
7. Beware O'Reilly, O'Malley, and It's
You've spent the last 17 years writing your new application and it works like a dream.
You've got error handling coming out of your ears, belts, and braces. You've tested the
application till you were blue in the face. Then, first day out in the field, a user contacts
you and says, "It won't let me save this record."
Huh? What could be wrong? It can't be your code. No way!
You hunt through the code, and eventually find out from the user that the error message
was a SQL syntax error that only reared its ugly head when the customers name was
O'SomethingOrOther. Ahha!

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Look at this embedded SQL Script:


sSQL = "UPDATE Customers Set CustName = '" & sCustName & _
"' WHERE CustID = " & custID"
Given a customer name of "Lomax" and an ID of 100, then the string sSQL will contain a
perfectly valid
"UPDATE Customers Set CustName = 'Lomax' WHERE CustID = 100"
However, change the customer name to "O'Reilly" and you generate a SQL syntax error:
"UPDATE Customers Set CustName = 'O'Reilly' WHERE CustID = 100"
The "O" is the string variable for CustName, but "Reilly" is a string that has no beginning
(or is it the "O" that has no end? Hmmm..). So what's the answer? Well, there are a few
ways of sorting out this little problem. One of the easiest is to replace all single quotation
marks ("'") within strings with an apostrophe ("`" ). There are several ways to do this, but
here's my favorite (for pre-VB6 code):
Mid(CustName, Instr(CustName, 1, "'"), 1) = "`"
This code fragment makes unusual use of Mid on the left side of the statement. It replaces
the single character "'" with the "`" character. However, this single line of code can only
replace the first instance of a single quotation mark ("'") in a string. To replace all
instances, you'd have to implement a Do While loop like the following:
Do While Instr(CustName,1,"'") > 1
Mid(CustName, Instr(CustName, 1, "'"), 1) = "`"
Loop
If you're using VB6, you can use the new Replace function to substitute a single line of
code for the code block:
CustName = Replace(CustName, "'", "`")
You'll also find that providing users with large, multi-line text boxes for entering free
format text provides opportunities for them to enter words like "It's", "They're", and other
apostrophe-containing, SQL script-breaking words. So be sure to remove the apostrophes
here, too.
8. Populate Classes in One Go
I've called this a mass assignation function. What's a mass assignation function? Let's say
you have a collection class that contains 20 properties, which you populate by reading
data from a database. You have a procedure that opens the database, creates a recordset,
and then assigns the values from the recordset to each of the relevant properties in the
class, something like this:
oClass.EmployeeNo = rsRecordset!EmpNo
oClass.FirstName = rsRecordset!FirstName
Etc...
Using this method, you are calling the Property Let procedure of each property. If there is
validation code within the Property Let procedure, this must execute too, most likely on
data that has been validated before being saved in the database. A more efficient method
of population is to create a function within the collection class, like this:
Friend Function Initialize(sEmpNo as String, _
sFirstName as String ...etc) As Boolean
msEmpNo = sEmpNo
msFirstName = sFirstName
...Etc...
This single function assigns all the values for the object in one go by assigning the values
directly to the local variables, thus bypassing the Property Let procedures and the redundant

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

validation code. You can therefore pass all the values to populate the object in one go:
If oClass.Initialise( rsRecordset!EmpNo, _
rsRecordset!FirstName, _
etc...) Then
Of course you should only use this method within a class module - never from outside,
and you should only employ this against data that you're certain has already been
validated. You will find that a mass assignation function will dramatically improve the
performance of your collection classes.
9. Using API Calls to Create a Software Timer Class
You can create your own timer class without needing a form and a Timer control present.
This solution is ideal for a remote server application where you don't want to be cluttering
up the server with forms. This example also shows how callback functions and the
AddressOf operator are used.
The following code forms two separate projects. The first is the automation server; this
consists of a class module (clsRemTimer) and a code module. The code module is
necessary to provide a callback procedure for the API functions used to initiate and
destroy the Windows Timer.
TimerServer.vbp - clsRemTimer.cls
Option Explicit

Public Event Timer()


Private blnEnabled As Boolean
Private lTimerID As Long
Private lInterval As Long

Public Property Let Interval(lVal As Long)


lInterval = lVal
End Property
Public Property Get Interval() As Long
Interval = lInterval
End Property
Public Property Let Enabled(blnVal As Boolean)
If blnVal = False Then
StopTimer
Else
StartTimer
End If
End Property

Private Function StartTimer() As Boolean

If Not blnEnabled Then


lTimerID = TimerStart(Me, lInterval)
If lTimerID = 0 Then
Err.Raise 60000 + vbObjectError, "clsTimer", "Could not start Timer"
End If
blnEnabled = True
End If
StartTimer = True

End Function

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Private Function StopTimer() As Boolean

If blnEnabled Then
lTimerID = TimerStop
If lTimerID = 0 Then
Err.Raise 60001 + vbObjectError, "clsTimer", "Could not stop Timer"
End If
blnEnabled = False
End If
StopTimer = False

End Function

Friend Function RaiseTimerEvent()

RaiseEvent Timer

End Function

Private Sub Class_Terminate()


Call TimerStop
End Sub
TimerServer.vbp - modTimer.bas
Option Explicit

Private oTimer As clsRemTimer


Private lTimerID As Long

Declare Function SetTimer Lib "user32" _


(ByVal hwnd As Long, _
ByVal nIDEvent As Long, _
ByVal uElapse As Long, _
ByVal lpTimerFunc As Long) As Long

Declare Function KillTimer Lib "user32" _


(ByVal hwnd As Long, _
ByVal nIDEvent As Long) As Long

Public Sub TimerCallBack(ByVal hwnd As Long, _


ByVal uMsg As Long, _
ByVal idEvent As Long, _
ByVal dwTime As Long)

oTimer.RaiseTimerEvent

End Sub

Public Function TimerStart(ByRef oTmr As clsRemTimer, _


lInterval As Long) As Long
Set oTimer = oTmr
lTimerID = SetTimer(0, 0, lInterval, AddressOf TimerCallBack)
TimerStart = lTimerID

End Function

Public Function TimerStop() As Long

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

TimerStop = KillTimer(0, lTimerID)


Set oTimer = Nothing
End Function
TimerClient.vbp - frmTimerClient.frm
Finally, here's a sample client, which is simply a form that uses the RemTimer class. Note
that for this to function correctly, the client must declare the instance of the RemTimer
class WithEvents. This sample client form contains one text box (named Text1) with its
Locked property set to True and one command button (named Command1).
Option Explicit

Private WithEvents oTimer As TimerServer.clsRemTimer

Private Sub Form_Load()

Set oTimer = New TimerServer.clsRemTimer


oTimer.Interval = 1000
Text1.Text = 0
Command1.Caption = "Start"

End Sub

Private Sub Command1_Click()

On Error GoTo Command1_Err

If Command1.Caption = "Start" Then


oTimer.Enabled = True
Command1.Caption = "Stop"
Else
oTimer.Enabled = False
Command1.Caption = "Start"
End If
Exit Sub

Command1_Err:
MsgBox Err.Description

End Sub

Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)


Set oTimer = Nothing
End Sub

Private Sub oTimer_Timer()

Text1.Text = Text1.Text + 1

End Sub
10. Use a Count Property to Populate a Class
One problem you always have when architecting a class hierarchy is implementing a
method that allows the user of the class to populate the class. For example, you could
implement a Load method that reads all relevant records into the class. However, I prefer
to automatically populate classes using the Count property; this method produces quite

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

elegant code within the client.


Here's an example of a Count property that automatically populates the class:
Public Property Count() As Long
If colMyCollection.Count = 0 Then
LoadRecordsIntoObject
End If
Count = colMyCollection.Count
End Property
Beware, though, you may need to add a filter property to reduce the number of records
automatically loaded into the object. Also, bear in mind that many times these days, the
person writing the objects will not be the person who writes the client-side code.
Therefore, you should make your class objects as easy to use as possible.
Here's an example of some client side code that utilizes the Count property shown above:
Set oDomains = New Domains
oDomains.Type = 1
If oDomains.Count > 0 Then
sNames = oDomains.Names
End If
Set oDomains = Nothing

11. Never Create Custom Subs


There is no operational difference between a sub and a function. A sub is the VB
equivalent of a void function in C and C++. However, if you use a function, you have the
choice of using or ignoring the function's return value.
If you were about to create a Sub because you don't necessarily need a particular return
value, then create a function with a Boolean return value instead. This way, you can
return True if all went well, or False if some error occurred. Your code will be much
more robust as a result.
For example, the following code calls two sub procedures, one after the other:
Call DoFirstRoutine(sVal)
Call SecondRoutine(sOther)
However, the call to the second may actually presuppose on the successful completion of
the first. In this case, it is safer to implement the two routines as functions that return a
Boolean value to denote the success or failure of the procedure, as follows:
If DoFirstRoutine(sVal) Then
SecondRouting sOther
End If
The only time you should need to write code within a sub procedure is as an event
handler; otherwise, use a function and return a value (at least a Boolean). The calling
statement can then choose whether to use or ignore the return value from your function.
12. Never Assume the Unusual Won't Happen
I love this story. It's true (and very recent). One weekend (like most in the summer), we
were off racing and had to stay over in a hotel, part of a major UK chain. The kids are
getting a bit older now, so we decided that they could have their own room. When I'd
filled out the form for the first room and signed in, the lady behind the check-in desk said,
"I'm sorry, but can we put your wife down as the occupier of the second room? It's just
that our system won't let us use the same surname and initial for two records on the same
day. We need a different initial."
Whose bright idea was it that the primary key on the day table should be last name and

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

initial?
OK, perhaps it's unusual, but what about father and adult son, both paying for their rooms
individually but both with the same first name? I knew a family once - a mother, father,
and three sons - all of whom had the same initial and last name! Nora, Norman, Nigel,
Nicholas, and Nathaniel. (OK, so I made the last name up because I could remember it...)
If someone says that something is "unusual," "unlikely," or "improbable," it does not
mean that it's impossible. Unusual, etc., means that it can happen. And as sure as eggs are
eggs, it's going to happen, and it'll happen to your system! Sound programming is not
about gambling on probabilities, its about banking on certainties!
13. Always Use the Event Log to Log Errors
One of the neat things about NT is its event log. Wherever possible (and I can't think of a
situation where it isn't), you should add a line of code (or two) into your error handling
routine to write a line out to the event log.
Even if your application is unfortunate enough to have to run on Windows 9x, you can
still specify that an error line be written to an event log file.
Once you are documenting errors via the event log, you can sit at your computer on the
network and (if you have administrator rights) keep your eye on the event logs of your
users for potential problems. No more having to rely on your users giving you the exact
error message that was shown on the screen.
You will soon find that, with the use of the event log, once difficult to track errors are
easy to trace, and even errors that may be caused by other sometimes "unseen" causes are
pinpointed quickly and accurately.
14. Navigate Effectively with SHIFT+F2 and CTRL+SHIFT+F2
One of the confusing aspects of developing a large project with Visual Basic is that code
snippets tend to be in any of a variety of places, and keeping track of what's where - not
to mention navigating to a particular routine when you want to see it - is often difficult.
However, a little-known keyboard shortcut can help.
Simply highlight the property or procedure name in the code and press SHIFT+F2; you'll
be transported as if by magic to the highlighted property or procedure. (If it's outside the
project, you'll be taken to the Object Browser, where you can obtain further information
about it.)
You can navigate back to where you came from just as easily. This time hit
CTRL+SHIFT+F2 and you're back at your original place. This can save hours and hours
of scrolling and jumping from one module to another in a large project.
15. Fast Combo Population from Objects with VB6
One of the most common tasks we have to undertake when creating a user form is to
provide a dropdown list of values based on property values within one of our objects. A
combo control, for example, might contain the names of all the employees in a certain
division of the company.
One of the most common methods of doing this (until VB6) was to populate the
collection object, then enumerate each object in the collection and assign the particular
value from each object into the combo control. The code looks something like this:
Set oEmployees = New Employees
For i = 1 to oEmployees.Count

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

If oEmployees.Exists(i) Then
Set oEmployee = oEmployees.Employee(i)
cboEmployees.AddItem oEmployee.Name
Set oEmployee = Nothing
End If
Next i
Set oEmployees = Nothing
The amount of processing and (if an object is remote) the massive amount of network
traffic that this simple procedure can create is staggering! So when I found that functions
can return arrays in VB6, I was delighted. This means that you can now execute a simple
SQL query that returns to the client only an array containing the values of that field that
you want to display in the list or combo box. Here's the server side code:
Public Property Get Names() As String()

ReDim sTemp(0) As String


Dim oADORec As ADODB.Recordset
Dim lRecCount As Long
Dim sSQL As String
Dim iCounter As Integer

Set oUtil = New PowerUtils.DBUtils


sSQL = "SELECT SName from Employees"
Set oADORec = oUtil.GetRecordset(sSQL)
If oADORec.RowCount > 0 Then
ReDim sTemp(oADORec.RowCount)
Do While Not oADORec.EOF
sTemp(iCounter) = oADORec!SName
iCounter = iCounter + 1
oADORec.MoveNext
Loop
End If
Set oADORec = Nothing
Set oUtil = Nothing
Names = sTemp

End Property
If you can't get the RowCount property to work with your system, it is faster to execute a
SQL Query that returns the row count and then do a single ReDim of the array, rather than
to use ReDim Preserve.
Here's how the Names property shown above is used at the client end:
Dim sNames() As String
Dim vName As Variant
Dim oEmps As Employees

Set oEmps = New Employees


sNames = oEmps.Names
For Each vName In sNames
cboEmployees.AddItem vName
Next
Set oEmps = Nothing
Note that only an array of string values is passed from the server object to the client.
Therefore, the amount of traffic between the two objects is negligible. Also note that to
assign an array from a function in VB6, your local variable must be dimensioned as a

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

dynamic array.

Ten Tips for Taking the Visual Basic Exams


Obtaining Microsoft certification is a rewarding achievement. However, it can also be a
costly and time-consuming process. (To get the MCSD certification you must pass four
exams. The two Visual Basic exams can be used as core exams or as electives.) In order
to pass the Visual Basic exams and become certified, you need to be proficient with all
aspects of the Visual Basic language. But because Visual Basic is such a rich and
complex language, even the most die-hard Visual Basic developer doesn't use all the
available features.
Although Microsoft doesn't release exam statistics, it's believed that nearly half the
people who take an exam for the first time fail it. Even competent developers fail exams
at times, not due to lack of knowledge or experience, but due to lack of preparation. It's
impossible to teach you everything you need to know to pass the exams in a top ten list
(that's why I wrote MCSD in a Nutshell), but this list will help prepare you for taking the
Visual Basic exams.

1. Know the requirements of each of the exams. It seems trivial, but many
developers don't consider all of the objectives listed for an exam, and therefore
they don't prepare accordingly. If a topic is listed in the objectives for an exam,
you are almost assured of encountering one or more questions related to the topic.
As a result, if you don't understand the topic you will probably miss one or more
questions. For example, you may not use the TreeView control in your projects,
but you can count on at least one question pertaining to the TreeView control, and
probably more. If you don't spend a few minutes learning the basics of the
TreeView control prior to taking an exam, you're going into the battle without
your guns fully loaded. You can obtain the latest exam objectives at Microsoft's
Certification site.

2. Be familiar with all of Microsoft's technologies. Back in the days of Visual


Basic 3, the exam (there was only one at the time) focused on your programming
abilities: Could you create a For.Next loop? Could you define a Select Case
construct? However, times have changed. The exams now make you demonstrate
a working knowledge of Microsoft technologies. You can actually write pretty
poor code and still be certified, as long as you understand Microsoft technologies.
It's doubtful that you use all of Microsoft's various technologies, such as for
creating ActiveX controls or ActiveX documents, but you must understand all of
the technologies in order to pass the exams. For example, ActiveX documents
have long been considered a solution looking for a problem, and very few people
use them. However, there are a surprising number of questions related to creating
ActiveX documents on both exams. If you don't understand the difference
between in-process .DLLs and out-of-process .EXEs, you're in trouble. Before
taking the exams, make sure you understand COM (including early and late
binding and reference counting), and creating and testing ActiveX code
components, controls, and user documents. Also make sure you have some

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

understanding of ADO (though more is required for the Distributed exam). The
more you understand about the implementation and coordination of the Microsoft
technologies, the better chance you have of passing the exams.
3. Learn the Internet programming controls. Not everyone programs for the
Internet, and of those that do, not everyone uses Visual Basic's intrinsic tools such
as the Internet Transfer control, the Winsock control, or DHTML. However, you
have to understand Visual Basic's Internet programming functionality in order to
pass the exams. The good news is you don't have to be an expert with these tools,
but you do need to have a working knowledge. Before taking an exam, make sure
you understand how to perform the basic tasks with each of the Internet controls.
This includes being able to browse Web pages using the WebBrowser control,
retrieve files via HTTP and FTP using the Internet Transfer control, creating peer-
to-peer and client/server applications that communicate via the Winsock control.
4. Understand package and deployment. This may seem like a small subject to
earn a right on the top ten list, (after all it's handled by a simple wizard), but let
me tell you: become an expert at packing and deploying solutions and you will
dramatically improve your performance on both exams. When I first took the
exams in beta, I was unpleasantly surprised at how many questions regarding
package and deployment appeared on both exams. I took the exams once again
after they were released to see how things changed so that I could keep my
material accurate. What I found is that both exams were still laden with package
and deployment questions. I personally think that much of the material is more
suited to the Distributed exam, but the reality is that both exams have include
many questions on package and deployment; if you don't fully understand
package and deployment (.CAB files, Internet deployment, and installation script
files, for example), your chances of passing either exam are greatly reduced.
5. Study for the exams. If you had to pay over $100 for each exam you took in high
school, would you have studied more? Regardless of how proficient you are with
Visual Basic, your chances of passing an exam are greatly increased if you simply
spend some time studying. Remember, the exams are designed to be difficult.
Studying gets your brain in a test-taking mode, and helps to refresh you on
concepts that you may not use all the time. If you've been programming
applications with Visual Basic for some time, you can probably create truly robust
applications quite easily. However, how much of your coding is 'raw' coding
versus cutting and pasting from a vast library of routines? You may have a great
class that encapsulates all of the functionality for working with the ListView
control, but if you can't remember the syntax to add items and sub items via code,
you're going to be stuck if you encounter such a question on the exam. Taking and
passing a Microsoft exam is an investment in your future, treat it as such as
devote some time to studying before taking an exam.
6. Take a Practice Exam. If you've never taken a Microsoft exam, you should
download a practice exam from Microsoft's Web site. The practice exam won't
teach you want you need to know, but it will help you get familiar with
Microsoft's testing software and approach, which is a big plus in my opinion.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

When you take an exam, you want to focus your efforts on answering questions,
not on getting comfortable with the testing software. I highly recommend
purchasing a Transcender practice test for the exam you want to take, in addition
to whatever other study materials you may use (<shameless_plug>MCSD in a
Nutshell<shameless_plug>). The Transcender exams won't teach you all you need
to know, but they do an excellent job in preparing you for the types of questions
you will be asked, and in the manner in which the questions appear. When you
first sit down to take an exam, you are given the opportunity to take a practice
exam. The time used to take the practice exam does not count against your
allotted time for the real exam, so you should go ahead and take the practice exam
to get acquainted (or reacquainted) with the testing software.

7. Read questions completely, and read them more than once. Know this: The
questions on the exams are worded to deliberately bait you into making mistakes.
Questions are often cleverly worded such that the question is actually asking the
opposite of what you think it is. In these cases, it's not uncommon for the first
answer to appear to be the correct answer, when in reality the correct answer
appears later in the list. If you read the question wrong the first time (which is
very easy to do), you'll formulate the answer in your head, see the answer listed as
the first choice, answer the question, and move on. There are so many questions
worded in this manner that you should read each and every one at least twice,
regardless of how obvious the answer may be to you. The second time you read
the question, ask yourself "is this question really asking what I think it's asking?
Is it asking the opposite of what I think it's asking? Is it actually asking something
altogether different than what I thought the first time I read it?" If you do this for
all questions (it takes only a second or so to do per question), you will greatly
reduce the amount of mistakes you make on an exam.

8. Use the Marking feature of the exam to your advantage. Microsoft's testing
software has a Marking feature that lets you flag questions for easy review at a
later time. If you're stuck on a question, select your best-guess answer, use the
Marking feature to flag the question, and then move on to the next question.
When you've gone through all of the questions, you'll get a window that shows all
of the test question numbers, with those you've marked highlighted. To review a
question, double-click the question number. During this review phase, choose
your best answer and unmark the question. If at all possible, you want to finish
the exam with no questions marked so that you know you revisited all of the
questions that gave you trouble. The reason that you make a best guess at the time
you mark the question is so that, in the event you do run out of time, you have at
least given an answer; by not answering a question, you're assured of getting it
wrong.

9. Use all of the time you are given. You are given a finite amount of time to
complete an exam. If you're prepared, the time will be more than adequate, and
you'll be left with some time to spare. Use all of the time allotted. If you've
answered all of the questions, review any questions you may have marked using
the exam software's Marking feature. If you've already reviewed the marked

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

questions, review all of the questions from the beginning. During this stage, there
are really two key things to keep in mind. The first is that it's possible you may
have encountered a question later in the exam that gave you enough information
to more accurately answer the question you're reviewing. The second thing to
keep in mind is the idea of rereading a question from the perspective of "did I
understand the question correctly when I answered it?" Sometimes coming back
to a question allows your mind to process the information and put you in a better
state to give a correct answer. If the time is available, use it to its full capacity.
10. Use Visual Basic. Lately, there's a lot of talk about 'paper MCSEs'. A paper
MCSE or paper MCSD is someone who has obtained certification, but doesn't
have the skills to back it up. This phenomenon has proliferated in a large part due
to 'brain dumps,' Web sites where users post questions and answers that they
remember after taking the exams. This information, coupled with good study
materials, such as study guides and practice tests, has allowed some people to
pass tests on subjects in which they really aren't proficient. Unfortunately, this has
negative consequences in many ways. First and foremost, it dilutes the value of
certification for those who have actually earned it. Microsoft recognizes this, and
as a result they are changing the way they administer exams. If you've taken the
Solutions Architecture exam, you've seen firsthand the results of these changes;
it's almost impossible to pass this exam without two years minimum of real-world
experience. Passing an exam without having real experience in the subject is very,
very difficult to do--but not impossible. However, certification has little value if
you can't walk the walk. In addition, the exams are evolving, and I believe that
soon you simply won't be able to obtain certification if you don't know your stuff.
If you're interested in getting certified in Visual Basic, the best way to get there is
to build applications using Visual Basic.

Executing Common SQL Coding Tasks Using Function


Calls

One of the great things about RDMS (Relational Database Management Systems) is its
unmatched flexibility in addressing a number of different work requirements. This article
shows examples of several common work requirements that are answered using function
calls. A function is a special single-word command in SQL that returns a single value.
The value of a function can be determined by input parameters, such as a function that
averages a list of database values. But many functions do not use any type of input
parameter, such as the function that returns the current system time, CURRENT_TIME.
The database vendor implementations shown in examples below (Microsoft SQL Server,
MySQL, Oracle, and PostgreSQL) are discussed in our upcoming book SQL in a
Nutshell. There are a great many function calls that are universally supported by the
ANSI (American National Standards Institute) standard and all the database vendors. For
example, most vendors support the commonly used aggregate functions of SUM, AVG,

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

MIN, and MAX. These functions extract summary value, average value, and minimum or
maximum value from a column or an expression, respectively. There are also a whole
variety of functions that are not universally supported, such as RPAD and LPAD or
SUBSTRING versus SUBSTR.

Although this article discusses database implementations by Microsoft SQL Server,


MySQL, Oracle, and PostgreSQL, this information represents just the tip of the iceberg
on how business operations accomplish common, everyday SQL coding tasks using
functions calls. As you will see, these functions can vary widely.

Date Operations
This first set of examples show how to query the database for common date-processing
operations using functions. To get the current date and time:
Microsoft SQL Server
SELECT GETDATE()
GO
MySQL [retrieving the date but not time]
SELECT CURDATE();
MySQL [retrieving date and time]
SELECT NOW();
Oracle
SELECT SYSDATE
FROM dual;
PostgreSQL
SELECT CURRENT_DATE;
As the examples illustrate, each vendor retrieves the current date and time differently
using its own proprietary function calls. Microsoft SQL Server uses a SELECT statement
calling the GETDATE() function. MySQL has two different function calls: CURDATE() and
NOW(). The former retrieves the date without time; the latter retrieves date and time.
Oracle uses the SYSDATE function call. And PostgreSQL uses the SQL99 CURRENT_DATE
function call. Note that for all of these function calls, no passed parameters are needed.
These next examples show how to find out what day a given date falls on:
Microsoft SQL Server
SELECT DATEPART(dw, GETDATE())
GO
MySQL
SELECT DAYNAME(CURDATE());
Oracle
SELECT TO_CHAR(SYSDATE,'Day')
FROM dual;
PostgreSQL
SELECT DATE_PART('dow', date 'now');
Microsoft SQL Server uses the DATEPART function call using the syntax
DATEPART(datetype, date_expression). This function requires the type of date (month, day,
week, day of week, and so on), as the first argument, and the date expression (either a
column containing a date or an actual date value), as the second part. MySQL offers the
DAYNAME(date_expression) as its function of choice for finding the day of the week for a
given date value. Oracle requires that the date be converted into a character value using
TO_CHAR, but allows the application of a format mask that returns the data of the week

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

value. Conversions of this type in Oracle follow the syntax TO_CHAR(conversion_expression,


'datetype'). In this case, TO_CHAR can be used to convert any other datatype to character
datatype, including INT and DATE datatypes. PostgreSQL accomplishes date conversion
using the DATE_PART function to extract the day of the week from the date expression.
The syntax is DATE_PART('text', timestamp), where text defines how the date is returned (in
our example, as a day of the week), and timestamp defines the date expression.
Sometimes an application needs to know how far two dates are from one another. To
determine how far away a date is from the current date (or any other date for that matter),
either in the future or in the past, use these examples:
Microsoft SQL Server
SELECT DATEDIFF(dd, '1/1/01', GETDATE())
GO
MySQL
SELECT FROM_DAYS(TO_DAYS(CURDATE()) -
TO_DAYS('2001-11-25'));
Oracle
SELECT TO_DATE('25-Nov-2000','dd-mon-yyyy') -
TO_DATE('25-Aug-1969','dd-mon-yyyy')
FROM dual;
PostgreSQL
SELECT AGE(CURRENT_DATE, '25-Aug-1969');
Measuring the time span between two dates is a procedure best left to procedure calls.
But again, the syntax varies widely between the vendors. Microsoft uses the DATEDIFF
function to measure the time span between two dates (in the example, between January 1,
2001 and today's date). The syntax is DATEDIFF(datetype, start_date, end_date), where
datetype is a code representing how the time span should be represented (days, weeks,
months, and so on), the start_date is the date to measure from, and the end_date is the date to
measure to. MySQL must use the somewhat more cumbersome FROM_DAYS and
TO_DAYS functions in a nested format to tell the time span between two dates. Oracle
very neatly allows date addition and subtraction. The only reason the TO_DATE function
is even needed is that the operation is being performed on character strings. If the
operation were performed against two columns of DATE datatype, then no TO_DATE
conversion would be necessary and the subtraction operation would act directly on the
date expression. PostgreSQL has a cool function called AGE(start_date, end_date) that tells
the time span between two passed dates as parameters.
It is common procedure to retrieve a date in a different format mask (Mon, DD, YYYY;
mm/dd/yy; dd/mm/yy; etc.). Here are some examples:
Microsoft SQL Server
SELECT CONVERT(VARCHAR(11), GETDATE(), 102)
GO
MySQL
SELECT DATE_FORMAT( "2001-11-25", "%M %e, %Y");
Oracle
SELECT TO_CHAR(SYSDATE,'dd-Mon-yyyy hh:mi:ss PM')
FROM dual;
PostgreSQL
SELECT TO_CHAR (timestamp(CURRENT_DATE),
'dd-Mon-yyyy hh:mi:ss PM');

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

In these examples, the vendors use specialized function calls to retrieve date expressions
in a specific format. Microsoft SQL Server uses the CONVERT function (though CAST
could also be used) in the syntax of CONVERT(convert_to_datatype, source_expression,
date_format), where the convert_to_datatype is the datatype to return in the query, the
source_expression is the source that will be converted, and the date_format is a set of codes
that Microsoft has set aside for specific date format masks. MySQL uses the
DATE_FORMAT function in the syntax of DATE_FORMAT(source_expression, date_format).
Oracle uses TO_CHAR, as shown earlier, in the syntax of TO_CHAR(source_expression,
date_format). PostgreSQL also uses TO_CHAR, though somewhat differently in that the
source_expression must be enclosed within the time-stamp subfunction, as shown in the
example above.
String Operations
Often, an application may need to find one string within another string. This is one way
of performing this operation across the different vendors:
Microsoft SQL Server
SELECT CHARINDEX('eat', 'great')
GO
MySQL
SELECT POSITION('eat' IN 'great');
Oracle
SELECT INSTR('Great','eat') FROM dual;
PostgreSQL
SELECT POSITION('eat' IN 'great');
Microsoft SQL Server uses its own function, CHARINDEX, to extract values from other
strings. In this example, it will return the starting position of one string, 'eat,' within
another, 'great.' The syntax is CHARINDEX(search_string, searched_string, [starting_position]).
MySQL and PostgreSQL both accomplish a similar operation using the POSITION
function, showing where 'eat' occurs within 'great.' Oracle uses the INSTR function,
although the order of the passed parameters are reversed. Unlike the other vendors,
Oracle requires the searched_string first, then the search_string.
It is often necessary to trim trailing and leading spaces from an expression in an SQL
operation:
Microsoft SQL Server
SELECT LTRIM(' sql_in_a_nutshell'),
SELECT RTRIM('sql_in_a_nutshell '),
SELECT LTRIM(RTRIM(' sql_in_a_nutshell ')
GO
MySQL
SELECT LTRIM(' sql_in_a_nutshell'),
SELECT RTRIM('sql_in_a_nutshell '),
SELECT TRIM(' sql_in_a_nutshell '),
SELECT TRIM(BOTH FROM ' sql_in_a_nutshell ');
Oracle
SELECT LTRIM(' sql_in_a_nutshell'),
SELECT RTRIM('sql_in_a_nutshell '),
TRIM(' sql_in_a_nutshell ')
FROM dual;
PostgreSQL
SELECT TRIM(LEADING FROM ' sql_in_a_nutshell'),

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

TRIM(TRAILING FROM 'sql_in_a_nutshell '),


TRIM(BOTH FROM ' sql_in_a_nutshell ');
Microsoft SQL Server uses the LTRIM and RTRIM functions to remove spaces from the
left or right side of an expression, respectively. When trimming spaces on both sides of
an expression in Microsoft SQL Server, the LTRIM function must encapsulate the RTRIM
function (or vice versa). MySQL and Oracle both use LTRIM and RTRIM, but differ from
SQL Server in that spaces can be trimmed from both sides of an expression with the TRIM
function. MySQL also allows TRIM with the BOTH operator to indicate that both left and
right sides of the expression should be trimmed. PostgreSQL uses only the TRIM function
and controls whether the left, right, or both sides should be trimmed using the LEADING,
TRAILING, and BOTH operators, as shown in the example above.
The opposite of trimming spaces is to pad them into an expression. To pad in x number of
trailing or leading spaces with the various vendors:
Microsoft SQL Server
Not supported
MySQL
SELECT LPAD('sql_in_a_nutshell', 20, ' '),
RPAD('sql_in_a_nutshell', 20, ' ');
Oracle
SELECT LPAD(('sql_in_a_nutshell', 20, ' '),
RPAD(('sql_in_a_nutshell', 20, ' ')
FROM dual;
PostgreSQL
SELECT LPAD('sql_in_a_nutshell', 20, ' '),
RPAD('sql_in_a_nutshell', 20, ' ');
In this example, the supporting vendors all use LPAD to insert spaces (or a character
expression) on the left side of a string expression and RPAD to put them on the right. The
syntax for MySQL, Oracle, and PostgreSQL is xPAD('string_expression1', length,
'string_expression2'), where string_expression1 is the string to have characters padded, length is
the total length of the string, and string_expression2 is the characters to pad out.
An operation similar to pad is to substitute characters within a string with other
characters:
Microsoft SQL Server [returns 'wabbit_hunting_season']
SELECT STUFF('wabbit_season', 7, 1, '_hunting_')
GO
MySQL [returns 'wabbit_hunting_season']
SELECT
REPLACE('wabbit_season','it_','it_hunting_');
Oracle [returns 'wabbit_hunting_season']
SELECT
REPLACE('wabbit_season','it_','it_hunting_')
FROM dual;
PostgreSQL
SELECT TRANSLATE('wabbit_season','it_','it_hunting_');
Microsoft SQL Server uses the STUFF function to overwrite existing characters. Using
this syntax, STUFF(string_expression, start, length, replacement_characters), string_expression is the
string that will have characters substituted, start is the starting position, length is the
number of characters in the string that are substituted, and replacement_characters are the
new characters interjected into the string. MySQL and Oracle both use the function call

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

REPLACE, using the syntax REPLACE(string_expression, search_string, replacement_string), where


every incidence of search_string found in the string_expression will be replaced with
replacement_string. PostgreSQL uses the TRANSLATE function as a synonym of REPLACE.
Many times, a SQL statement must retrieve only a portion of a string. The following
examples show how to extract 'duck_season' from the string 'wabbit_duck_season' for
each vendor:
Microsoft SQL Server
SELECT SUBSTRING('wabbit_duck_season', 7, 11)
GO
MySQL
SELECT
SUBSTRING('wabbit_duck_season', 7, 11);
Oracle
SELECT SUBSTR('wabbit_duck_season', 7, 11)
FROM dual;
PostgreSQL
SELECT
SUBSTR('wabbit_duck_season', 7, 11);
In each example, the syntax for SUBSTRING (or SUBSTR) is essentially the same:
SUBSTRING(string_expression, start, length), where string_expression is the expression or column
to be searched, start is an integer telling the starting position, and length is an integer
telling the database how many characters to extract.
Some vendors allow function calls that can structure an IF, THEN, ELSE result set within
the query:
Microsoft SQL Server
SELECT CASE
WHEN foo = 'hi' THEN 'there'
WHEN foo = 'good' THEN 'bye'
ELSE 'default'
END
FROM t2
GO
MySQL
N/A
Oracle
SELECT DECODE
(payments_info,'CR','Credit','DB','Debit', null)
FROM dual;
PostgreSQL
SELECT CASE
WHEN foo = 'hi' THEN 'there'
WHEN foo = 'good' THEN 'bye'
ELSE 'default'
END
FROM t2;
Microsoft SQL Server and PostgreSQL support the powerful ANSI SQL command CASE.
CASE has two usages: simple and searched. Simple CASE expression compares one value,
the input_value, with a list of other values and returns a result associated with the first
matching value. Searched CASE expressions allow the analysis of several logical
conditions and returns a result associated with the first one that is true.
Simple comparison operation

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

CASE input_value
WHEN when_condition THEN resulting_value
[...n]
[ELSE else_result_value]
END
Boolean searched operation
CASE
WHEN Boolean_condition THEN resulting_value
[...n]
[ELSE else_result_expression]
END
In the simple CASE function, the input_value is evaluated against each WHEN clause. The
resulting_value is returned for the first TRUE instance of input_value = when_condition. If no
when_condition evaluates as TRUE, the else_result_value is returned. If no else_result_value is
specified, then NULL is returned.
In the more elaborate Boolean searched operation, the structure is essentially the same as
the simple comparison operation except that each WHEN clause has its own Boolean
comparison operation. In either usage, multiple WHEN clauses are used, although only
one ELSE clause is necessary.
Oracle supports its own extremely powerful IF, THEN, ELSE function call: DECODE.
DECODE has a unique syntax along these lines, DECODE(search_expression, search1, replace1,
search[,.n], replace,.n], default), where search_expression is the string to be searched;
subsequently each search string is paired with a replacement string. If a search is
successful, the corresponding result is returned. In our example, when returning a result
set from the payments_info column, any incident of 'CR' will be replaced with 'Credit,' any
instance of 'DB' will be replace with 'Debit,' and any other values will be replaced with a
default value of Null.
Nulls Operations
Nulls are sometimes tricky business. Sometimes a company process, such as a query or
other data manipulation statement, must explicitly handle NULLs. These examples show
how to return a value specified when a field or result is null:
Microsoft SQL Server
SELECT ISNULL(foo, 'Value is Null')
GO
MySQL
N/A
Oracle
SELECT NVL(foo,'Value is Null')
FROM dual;
PostgreSQL [allows you to write a user-defined function to handle this feature]
N/A
Microsoft SQL Server uses the
ISNULL function following the syntax
ISNULL(string_expression, replacement_value), where string_expression is a string or column
being searched and replacement value is the value returned if string_expression is NULL. Oracle
uses a different function, NVL, but follows an almost identical syntax.
Alternately, there may be times when a NULL value is needed if a field contains a
specific value:
Microsoft SQL Server [returns NULL when foo equates to 'Wabbits!']

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

SELECT NULLIF(foo, 'Wabbits!')


GO
MySQL
N/A
Oracle
SELECT DECODE(foo,'Wabbits!',NULL)
FROM dual;
PostgreSQL
SELECT NULLIF(foo, 'Wabbits!');
Aside from using CASE or DECODE to solve this problem, Microsoft and PostgreSQL
allow use of the NULLIF function. The syntax is NULLIF(expression1, expression2), which tells
the database that if expression1 equals expression2, then returns a NULL value.
Summary
There are a great many function calls that are universally supported by the ANSI standard
and all the database vendors. This article has shown a variety of useful function calls
available in database implementations by Microsoft SQL Server, MySQL, Oracle, and
PostgreSQL. For more details on ANSI standard functions, check out our book, SQL in a
Nutshell.

Kevin Kline serves as the lead information architect for shared information services at
Deloitte & Touche LLP. In addition to coauthoring SQL in a Nutshell, Kevin is the
coauthor of Transact-SQL Programming, also for O'Reilly & Associates. He coauthored
Professional SQL Server 6.5 Administration (WROX Press), and is also the author of
Oracle CDE: Reference and User's Guide (Butterworth-Heinemann). He can be reached
at kekline@compuserve.com.
(Editor's Note: We tried to link directly to Kevin's book published by BH, however the BH
site does not produce urls that work when copied.)
Daniel Kline is an assistant professor of English at the University of Alaska, Anchorage,
where he specializes in medieval literature, literary and cultural theory, and computer-
assisted pedagogy. Dan's technical specialty is in HTML and Web applications for higher
education.

Win32 API Programming with Visual Basic

Chapter 6
Strings
The subject of strings can be quite confusing, but this confusion tends to
disappear with some careful attention to detail (as is usually the case). The
main problem is that the term string is used in at least two different ways
in Visual Basic!

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Just what is a string in Visual Basic? According to the VB documentation,


it is:
A data type consisting of a sequence of contiguous
characters that represent the characters themselves rather
than their numeric values.
Huh?
It seems to me that Microsoft is trying to say that the underlying set for the
String data type is the set of finite-length sequences of characters. For
Visual Basic, all characters are represented by 2-byte Unicode integers.
Put another way, VB uses Unicode to represent the characters in a string.
For instance, the ASCII representation for the character h is &H68, so the
Unicode representation is &H0068, appearing in memory as 68 00.
Thus, the string "help" is represented as:
00 68 00 65 00 6C 00 70
Note, however, that because words are written with their bytes reversed in
memory, the string "help" appears in memory as:
68 00 65 00 6C 00 70 00
This is fine, but it is definitely not how we should think of strings in VB
programming. To avoid any possibility of ambiguity, we will refer to this
type of object as a Unicode character array which is, after all, precisely
what it is! This also helps distinguish it from an ANSI character array,
that is, an array of characters represented using single-byte ANSI
character codes.
Here is the key to understanding strings: when we write the code:
Dim str As String
str = "help"
we are not defining a Unicode character array per se. We are defining a
member of a data type called BSTR, which is short for Basic String. A
BSTR is, in fact, a pointer to a null-terminated Unicode character array
that is preceeded by a 4-byte length field. We had better elaborate on this.

The BSTR
Actually, the VB string data type defined by:
Dim str As String
underwent a radical change between versions 3 and 4 of Visual Basic, due
in part to an effort to make the type more compatible with the Win32
operating system.
Just for comparison (and to show that we are more fortunate now), Figure
6-1 shows the format for the VB string data type under Visual Basic 3,
called an HLSTR (High-Level String).
The rather complex HLSTR format starts with a pointer to a string
descriptor, which contains the 2-byte length of the string along with
another pointer to the character array, which is in ANSI format (one byte
per character).

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

With respect to the Win32 API, this string format is a nightmare.


Beginning with Visual Basic 4, the VB string data type changed. The new
data type, called a BSTR, is shown in Figure 6-2.
This data type is actually defined in the OLE 2.0 specifications; that is, it
is part of Microsoft's ActiveX specification.
There are several important things to note about the BSTR data type.
• The BSTR is the actual pointer variable. It has size 32 bits, like all
pointers, and points to a Unicode character array. Thus, a Unicode
character array and a BSTR are not the same thing. It is correct to
refer to a BSTR as a string (or VB string) but, unfortunately, the
Unicode character array is also often called a string! Hence, we
will not refer to a BSTR simply as a string--we will refer to it by
its unequivocal name--BSTR.
• The Unicode character array that is pointed to by a BSTR must be
preceded by a 4-byte length field and terminated by a single null 2-
byte character (ANSI = 0).

• There may be additional null characters anywhere within the


Unicode character array, so we cannot rely on a null character to
signal the end of the character array. This is why the length field is
vital.
• Again, the pointer points to the beginning of the character array,
not to the 4-byte length field that precedes the array. As we will
see, this is critical to interpreting a BSTR as a VC++-style string.
• The length field contains the number of bytes (not the number of
characters) in the character array, excluding the terminating null
bytes. Since the array is Unicode, the character count is one-half
the byte count.
We should emphasize that an embedded null Unicode character is a 16-bit
0, not an 8-bit 0. Watch out for this when testing for null characters in
Unicode arrays.
Note that it is common practice to speak of "the BSTR `help'" or to say
that a BSTR may contain embedded null characters when what is really
being referred to is the character array pointed to by the BSTR.
Because a BSTR may contain embedded null characters, the terminating
null is not of much use, at least as far as VB is concerned. However, its
presence is extremely important for Win32. The reason is that the Unicode
version of a Win32 string (denoted by LPWSTR) is defined as a pointer to
a null-terminated Unicode character array (which, by the way, is not
allowed to contain embedded null characters).
This makes it clear why BSTR's are null terminated. A BSTR with no
embedded nulls is also an LPWSTR. We will discuss C++ strings in a
moment.
Let us emphasize that code such as:

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Dim str As String


str = "help"
means that str is the name of a BSTR, not a Unicode character array. In
other words, str is the name of the variable that holds the address xxxx, as
shown in Figure 6-2.
Here is a brief experiment we can do to test the fact that a VB string is a
pointer to a character array and not a character array. Consider the
following code, which defines a structure whose members are strings:
Private Type utTest
astring As String
bstring As String
End Type

Dim uTest As utTest


Dim s as String

s = "testing"
uTest.astring = "testing"
uTest.bstring = "testing"

Debug.Print Len(s)
Debug.Print Len(uTest)
The output from this code is:
7
8
In the case of the string variable s, the Len function reports the length of
the character array; in this case there are 7 characters in the character array
`testing'. However, in the case of the structure variable uTest, the Len
function actually reports the length of the structure (in bytes). The return
value of 8 clearly indicates that each of the two BSTRs has length 4. This
is because a BSTR is a pointer!

C-Style LPSTR and LPWSTR Strings


VC++ and Win32 use the string data types LPSTR and LPWSTR.
An LPSTR string is defined as a pointer to a null-terminated ANSI
character array. However, because the only way that we can tell when an
LPSTR string ends is by the location of the terminating null, LPSTRs are
not allowed to contain embedded null characters. Similarly, an LPWSTR
is a pointer to a null-terminated Unicode character set with no embedded
nulls. (The W in LPWSTR stands for Wide, which is Microsoft's way of
saying Unicode.) These string data types are pictured in Figure 6-3.
We will also encounter the data types LPCSTR and LPCWSTR. The
embedded C stands for constant and simply means that an instance of this
data type cannot (and will not) be changed by any API function that uses
this type. Otherwise, an LPCSTR is identical to an LPSTR, and, similarly,
an LPCWSTR is identical to an LPWSTR.
Finally, the generic LPTSTR data type is used in conditional compilation,
just like the TCHAR data type, to cover both ANSI and Unicode in a
single source code. Here are the declarations:

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

#ifdef UNICODE

typedef LPWSTR LPTSTR; // LPTSTR is synonym for LPWSTR


under Unicode
typedef LPCWSTR LPCTSTR; // LPCTSTR is synonym for
LPCWSTR under Unicode

#else

typedef LPSTR LPTSTR; // LPTSTR is synonym for LPSTR under


ANSI
typedef LPCSTR LPCTSTR; // LPTCSTR is synonym for LPCSTR
under ANSI

#endif
Figure 6-4 summarizes the possibilities.
Thus, for instance, LPCTSTR is read long pointer to a constant generic
string.

String Terminology
To avoid any possible confusion, we will use the terms BSTR, Unicode
character array, and ANSI character array. When we do use the term
string, we will modify it by writing VB string (meaning BSTR) or VC++
string (meaning LP??STR). We will avoid using the term string without
some modification.
However, in translating VB documentation, you will see the unqualified
term string used quite often. It falls to you to determine whether the
reference is to a BSTR or a character array.

Tools for Exploring Strings


If we are going to do some exploring, then we will need some tools. We
have already discussed the CopyMemory API function. Let us take a look
at some additional tools for dealing with strings.
The Visual Basic StrConv Function
The StrConv function is used to convert character arrays from one format
to another. Its syntax is:
StrConv(string, conversion, LCID)
where string is a BSTR, conversion is a constant (described later), and
LCID is an optional locale identifier (which we will ignore).
Among the possible constants, and the only ones that interest us, are:
• vbUnicode (which should have been vbToUnicode)

• vbFromUnicode
These constants convert the character array of the BSTR between Unicode
and ANSI.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

But now we have a problem (which really should have been addresed by
the official documentation). There is no such thing as an ANSI BSTR. By
definition, the character array pointed to by a BSTR is a Unicode array.
However, we can image what an ANSI BSTR would be--just replace the
Unicode character array in Figure 6-2 with an ANSI array. We will use the
term ABSTR to stand for ANSI BSTR, but you should keep in mind that
this term will not be officially recognized outside of this book.
We can now say that there are two legal forms for StrConv :
StrConv(aBSTR, vbFromUnicode) ' returns an ABSTR
StrConv(anABSTR, vbUnicode) ' returns a BSTR
The irony is that, in the first case, VB doesn't understand the return value
of its own function! To see this, consider the following code:
s = "help"
Debug.Print s
Debug.Print StrConv(s, vbFromUnicode)
The result is:
help
??
because VB tries to interpret the ABSTR as a BSTR. Look at the
following code:
s = "h" & vbNullChar & "e" & vbNullChar & "l" & vbNullChar & "p"
& vbNullChar
Debug.Print s
Debug.Print StrConv(s, vbFromUnicode)
The output is:
help
help
Here we have tricked VB by padding the original Unicode character array
so that when StrConv does its conversion, the result is an ABSTR that
happens to have a legitimate interpretation as a BSTR!
This shows that the StrConv function doesn't really understand or care
about BSTRs and ABSTRs. It assumes that whatever you feed it is a
pointer to a character array and it blindly does its conversion on that array.
As we will see, many other string functions behave similarly. That is, they
can take a BSTR or an ABSTR--to them it is just a pointer to some null-
terminated array of bytes.
The Len and LenB Functions
Visual Basic has two string-length functions: Len and LenB. Each takes a
BSTR or ABSTR and returns a long. The following code tells all.
s = "help"
Debug.Print Len(s), LenB(s)
Debug.Print Len(StrConv(s, vbFromUnicode)), LenB(StrConv(s,
vbFromUnicode))
The output is:
4 8
2 4
showing that Len returns the number of characters and LenB returns the
number of bytes in the BSTR.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

The Chr, ChrB, and ChrW Functions


These three functions have different input ranges and produce different
outputs. These differences can seem confusing at first--you may have to
read the definitions a few times:
• Chr takes a long value x in the range 0 to 255 and returns a BSTR
of length 1. This one character pointed to by the BSTR has
Unicode code equal to x. (In this case, the Unicode and ANSI
values are actually equal.) Note that, according to the latest
documentation, there is no difference between Chr and Chr$.
• ChrB takes a long value x in the range 0 to 255 and returns an
ABSTR of length 1 (byte). This one byte pointed to by the ABSTR
has ANSI code equal to x.
• ChrW takes a long value x in the range 0 to 65535 and returns a
BSTR of length 1. This one character pointed to by the BSTR has
Unicode code equal to x.

The Asc, AscB, and AscW Functions


These functions are the inverses of the Chr functions. For instance, AscB
takes a single character (byte) ABSTR and returns a byte equal to the
character's ANSI code. To see that the return type is a byte, try running the
code:
Debug.Print VarType(AscB("h")) = vbByte
(The output is True.) It may appear that AscB will accept a BSTR as input,
but in reality, it just takes the first byte in the BSTR.
The Asc function takes a BSTR (but not an ABSTR) and returns an integer
equal to the character's Unicode code.
Null Strings and Null Characters
To its credit, VB does allow null BSTRs. The code:
Dim s As String
s = vbNullString
Debug.Print VarPtr(s)
Debug.Print StrPtr(s)
produces the following output (your address may vary, of course):
1243948
0
This shows that a null BSTR is simply a pointer whose contents are 0.
(We will discuss the meaning of StrPtr in a moment.) In Win32 and
VC++, this is called a null pointer. You can probably see the difference
between vbNullString and vbNullChar at this point. vbNullChar is not a pointer-
-it is a Unicode character whose value is 0. Thus, at the bit level, the
values vbNullString and vbNullChar are identical. However, they are
interpreted differently, so they are in fact different.
It is also important not to confuse a null BSTR with an empty BSTR,
usually denoted by a pair of adjacent quotation marks:

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Dim s As String
Dim t As String
s = vbNullString
t = ""
Unlike a null string, the empty BSTR t is a pointer that points to some
nonzero memory address. At that address resides the terminating null
character for the empty BSTR, and the preceeding length field also
contains a 0.
VarPtr and StrPtr
We have discussed the function VarPtr already, but not in connection with
strings. The functions VarPtr and StrPtr are not documented by Microsoft,
but they can be very useful, so we will use them often, particularly the
VarPtr function.
If var is a variable, we have seen that:
VarPtr(var)
is the address of that variable, returned as a long. If str is a BSTR variable,
then:
StrPtr(str)
gives the contents of the BSTR! These contents are the address of the
Unicode character array pointed to by the BSTR.
Let us elaborate. Figure 6-5 shows a BSTR.
Figure 6-5. A BSTR

The code for this figure is simply:


Dim str As String
str = "help"
Note that the variable str is located at address aaaa and the character array
begins at address xxxx, which is the contents of the pointer variable str.
To see that:
VarPtr = aaaa
StrPtr = xxxx
just run the following code:
Dim lng As Long
Dim i As Integer
Dim s As String
Dim b(1 To 10) As Byte
Dim sp As Long, vp As Long

s = "help"

sp = StrPtr(s)
Debug.Print "StrPtr:" & sp

vp = VarPtr(s)
Debug.Print "VarPtr:" & vp

' Verify that sp = xxxx and vp = aaaa


' by moving the long pointed to by vp (which is xxxx)

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

' to the variable lng and then comparing it to sp


CopyMemory lng, ByVal vp, 4
Debug.Print lng = sp

' To see that sp contains address of char array,


' copy from that address to a byte array and print
' the byte array. We should get "help".
CopyMemory b(1), ByVal sp, 10
For i = 1 To 10
Debug.Print b(i);
Next
The output is:
StrPtr:1836612
VarPtr:1243988
True
104 0 101 0 108 0 112 0 0 0
This shows again that the character array in a BSTR is indeed in Unicode
format. Also, by adding the following lines:
Dim ct As Long
CopyMemory ct, ByVal sp - 4, 4
Debug.Print "Length field: " & ct
just after the lines:
sp = StrPtr(s)
Debug.Print "StrPtr:" & sp
we get the output:
Length field: 8
which shows that the length field does indeed hold the byte count and not
the character count.
As mentioned earlier, if you do not like to use undocumented functions
(and who can blame you for that?), you can use the function rpiVarPtr in
the rpiAPI.dll library on the accompanying CD. You can also simulate
StrPtr as follows:
' Simulate StrPtr
Dim lng As Long
CopyMemory lng, ByVal VarPtr(s), 4
' lng = StrPtr(s)
As we have seen, this code copies the contents of the BSTR pointer, which
is the value of StrPtr, to a long variable lng.

String Conversion by VB
Now we come to the strange story on how VB handles passing BSTRs to
external DLL functions. It doesn't.
As we have seen, VB uses Unicode internally; that is, BSTRs use the
Unicode format. Window NT also uses Unicode as its native character
code. However, Windows 9x does not support Unicode (with some
exceptions). Let's examine the path that is taken by a BSTR argument to
an external DLL function (Win32 API or otherwise).
In an effort to be compatible with Windows 95, VB always (even when
running under Windows NT) creates an ABSTR, converts the BSTR's
Unicode character array to ANSI, and places the converted characters in

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

the ABSTR's character array. VB then passes the ABSTR to the external
function. As we will see, this is true even when calling the Unicode entry
points under Windows NT.
Preparing the BSTR
Before sending a BSTR to an external DLL function, VB creates a new
ABSTR string at a location different from the original BSTR. It then
passes that ABSTR to the DLL function. This duplication/translation
process is pictured in Figure 6-6.
Figure 6-6. Translating a BSTR to an ABSTR

When we first introduced the CopyMemory function, we used it to


demonstrate this Unicode-to-ANSI translation process. But let's do that
again in a different way. The rpiAPI.dll library includes a function called
rpiBSTRtoByteArray, whose purpose is to return the values of VarPtr and
StrPtr on the string that is actually passed to a DLL function. The VB
declaration is as follows.
Public Declare Function rpiBSTRtoByteArray Lib "???\rpiAPI.dll" ( _
ByRef pBSTR As String, _
ByRef bArray As Byte, _
pVarPtr As Long, _
pStrPtr As Long
) As Long
For its first parameter, this function takes as input a BSTR, which is
passed by reference. Hence, the address of the BSTR is passed, not the
address of the character array. (Thus, we are passing a pointer to a pointer
to the character array.)
The second parameter should be set to the first byte of a byte array that the
caller must allocate with enough space to accommodate all of the bytes of
the BSTR. Failing to do so will definitely crash the application.
The last two parameters are OUT parameters, meaning that the caller just
declares a pair of long variables, which the function will fill in. The
pVarPtr variable will be filled by the address of the BSTR, and the pStrPtr
will be filled by the contents of the BSTR (which, as we know, is the
address of the character array) as the DLL function sees it. Thus, we will
be able to get a glimpse of what the DLL is actually passed by VB!
The function returns the length (in bytes) of the original string. Finally, in
order to convince ourselves that everything is working as it should, the
function changes the first character of the original string to an X.
Here is a test run (the function VBGetTarget was discussed in Chapter 3,
API Declarations, under the section "Implementing Indirection in Visual
Basic"):
Sub BSTRTest()

Dim i As Integer
Dim sString As String
Dim bBuf(1 To 10) As Byte

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Dim pVarPtr As Long


Dim pStrPtr As Long
Dim bTarget As Byte
Dim lTarget As Long

sString = "help"

' Print the BSTR's initial address and contents


Debug.Print "VarPtr:" & VarPtr(sString)
Debug.Print "StrPtr:" & StrPtr(sString)

' Call the external function


Debug.Print "Function called. Return value:" & _
rpiBSTRToByteArray(sString, bBuf(1), pVarPtr, pStrPtr)

' Print what the DLL sees, which is the temp ABSTR
' Its address and contents are:
Debug.Print "Address of temp ABSTR as DLL sees it: " & pVarPtr
Debug.Print "Contents of temp ABSTR as DLL sees it: " & pStrPtr

' Print the buffer pointed to by temp ABSTR


Debug.Print "Temp character array: ";
For i = 1 To 10
Debug.Print bBuf(i);
Next
Debug.Print

' Now that we have returned from the DLL function call
' check status of the passed string buffer -- it has been deallocated
VBGetTarget lTarget, pVarPtr, 4
Debug.Print "Contents of temp ABSTR after DLL returns: " & lTarget

' Check the string for altered character


Debug.Print "BSTR is now: " & sString

End Sub
Here is the output:
VarPtr:1242736
StrPtr:2307556
Function called. Return value:4
Address of temp ABSTR as DLL sees it: 1242688
Contents of temp ABSTR as DLL sees it: 1850860
Temp character array: 104 101 108 112 0 0 0 0 0 0
Contents of temp ABSTR after DLL returns: 0
BSTR is now: Xelp
This code first prints the address (VarPtr ) and the contents (StrPtr ) of the
original BSTR as VB sees it. It then calls the function, which fills in the
byte buffer and the OUT parameters. Next, the buffer and OUT
parameters are printed. The important point to note is that the address and
contents of the "string," as returned by the DLL function, are different
than the original values, which indicates that VB has passed a different
object to the DLL. In fact, the buffer is in ANSI format; that is, the object
is an ABSTR.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Next, we print the contents of the passed ABSTR, when the DLL has
returned. This is 0, indicating that the temporary ABSTR has been
deallocated. (It is tempting but not correct to say that the ABSTR is now
the null string--in fact the ABSTR no longer exists!)
Finally, note that I am running this code under Windows NT--the
translation still takes place even though Windows NT supports Unicode.
The Returned BSTR
It is not uncommon for a BSTR that is passed to a DLL function to be
altered and returned to the caller. In fact, this may be the whole purpose of
the function.
Figure 6-7 shows the situation. After the ABSTR is altered by the DLL
function, the translation process is reversed. Thus, the original BSTR str
will now point to a Unicode character array with the output of the API
function. Note, however, that the character array may not be returned to
its original location. For instance, as we will see, the API function
GetWindowText seems to move the array. The point is that we cannot rely
on the contents of the BSTR to remain unchanged, only its address. This
will prove to be an important issue in our discussions later in the chapter.
Figure 6-7. The return translation

What to Call
Since Windows 9x does not implement Unicode API entry points, for
compatibility reasons you will probably want to call only ANSI API entry
points in your applications. For instance, you should call SendMessageA,
not SendMessageW. (Nonetheless, we will do a Unicode entry point
example a little later.)
The Whole String Trip
Let's take a look at the entire round trip that a BSTR takes when passed to
an external DLL.
Assume that we call a DLL function that takes a string parameter and
modifies that string for return. The CharUpper API function is a good
example. This function does an in-place conversion of each character in
the string to uppercase. The VB declaration for the ANSI version is as
follows.
Declare Function CharUpperA Lib "user32" ( _
ByVal lpsz As String _
) As Long
Under Windows 9x
Under Windows 9x, the following happens to the string argument.
Remember that it is the character array pointers that are being passed back
and forth, not the actual character arrays:

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

• The BSTR lpsz is duplicated as an ABSTR by VB, and the


duplicate is passed to the function CharUpperA, which treats it as
an LPSTR.
• This function processes the LPSTR and passes the result to VB.
• VB translates the LPSTR back to a BSTR.
Note that since most API functions (in this case CharUpper) treat BSTRs
as LPSTRs, that is, they ignore the length field, we cannot be certain that
this field will always be accurate. For CharUpper, the length is not
changed, so it should remain correct, but other API functions could
conceivably change the length of the character array. Unless written
specifically for the BSTR format, the function will just null-terminate the
new character array, without updating the length field. Thus, we cannot
rely on the length field to be valid.
Under Windows NT
Under Windows NT, our string argument will go through the following
machinations:
1. The string is translated from a BSTR to an ABSTR by VB and
passed to the function CharUpperA, which treats it as an LPSTR.

2. This function translates the LPSTR to an LPWSTR and passes the


LPWSTR to the Unicode entry point CharUpperW.
3. The Unicode function CharUpperW processes the LPWSTR and
produces an LPWSTR for output, returning it to CharUpperA.
4. The function CharUpperA translates the LPWSTR back to an
LPSTR and passes it to VB, which thinks of it as an ABSTR.

5. VB translates the ABSTR back to a BSTR!

A Unicode Entry Point Example


Under Windows NT, we can call the Unicode entry points and expect to
get something meaningful in return. However, VB still makes the BSTR-
to-ABSTR translations, and we must counteract this translation. Here is
the ANSI version of a call to CharUpperA:
s = "d:\temp"
Debug.Print s
CharUpperA s
Debug.Print s
Under both Windows 9x and Windows NT, the outcome is as expected:
d:\temp
D:\TEMP
Under Windows NT, we might first attempt the Unicode version thusly:
s = "d:\temp"
Debug.Print s
CharUpperW s

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Debug.Print s
but the result is:
d:\temp
d:\temp
Clearly, something is wrong. Incidentally, here is what the documentation
says about errors in the CharUpper function.
There is no indication of success or failure. Failure is rare.
There is no extended error information for this function; do
no [sic] call GetLastError.
Nonetheless, we know that the problem is that VB is making the BSTR-to-
ABSTR translation. So let us try the following code:
s = "d:\temp"
Debug.Print s
s = StrConv(s, vbUnicode)
Debug.Print s
CharUpperW s
Debug.Print s
s = StrConv(s, vbFromUnicode)
Debug.Print s
The output is:
d:\temp
d:\temp
D:\TEMP
D:\TEMP
What we are doing here is compensating for the shrinking of our BSTR to
an ABSTR by expanding it first. Indeed, the first call to the StrConv
function simply takes each byte in its operand and expands it to Unicode
format. It doesn't know or care that the string is already in Unicode format.
Consider, for instance, the first Unicode character d. Its Unicode code is
0064 (in hex), which appears in memory as 64 00. Each byte is translated by
StrConv to Unicode, which results in 0064 0000 (appearing in memory as 64
00 00 00). The effect is to put a null character between each Unicode
character in the original Unicode string.
Now, in preparation for passing the string to CharUpperW, VB takes this
expanded string and converts it from Unicode to ANSI, thus returning it to
its original Unicode state. At this point, CharUpperW can make sense of it
and do the conversion to uppercase. Once the converting string returns
from CharUpperW, VB "translates" the result to Unicode, thus expanding
it with embedded null characters. We must convert the result to ANSI to
remove the supererogatory padding.

Passing Strings to the Win32 API


We can now discuss some of the practical aspects of string passing.
ByVal Versus ByRef
Some authors like to say that the ByVal keyword is overloaded for strings,
meaning that it takes on a different meaning when applied to strings than
when applied to other variables. Frankly, I don't see it. Writing:

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

ByVal str As String


tells VB to pass the contents of the BSTR (actually the ABSTR), which is
the pointer to the character array. Thus, ByVal is acting normally--it just
happens that the content of the BSTR is a pointer to another object, so this
simulates a pass by reference. Similarly:
ByRef str As String
passes the address of the BSTR, as expected.
IN and OUT String Parameters
There are many API functions that require and/or return strings. Almost all
of these functions deal with C-style strings, that is, LPSTRs or LPWSTRs.
Some OLE-related functions do require BSTRs. By way of example, the
following function is part of the Microsoft Web Publishing API. Note that
it uses BSTRs. (Note also that the declaration is kind enough to tell us
which parameters are IN parameters and which are OUT parameters. This
is all too rare.)
HRESULT WpPostFile(
[in] LONG hWnd
[in] BSTR bstrLocalPath
[in, out] LONG * plSiteNameBufLen
[in, out] BSTR bstrSiteName
[in, out] LONG * plDestURLBufLen
[in, out] BSTR bstrDestURL
[in] LONG lFlags
[out, retval] LONG * plRetCode
);
In general, API functions that use strings can do so in three ways:
• They can require a string as input in an IN parameter
• They can return a string as output in an OUT parameter
• They can do both, either in the same parameter or in separate
parameters
To illustrate, Example 6-1 shows three API declarations.
Example 6-1: Three Example Declarations
// IN parameter example
HWND FindWindow(
LPCTSTR lpClassName, // pointer to class name
LPCTSTR lpWindowName // pointer to window name
);

// OUT parameter example


int GetWindowText(
HWND hWnd, // handle to window or control with text
LPTSTR lpString, // address of buffer for text
int nMaxCount // maximum number of characters to
copy
);

// IN/OUT parameter example

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

LPTSTR CharUpper(
LPTSTR lpsz // single character or pointer to string
);
The FindWindow function returns a handle to a top-level window whose
class name and/or window name matches specified strings. In this case,
both parameters are IN parameters.
The GetWindowText function returns the text of a window's title bar in an
OUT parameter lpString. It also returns the number of characters in the
title as its return value.
The CharUpper function converts either a string or a single character to
uppercase. When the argument is a string, the function converts the
characters in the character array in place, that is, the parameter is IN/OUT.
How shall we convert these function declarations to VB?
We could simply replace each C-style string with a VB-style:
ByVal str As String
declaration, which, as we know, is a BSTR data type. However, there are
some caveats. First, let us talk about the difference between passing a
BSTR by value as opposed to by reference.
Dealing with IN Parameters
The first declaration in Example 6-1:
HWND FindWindow(
LPCTSTR lpClassName, // pointer to class name
LPCTSTR lpWindowName // pointer to window name
);
might be translated as follows:
Declare Function FindWindow Lib "user32" Alias "FindWindowA" ( _
ByVal lpClassName As String, _
ByVal lpWindowName As String _
) As Long
This works just fine. Since the FindWindow function does not alter the
contents of the parameters (note the C in LPCTSTR), the BSTRs will be
treated by Win32 as LPSTRs, which they are. In general, when dealing
with a constant LPSTR, we can use a BSTR.
We should also note that FindWindow allows one (but not both) of these
string parameters to be set, with the remaining parameter set to a null. In
Win32, this parameter that the programmer chooses not to supply is
represented by a null pointer--that is, a pointer that contains the value 0.
Of course, 0 is not a valid address, so a null pointer is a very special type
of pointer and is treated in this way by Win32.
Fortunately, VB has the vbNullString keyword, which is a null BSTR (and
so also a null LPWSTR). It can be used whenever a null string is desired
(or required). Actually, this is not as trivial an issue as it might seem at
first. Before the introduction of the vbNullString into Visual Basic (I think
with VB 4), we would need to do something like:
FindWindow(0&,. . .)
to simulate a null string for the first parameter. The problem is that VB
would issue a type mismatch error, because a long 0 is not a string. The
solution was to declare three separate aliases just to handle the two extra

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

cases of null parameters. With the introduction of vbNullString, this


annoyance went away.
To illustrate, in order to get the handle of the window with title "Microsoft
Word - API.doc," we can write:
Dim sTitle As String
Dim hnd As Long
sTitle = "Microsoft Word - API.doc"
hnd = FindWindow(vbNullString, sTitle)
or more simply:
Dim hnd As Long
hnd = FindWindow(vbNullString, "Microsoft Word - API.doc")

Dealing with OUT Parameters


Now consider the second declaration in Example 6-1:
int GetWindowText(
HWND hWnd, // handle to window or control with text
LPTSTR lpString, // address of buffer for text
int nMaxCount // maximum number of characters to copy
);
This might be translated to VB as follows:
Declare Function GetWindowText Lib "user32" Alias
"GetWindowTextA" ( _
ByVal hwnd As Long, _
ByVal lpString As String, _
ByVal cch As Long _
) As Long
An HWND is a long value, as is a C-style int (integer). In this case, the
string parameter is an OUT parameter, meaning that the function is going
to fill this string with something useful--in this case, the title of the
window whose handle is in the hwnd parameter.
Here is an example of a call to this function:
Sub GetWindowTitle()

Dim sText As String


Dim hnd As Long
Dim cTitle As Integer
Dim lngS As Long, lngV As Long

' Allocate string buffer


sText = String$(256, vbNullChar)

' Save the BSTR and Unicode character array locations


lngV = VarPtr(sText)
lngS = StrPtr(sText)

' Search for window with a given class


hnd = FindWindow("ThunderRT5Form", vbNullString)

' If window found, get title


If hnd > 0 Then
cTitle = GetWindowText(hnd, sText, 255)
sText = Left$(sText, cTitle)
Debug.Print sText

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

' Compare the BSTR and character array locations


' to look for changes
Debug.Print VarPtr(sText), lngV
Debug.Print StrPtr(sText), lngS
Else
Debug.Print "No window with this class name.", vbInformation
End If

End Sub
The output of one run is:
RunHelp - Unregistered Copy - Monday, December 7, 1998
10:11:53 AM
1243480 1243480
2165764 2012076
(Don't worry--this unregistered program is mine own.)
We first allocate a string buffer for the window title. We will discuss this
important point further in a moment. Then we use FindWindow to search
for a window with class name ThunderRT5Form--a VB5 runtime form. If
such a window is found, its handle is returned in the hnd parameter. We
can then call GetWindow-Text, passing it hnd as well as our text buffer
sText and its size. Since the GetWindowText function returns the number
of characters placed in the buffer, not including the terminating null, that
is, the number of characters in the window title, we can use the Left
function to extract just the title from the string buffer.
Note also that we have saved both the BSTR address (in lngV) and the
character array address (in lngS ), so that we can compare these values to
the same values after calling GetWindowText. Lo and behold, the BSTR
has not moved, but its contents have changed, that is, the character array
has moved, as we discussed earlier.
Incidentally, since the returned string is null terminated and contains no
embedded nulls, the following function also extracts the portion of the
buffer that contains the title. This little utility is generic, and I use it often
(in this book as well as in my programs).
Public Function Trim0(sName As String) As String
' Right trim string at first null.
Dim x As Integer
x = InStr(sName, vbNullChar)
If x > 0 Then Trim0 = Left$(sName, x - 1) Else Trim0 = sName
End Function
Getting back to the issue at hand, it is important to understand that, when
OUT string parameters are involved, it is almost always our responsibility
to set up a string buffer, that is, a BSTR that has enough space allocated to
hold the data that will be placed in it by the API function. Most Win32
API functions do not create strings--they merely fill strings created by the
caller. It is not enough simply to declare:
Dim sText As String
We must allocate space, as in:
sText = String$(256, vbNullChar)
Thus, it is important to remember:

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

When dealing with OUT string parameters, be sure to


allocate a string buffer of sufficient size.
Note that in some cases, such as GetWindowText, the function provides an
IN parameter for specifying the size of the buffer. This is actually a
courtesy to us, in the sense that the function agrees not to place more
characters in the buffer than we specify as the size of the buffer. (I often
give the buffer an extra character that the function doesn't know about.
Usually, the function includes the terminating null in its reckoning, but
why take chances?)
Note that there are other cases in which no such courtesy is extended, so
we must be careful.
Consider the case of SendMessage, for example. Here is part of what the
Win32 documentation says about the LB_GETTEXT message, which can be
used to retrieve the text of an item in a list box.
An application sends an LB_GETTEXT message to
retrieve a string from a listbox.
wParam = (WPARAM) index; // item index [0-based]
lParam = (LPARAM) (LPCTSTR) lpszBuffer; // address of buffer
[The parameter lpszBuffer is a] pointer to the buffer that
will receive the string. The buffer must have sufficient
space for the string and a terminating null character. An
LB_GETTEXTLEN message can be sent before the
LB_GETTEXT message to retrieve the length, in
characters, of the string.
Thus, in this case, there is no IN parameter to act as a safety net. If we fail
to allocate sufficient space in the buffer, the function will write over the
end of our buffer, into unknown memory. If we are lucky, this will crash
the program. If we are not lucky, it will overwrite some other data,
possibly resulting in logical errors in our program, or crashing a client's
program!
However, in this case Windows is not completely devoid of compassion. It
does provide the LB_GETTEXTLEN message for us to use to first retrieve
the length of the item in question. With this value, we can allocate a
sufficiently capacious buffer. Example 6-2 shows some sample code. This
code extracts the items from a listbox (which might belong to some other
application) and places them in our listbox lstMain. We will expand this
example considerably in Chapter 16, Windows Messages. Note the use of
two different forms of the SendMessage function.
Example 6-2: Using LB_GETTEXT
Public Sub ExtractFromListBox(hControl As Long)

Dim cItems As Integer


Dim i As Integer
Dim sBuf As String
Dim cBuf As Long

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Dim lResp As Long

' Get item count from control


cItems = SendMessageByLong(hControl, LB_GETCOUNT, 0&, 0&)

If cItems <= 0 Then Exit Sub

' Put items into list box


For i = 0 To cItems - 1

' Get length of item


cBuf = SendMessageByString(hControl, LB_GETTEXTLEN,
CLng(i), vbNullString)

' Allocate buffer to hold item


sBuf = String$(cBuf + 1, " ")

' Send message to get item


lResp = SendMessageByString(hControl, LB_GETTEXT, CLng(i),
sBuf)

' Add item to local list box


If lResp > 0 Then
Form1.lstMain.AddItem Left$(sBuf, lResp)
End If

Next i

Form1.lstMain.Refresh

End Sub

An IN/OUT Parameter Example--Watching Out for As Any


Consider now the third and final function in Example 6-1:
PTSTR CharUpper(
LPTSTR lpsz // single character or pointer to string
);
One problem here is that, despite the declaration of lpsz as an LPTSTR,
the function allows the parameter to be filled with a non-LPTSTR. To wit,
the documentation states that the lpsz parameter is a:
Pointer to a null-terminated string or specifies a single
character. If the high-order word of this parameter is zero,
the low-order word must contain a single character to be
converted.
For use with string input, we can translate this into VB as:
Declare Function CharUpperForString Lib "user32" Alias
"CharUpperA" ( _
ByVal lpsz As String _
) As Long
This will generally work, as in:
' Convert string
str = "help"
Debug.Print StrPtr(str)

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Debug.Print CharUpperForString(str)
Debug.Print str
whose output is:
1896580
1980916
HELP
Let us pause for a moment to inspect this output. The CharUpper
documentation also states:
If the operand is a character string, the function returns a
pointer to the converted string. Since the string is converted
in place, the return value is equal to lpsz.
On the other hand, the two addresses StrPtr(s) (which is the address of the
character array) and CharUpper(s) seem to be different. But remember the
BSTR-to-ABSTR translation issue. Our string str undergoes a translation
to a temporary ABSTR string at another location. This string is passed to
the CharUpper function, which then changes the string (uppercases it) and
also returns the location of the ABSTR string. Now, VB translates the
ABSTR back to our BSTR, but it knows nothing about the fact that the
return value represents the location of the temporary ABSTR, so it returns
the address of that string!
We can confirm this further by calling the Unicode entry point, just as we
did in an earlier example. The following declaration and code:
Declare Function CharUpperWide Lib "user32" Alias "CharUpperW" (
_
ByVal lpsz As Long _
) As Long

' Construct an LPSTR


s = "help"
lng = StrPtr(s)
Debug.Print lng
Debug.Print CharUpperWide(lng)
Debug.Print s
returns:
1980916
1980916
HELP
Now the two addresses are the same, since no translation occurs!
For dealing with characters, we can make the following declaration:
Declare Function CharUpperForChar Lib "user32" Alias
"CharUpperA" ( _
ByVal lpsz As Long _
) As Long
For instance, calling:
Debug.Print Chr(CharUpperForChar(CLng(Asc("a"))))
returns an uppercase A.
You might think we could combine the two declarations by using As Any.
Declare Function CharUpperAsAny Lib "user32" Alias "CharUpperA"
(_
ByVal lpsz As Any _

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

) As Long
The following code works:
s = "help"
Debug.Print StrPtr(s)
Debug.Print CharUpperAsAny(s)
Debug.Print s
as does:
Debug.Print Chr(CharUpperAsAny(CLng(Asc("a"))))
and:
Debug.Print Chr(CharUpperAsAny(97&))
(which returns the uppercase letter A.) However, the following code
crashes my computer:
Debug.Print CharUpperAsAny(&H11000)
The problem is that the CharUpper function sees that the upper word of
&H11000 is nonzero, so it assumes that the value is an address. But this is
fatal. Who knows what is at address &H1100? In my case, it is protected
memory.
What Happened to My Pointer?
There is another, much more insidious problem that can arise in
connection with passing strings to API functions. As we can see from the
CharUpper case, the API occasionally uses a single parameter to hold
multiple data types (at different times, of course). Imagine the following
hypothetical circumstance.
A certain API function has declaration:
PTSTR WatchOut(
int nFlags // flags
LPTSTR lpsz // pointer to string or length as a long
);
The documentation says that if nFlags has value WO_TEXT (a symbolic
constant defined somewhere), then lpsz will receive an LPTSTR string
(pointer to a character array), but if nFlags has value WO_LENGTH, then
lpsz gets the length of the string, as a long.
Now, if we make the VB declaration:
Declare Function WatchOut Lib "whatever" ( _
ByVal nFlags As Integer
ByVal lpsz As String _
) As Long
we can get into real trouble. In particular, if we set nFlags equal to
WO_LENGTH, then the following events take place under Windows 9x:
1. We create an initial BSTR string buffer for lpsz, say: Dim str As
String str = String$(256, vbNullChar)

2. VB creates a temporary ABSTR to pass to WatchOut, as shown in


Figure 6-8.
Figure 6-8. Creating a temporary ABSTR

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

3. As Figure 6-9 shows, because nFlags = WO_LENGTH, WatchOut


changes the pointer, not the character array!
Figure 6-9. Changing the pointer rather than the character array

4. As Figure 6-10 shows, VB tries to translate what it thinks is an


ANSI character array at address zzzz of length ????. This is a
disaster.
Figure 6-10. The resulting broken pointer

Under Windows NT, the WatchOut function changes the original BSTR
pointer (instead of an ANSI copy), but this will have the same disastrous
effects. Note that even if we somehow are unlucky enough to escape a
crash when VB tries to translate the fraudulent ABSTR, the result will be
garbage, the program may crash after we send it to our customers, and
there is still the matter of the dangling string, whose memory will not be
recovered until the program terminates. This is called a memory leak.
The problem can be summarized quite simply: occasionally an API
function will change a string pointer (not the string itself) to a numeric
value. But VB still thinks it has a pointer. This spells disaster. In addition,
testing to see whether the contents of the BSTR pointer variable have
changed doesn't solve the problem, because as we have seen (Figure 6-8),
VB sometimes changes the pointer to point to a legitimate character array!
As it happens, the situation described earlier can occur. Here is an
important example, which we will play with at the end of the chapter.
The GetMenuItemInfo function retrieves information about a Windows
menu item. Its declaration is:
BOOL GetMenuItemInfo(
HMENU hMenu, // handle of menu
uint uItem, // indicates which item to look at
BOOL fByPosition, // used with uItem
MENUITEMINFO *lpmii // pointer to structure (see discussion)
);
where, in particular, the parameter lpmii is a pointer to a MENUITEMINFO
structure that will be filled in by GetMenuItemInfo. This structure is:
typedef struct tagMENUITEMINFO {
UINT cbSize;
UINT fMask;
UINT fType;
UINT fState;
UINT wID;
HMENU hSubMenu;
HBITMAP hbmpChecked;
HBITMAP hbmpUnchecked;
DWORD dwItemData;
LPTSTR dwTypeData;

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

UINT cch;
}
Note that the penultimate member is an LPTSTR.
Now, the rpiAPIData application on the accompanying CD will
automatically translate this to a VB user-defined type, replacing all C data
types in this case by VB longs:
Public Type MENUITEMINFO
cbSize As Long '//UINT
fMask As Long '//UINT
fType As Long '//UINT
fState As Long '//UINT
wID As Long '//UINT
hSubMenu As Long '//HMENU
hbmpChecked As Long '//HBITMAP
hbmpUnchecked As Long '//HBITMAP
dwItemData As Long '//DWORD
dwTypeData As Long '//LPTSTR
cch As Long '//UINT
End Type
Suppose instead that the LPTSTR was translated into a VB string:
dwTypeData As String
'//LPTSTR
According to the documentation for MENUITEMINFO, if we set the fMask
parameter to MIIM_TYPE, allocate a suitable string buffer in dwTypeData,
and place its length in cch, then the GetMenuItemInfo function will
retrieve the type of the menu item into fType (and adjust the value of cch).
If this type is MFT_TEXT, then the string buffer will be filled with the text
of that menu item. However, and this is the problem, if the type is
MFT_BITMAP, then the low-order word of dwTypeData gets the bitmap's
handle (and cch is ignored).
Thus, GetMenuItemInfo may change dwDataType from an LPTSTR to a
bitmap handle! This is exactly the problem we described earlier. We will
consider an actual example of this later in the chapter. Keep in mind also
that even if the type is MFT_TEXT, the dwDataType pointer may be
changed to point to a different character buffer.
So if we shouldn't use a string variable for dwDataType, what should we
do?
The answer is that we should create our own character array by declaring a
byte array and pass a pointer to that array. In other words, we create our
own LPSTR. VB doesn't know anything about LPSTRs, so it will try to
interpret it as a VB string.
This even solves the orphaned array problem, for if the API function
changes our LPSTR to a numeric value (like a bitmap handle), we still
retain a reference to the byte array (we had to create it somehow), so we
can deallocate the memory ourselves (or it will be allocated when the byte
array variable goes out of scope).
Before getting into a discussion of byte arrays and looking at an example,
let us summarize:
Occasionally an API function will change an LPSTR to a
numeric value. But VB will still think it has a string. This

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

spells disaster. Moreover, testing to see whether the


contents of the BSTR pointer variable have changed doesn't
help because VB sometimes changes the original BSTR to
point to a legitimate character array. Hence, if there is a
chance that this might happen, you should create your own
LPSTR using a byte array and use it in place of the BSTR.
For safety, you may want to do this routinely when the
string is embedded within a structure.
The last point made in the caveat is worth elaborating. Oftentimes an API
function parameter refers to a structure, whose members may be other
structures, whose members may, in turn, be other structures. This structure
nesting can get quite involved. We will see an example when we create
our DLL Export Table application. This makes it very difficult to keep
track of what the API function might be doing to all of the structure
members. The safest thing to do is to always use pointers to byte arrays
(that is, LPSTRs) and avoid BSTRs completely when dealing with strings
embedded in structures.

Strings and Byte Arrays


Of course, a byte array is just an array whose members have type byte, for
instance:
Dim b(1 to 100) As Byte
To get a pointer to this byte array, we can use VarPtr:
Dim lpsz As Long
lpsz = VarPtr(b(1)) ' or rpiVarPtr(b(1))
(Even though it doesn't seem so, the letters lpsz stand for long pointer to
null-terminated string.) Note that the address of the first member of the
array is the address of the array.
Remembering that an LPSTR is a pointer to a null-terminated character
array, we should initialize the array to nulls:
For i = 1 To 100
b(i) = 0
Next
(It is true that VB does its own initialization, but it is not good
programming practice to rely on this.)
Translating Between Byte Arrays and BSTRs
To copy a BSTR:
Dim s As String
to a byte array, we can proceed in a couple of different ways. For a strictly
VB solution, we have:
s = "help"
Dim b(1 To 8) As Byte
For i = 1 To 8
b(i) = AscB(MidB(s, i))
Next
Another approach is:
s = "help"

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Dim b(1 To 8) As Byte


CopyMemory b(1), ByVal StrPtr(s), LenB(s)
Note that (in both cases) we get:
104 0 101 0 108 0 112 0
showing that the bytes are reversed in each Unicode integer.
In the other direction, to copy a byte array into a BSTR, VB gives us some
help. If b is a Unicode byte array, we can just write:
Dim t As String
t=b
For an ANSI byte array b, we write:
Dim t As String
t = StrConv(b, vbUnicode)
Note, however, that the StrConv function does not recognize a null
terminator in the byte array--it will translate the entire array. Any nulls
that are encountered in the array become embedded nulls in the BSTR.
Translating Between BSTRs and LPTSTRs
Let us consider how to translate back and forth between BSTRs and
LPTSTRs.
From BSTR to LPWSTR
Getting a BSTR into a Unicode byte array is conceptually easy, because
the character array of the BSTR is a Unicode byte array, so all we need to
do is copy the bytes one by one. Here is a function to translate BSTRs to
LPWSTRs:
Function BSTRtoLPWSTR(sBSTR As String, b() As Byte, lpwsz As
Long) As Long

' Input: a nonempty BSTR string


' Input: **undimensioned** byte array b()
' Output: Fills byte array b() with Unicode char string from sBSTR
' Output: Fills lpwsz with a pointer to b() array
' Returns byte count, not including terminating 2-byte Unicode null
character
' Original BSTR is not affected

Dim cBytes As Long

cBytes = LenB(sBSTR)

' ReDim array, with space for terminating null


ReDim b(1 To cBytes + 2) As Byte

' Point to BSTR char array


lpwsz = StrPtr(sBSTR)

' Copy the array


CopyMemory b(1), ByVal lpwsz, cBytes + 2

' Point lpsz to new array


lpwsz = VarPtr(b(1))

' Return byte count

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

BSTRtoLPWSTR = cBytes

End Function
This function takes a BSTR, an undimensioned byte array, and a long
variable lng and converts the long to an LPWSTR. It returns the byte
count as the return value of the function. Here is an example:
Dim b() As Byte
Dim lpsz As Long, lng As Long
lng = BSTRToLPWSTR("here", b, lpsz)
It might have occurred to you to simply copy the contents of the BSTR to
the contents of lpsz:
lpsz = StrPtr(sBSTR)
The problem is that now we have two pointers to the same character array-
-a dangerous situation because VB does not realize this and might
deallocate the array.
From BSTR to LPSTR
The function to convert a BSTR to an LPSTR is similar, but requires a
translation from Unicode to ANSI first:
Function BSTRtoLPSTR(sBSTR As String, b() As Byte, lpsz As Long)
As Long

' Input: a nonempty BSTR string


' Input: **undimensioned** byte array b()
' Output: Fills byte array b() with ANSI char string
' Output: Fills lpsz with a pointer to b() array
' Returns byte count, not including terminating null
' Original BSTR is not affected

Dim cBytes As Long


Dim sABSTR As String

cBytes = LenB(sBSTR)

' ReDim array, with space for terminating null


ReDim b(1 To cBytes + 2) As Byte

' Convert to ANSI


sABSTR = StrConv(sBSTR, vbFromUnicode)

' Point to BSTR char array


lpsz = StrPtr(sABSTR)

' Copy the array


CopyMemory b(1), ByVal lpsz, cBytes + 2

' Point lpsz to new array


lpsz = VarPtr(b(1))

' Return byte count


BSTRtoLPSTR = cBytes

End Function

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

From LPWSTR to BSTR


On return from an API call, you may have an LPWSTR, that is, a pointer
to a null-terminated Unicode character array. Visual Basic makes it easy to
get a BSTR from a byte array--just make an assignment using the equal
sign. However, VB doesn't know how to handle a pointer to a byte array.
Here is a little utility:
Function LPWSTRtoBSTR(ByVal lpwsz As Long) As String

' Input: a valid LPWSTR pointer lpwsz


' Return: a sBSTR with the same character array

Dim cChars As Long

' Get number of characters in lpwsz


cChars = lstrlenW(lpwsz)

' Initialize string


LPWSTRtoBSTR = String$(cChars, 0)

' Copy string


CopyMemory ByVal StrPtr(LPWSTRtoBSTR), ByVal lpwsz, cChars *
2

End Function
From LPSTR to BSTR
We can modify the previous utility to return a BSTR from an LPSTR as
follows (recall that Trim0 just truncates a string at the first null character):
Function LPSTRtoBSTR(ByVal lpsz As Long) As String

' Input: a valid LPSTR pointer lpsz


' Output: a sBSTR with the same character array

Dim cChars As Long

' Get number of characters in lpsz


cChars = lstrlenA(lpsz)

' Initialize string


LPSTRtoBSTR = String$(cChars, 0)

' Copy string


CopyMemory ByVal StrPtr(LPSTRtoBSTR), ByVal lpsz, cChars

' Convert to Unicode


LPSTRtoBSTR = Trim0(StrConv(LPSTRtoBSTR, vbUnicode))

End Function

Example: Using Byte Arrays


Let us demonstrate the use of byte arrays with a simple example using
CharUpper. We have seen that this function is declared as:
LPTSTR CharUpper(

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

LPTSTR lpsz // single character or pointer to string


);
This leads to two reasonable translations into VB:
Declare Function CharUpperByBSTR Lib "user32" Alias
"CharUpperA" ( _
ByVal s As String _
) As Long
or:
Declare Function CharUpperByLPSTR Lib "user32" Alias
"CharUpperA" ( _
ByVal lpsz As Long _
) As Long
We have seen the first form in action, so let us try the second form.
The following code first converts a BSTR to an LPSTR. Note that we
should not convert to LPWSTR, since LPWSTRs are passed to
CharUpperA without translation by VB; and if we passed an LPWSTR,
then as soon as CharUpperA encountered the null byte that is part of the
first Unicode character in the LPWSTR, it would think the string had
ended. Thus, it would capitalize only the first character in the string.
The LPSTR is then passed to CharUpperA, which converts it to
uppercase. Having saved the LPSTR pointer, we can check to see if it has
been changed. If not, we translate the LPSTR back to a BSTR and print it.
If the pointer is changed, then we must deallocate the byte array ourselves
(or just let the array variable pass out of scope).
Of course, in this simple example, the pointer should not be changed by
CharUpper. Nevertheless, this same procedure will deal with API
functions that may change the pointer:
Public Sub CharUpperText

Dim lpsz As Long


Dim lpszOrg As Long
Dim sBSTR As String
Dim b() As Byte

sBSTR = "help"

' Convert BSTR to LPSTR.


BSTRtoLPSTR sBSTR, b, lpsz

' Save LPSTR to check for modification by API function


lpszOrg = lpsz

' Convert to upper case


CharUpperAsLPWSTR lpsz

' If pointer not modified, then convert back to BSTR


' and print
If lpszOrg = lpsz Then
Debug.Print LPSTRtoBSTR(lpsz)
Else
Erase b
' Use new value of lpsz if desired...

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

End If

End Sub

Example: Windows Menus


Let us turn to the example involving GetMenuItemInfo that we promised
earlier. Recall that the GetMenuItemInfo function retrieves information
about a Windows menu item. Its VB declaration is:
Declare Function GetMenuItemInfo Lib "user32" Alias
"GetMenuItemInfoA" ( _
ByVal hMenu As Long, _
ByVal uItem As Long, _
ByVal lByPos As Long, _
ByRef lpMenuItemInfo As MENUITEMINFO _
) As Long
where, in particular, the parameter lpmii is a pointer to a MENUITEMINFO
structure that will be filled in by GetMenuItemInfo. This structure is:
Public Type MENUITEMINFO
cbSize As Long
fMask As Long
fType As Long
fState As Long
wID As Long
hSubMenu As Long
hbmpChecked As Long
hbmpUnchecked As Long
dwItemData As Long
dwTypeData As Long
cch As Long
End Type
According to the documentation, if we set the fMask parameter to
MIIM_TYPE, allocate a suitable string buffer in dwTypeData, and place its
length in cch, then the GetMenuItemInfo function will retrieve the type of
the menu item into fType. If this type is MFT_TEXT, then the string buffer
will be filled with the text of that menu item. However, and this is the
problem, if the type is MFT_BITMAP, then the low-order word of
dwTypeData gets the bitmap's handle. Thus, in this case,
GetMenuItemInfo will change dwTypeData from an LPTSTR to a bitmap
handle!
Figure 6-11 shows a menu with a bitmap. We will discuss how to create
such a menu in VB using the Win32 API in Chapter 21, Bitmaps.
Figure 6-11. A menu with a bitmap

Example 6-3 shows the code used to get the text for each of the items in
this menu.
Example 6-3: Getting Menu Text
Public Sub GetMenuInfoExample

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Const MIIM_TYPE = &H10 ' from WINUSER.H


Dim uMenuItemInfo As MENUITEMINFO
Dim bBuf(1 To 50) As Byte
Dim sText As String

' Initialize structure


uMenuItemInfo.cbSize = LenB(uMenuItemInfo)
uMenuItemInfo.fMask = MIIM_TYPE
uMenuItemInfo.dwTypeData = VarPtr(bBuf(1))
uMenuItemInfo.cch = 49

' Get menu text


For i = 0 To 2
' Must reset count each time
uMenuItemInfo.cch = 49

' Get TypeData before


Debug.Print "Before:" & uMenuItemInfo.dwTypeData

' Call API


lng = GetMenuItemInfo(hSubMenu, CLng(i), -1, uMenuItemInfo)

' Get TypeData after


Debug.Print "After:" & uMenuItemInfo.dwTypeData

' Print text -- CAREFUL HERE


' sText = StrConv(bBuf, vbUnicode)
' Debug.Print sText

Next

End Sub
Here is what happens as this code executes.
The first loop (i = 0) presents no problems, and the output is:
Before:1479560
After:1479560
Observe that the buffer pointer had not changed. Hence, the commented
code that prints the menu text would run without error.
The second loop also runs without error (as long as the statements
involving sText are commented out). The output, however, is:
Before:1479560
After:3137668829
As the documentation suggests, GetMenuItemInfo returns the bitmap's
handle in uMenuItemInfo.dwTypeData. Thus, we have lost the pointer to
the buffer sBuf. On the third loop, the program will crash, because the
third call to GetMenuItem-Info will try to write the menu text for the third
item to an imaginary buffer at address 3137668829 = &Hbb0506dd. If this
memory is protected (as it probably is), you will get a message similar to
the one I got in Figure 6-12.
Figure 6-12. Whoops

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Note that if we uncomment the lines of code that print the menu text, the
code will probably crash when we come to these lines during the second
loop.
To fix this code, we need to pay attention to when the pointer changes and
correct the problem, as in Example 6-4.
Example 6-4: A Corrected Version of Example 6-3
Public Sub GetMenuInfoExample

Dim uMenuItemInfo As utMENUITEMINFO


Dim bBuf(1 To 50) As Byte
Dim sText As String
Dim lPointer As Long

' Initialize structure


uMenuItemInfo.cbSize = LenB(uMenuItemInfo)
uMenuItemInfo.fMask = MIIM_TYPE
uMenuItemInfo.dwTypeData = VarPtr(bBuf(1))
uMenuItemInfo.cch = 49

' Get menu text


For i = 0 To 2
' Must reset count each time
uMenuItemInfo.cch = 49

' Save buffer pointer


lPointer = uMenuItemInfo.dwTypeData

Debug.Print "Before:" & uMenuItemInfo.dwTypeData

' Call API


lng = GetMenuItemInfo(hSubMenu, CLng(i), -1, uMenuItemInfo)

Debug.Print "After:" & uMenuItemInfo.dwTypeData

' Check for pointer change


If lPointer <> uMenuItemInfo.dwTypeData Then
Debug.Print "Bitmap!"
' Restore pointer
uMenuItemInfo.dwTypeData = lPointer
Else
' Print text
sText = StrConv(bBuf, vbUnicode)
Debug.Print sText
End If

Next

End Sub
The output is:
Before:1760168
After:1760168
Test1
Before:1760168

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

After:1443168935
Bitmap!
Before:1760168
After:1760168
Test3
Note that if we had declared uMenuItemInfo.dwTypeData of type String,
then as soon as GetMenuItemInfo changed the pointer to the bitmap
handle, VB would think it had a character array at that location. We can't
even watch out for this and reset the pointer, because the change might
have been legitimate.
The previous discussion and the previous example have shown that we
need to be very careful about BSTRs. In short, there are two issues that
must be addressed:
• A BSTR undergoes a BSTR-to-ABSTR translation when passed to
an external function.
• A BSTR may have its value changed to a non-BSTR value (such as
a handle or length) by an external function.
Note that these issues must be addressed even when a BSTR is embedded
in a structure.
In any case, the translation issue is generally not a problem, since VB does
the reverse translation on the return value. However, the other issue can be
a fatal problem. The only way to avoid it completely is to manually
replace any BSTRs by LPSTRs, using a byte array.

Getting the Address of a Variable


of User -Defined Type
An API programmer often needs to get the address of a variable of user-
defined type. Consider, for example, the structure:
Type utExample
sString As String
iInteger As Integer
End Type

Dim uEx As utExample


Suppose we want to find the address of the variable uEx. First, note that
the address of a structure variable is the same as the address of its first
member.
Now consider the following code:
Debug.Print VarPtr(uEx)
Debug.Print VarPtr(uEx.sString)
Debug.Print VarPtr(uEx.iInteger)
Debug.Print
Debug.Print rpiVarPtr(uEx)
Debug.Print rpiVarPtr(uEx.sString)
Debug.Print rpiVarPtr(uEx.iInteger)
whose output is as follows.
1243836
1243836

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

1243840

1243824
1243820
1243840
As you can see, VarPtr reports the address as you would expect: the
address of uEx is the same as the address of uEx.aString, and the address
of uEx.iInteger is 4 bytes larger, to account for the 4-byte BSTR.
On the other hand, the rpiVarPtr is susceptible to BSTR-to-ABSTR
translation, which occurs on the member of the structure that is a BSTR.
The relationship between the first and second address in the second group
may look strange until we remember that each call to rpiVarPtr produces
a translation, so we cannot compare addresses from two separate calls,
both of which involve translations!
On the other hand, the third address is the address of the original integer
member. There is no translation in the call:
Debug.Print rpiVarPtr(uEx.iInteger)
because there are no BSTR parameters. Thus, we can use an external
function such as rpiVarPtr to compute the address of a structure provided
the structure has at least one non-BSTR parameter. In this event, we get
the address of one such parameter and count backwards to thebeginning of
the structure

Using Win32 API in VB

Table of Contents
o What's API - description of API
o API declarations - how to declare APIs
o API functions - types of function
o API messages
o Handles, Coordinates, Structs, ...
o API parameter types - VB equivalents
o Any
o Passing parameters - ByVal, ByRef, structs, strings, arrays...
o Callbacks
o WinProc
o Subclassing
o Getting Error result

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

o Combining flags
o Handling parameters
o How to know the functions?
o An example
o Closing Words

What's API
API (Application Programers Interface) is a set of predefined Windows
functions used to control the appearance and behaviour of every Windows
element (from the outlook of the desktop window to the allocation of
memory for a new process). Every user action causes the execution of
several or more API function telling Windows what's happened.
It is something like the native code of Windows. Other languages just act
as a shell to provide an automated and easier way to access APIs. VB has
done a lot in this direction. It has completely hidden the APIs and
provided a quite different approach for programming under Windows.
Here is the place to say that, Every line of code you write in VB is beening
translated by VB into API function and sent to Windows. Thus calling
something like Form1.Print ... causes VB to call TextOut API function
with the needed parameters (either given by you in the code, or taken by
some defaults).
Also, when the user click a button on your form, Windows sends a
message to your windows procedure (that is eventually hidden for you),
VB gets the call, analyses it and raises a given event (Button_Click) for
you.

The API functions reside in DLLs (like User32.dll, GDI32.dll, Shell32.dll,


...) in the Windows system directory.

API Declarations.
As said in "What's API", functions are declared in DLL's (Dynamic Link
Libraries) located in the Windows System directory. You can type in the
declaration of an API just as you do with any other function exported from
a Dll, but VB has provided an easier way to do it. It is called API Text
Viewer.
To have some API declared in your project, just launch API Text Viewer,
open Win32Api.txt (or .MDB if you have converted it into a database to
speed it up), choose Declares, find the function, click Add and then Copy.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Go to your project and paste it in. Do the same to have a predefined


constant or type.
There are few problems you may face:
Suppose you want to declare a function in your form module. You paste
the declaration and run the program. VB says "Compile Error, ...and
Declare statemets not allowed as Public members of ... ." Sounds bad, but
it isn't, all you need to do is add Private in front of the declaration (like
Private Declare Function ...). Do not forget, though, that the function will
be visible only within this form module.
In some cases, you may get the message Ambiguous name detected by
VB. It means you have two functions, constants or whatsoever sharing one
name. As most of the functions (maybe all of them, I haven't checked that)
are aliased, which means they are given different names, and not their
original using Alias clause, you may simply change the name of the
function and it will still work.

You may read the Declare statement help topic of VBs for description of
Alias.

Messages
OK, now you know what API function is, but you have definitely heard of
messages (if you haven't you will soon do) and wonder what is this.
Messages are the basic way Windows tells your program that some kind of
input has occured and you must process it. A message to your form is sent
when the user clicks on a button, moves the mouse over it or types text in
a textbox.
All messages are sent along with four parameters - a window handle, a
message identifier and two 32-bit (Long) values. The window handle
contains the handle of the window the message is goint to. The identifier is
actually the type of input occured (click, mousemove) and the two value
specify an additional information for the message (like where is the mouse
cursor when the mouse is been moved).
But, when messages are sent to you, why don't you see them, looks like
someone is stealing your mail. And before you get angry enough, let me
tell you.
The theft is actually VB. But he does not steal your mail, but instead reads
it for you and give you just the most important in a better look (with some
information hidden from time to time). This better look is the events you
write code for.

So, when the user moves the mouse over your form, Windows sends
WM_MOUSEMOVE to your window, VB get the message and its

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

parameters and executes the code you've entered for Button_MouseMove


event. Along the way, VB has transformed the second 32-bit value of the
message (it contains the X and Y position in pixels, 16-bit each) into two
Single type value of twips.
Now, say you need the coordinates of the mouse in pixels. VB converted
them into twips, and now you will convert them into pixels again. Apart
from losing time, it is somehow iritating to know Windows give you what
you need and VB "favorably" alters it so that you must re-alter it. Here
you will ask - Can't I receive the messages myself. OK, there is a way
called Subclassing, but you should use it only if really necessary as it goes
a bit against the concepts of safe programming of VB.
Something else that need to be said: You can send massages to your own
window or to another one yourself. You just call SendMessage or
PostMessage (SendMessage will cause the window to process the message
immediately and Post message will post it onto a queue, called message
queue, after any other messages waiting to be processed (it will return
after the message is processed, i.e. with some delay)). You must specify
the window handle to send the message to, the message identifier (all
message identifiers are available as constants in VB API Text Viewer) and
the two 32-bit values.

Some Windows specifics.


This topic is intended to give you a clue about some Windows specifics
that are not the same under VB.
Windows identifies every form, control, menu, menu item or whatever
you can think of by its handle. When your application is run, every control
on it is assigned a handle which is used later to separate the button from
the rest of the controls. If you want to perform any operation on the button
through an API you must use this handle. Where to get it from? Well VB
has provided a hWnd property for all controls that have handles in
Windows.

Windows works with pixels, not twips. So, it is a good idea to have the
controls you'll use API functions over set their ScaleMode properties to
Pixel(3) so that you can use ScaleXXX properties to get their metrics. But
even though, you have this opportunity, you may still need to convert
twips to pixels and vice versa. You do it using TwipsPerPixelX and
TwipsPerPixelY ot the global Screen object. Here it is:
pixXValue = twipXValue \
Screen.TwipsPerPixelX
pixYValue = twipYValue \
Screen.TwipsPerPixelY
twipXValue = pixXValue *

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Screen.TwipsPerPixelX
twipYValue = pixYValue *
Screen.TwipsPerPixelY
I haven't really seen the TwipsPerPixelX and TwipsPerPixelY value to be
different, but its always better to make difference, at least for the good
programing style. Also note that \ (for integer division) is used instead of /
as pixels must always be whole numbers.
Another thing to mention is that Windows uses different coordinate
systems for the functions. So, be careful.
And lastly, don't forget that VB is safe till the moment you begin to use
APIs. A single syntax error in an API call may cause VB to crash (save
often!). Also VB cannot debug APIs and if your program is crashing or
behaving awkwardly, firstly check the API calls - for missed ByVal, for
mistaken type or parameter, everything).
Where to get the functions description from
This topics won't tell how to change the button text through API or how to
find a file quickly. It is not a API functions documentation.
To get the description of an API function, you need to have either SDK
help file or the Microsoft SDK documentation (it's more that 40MB I think
- how can I place it here?). Such SDK helps are shipped with Borland
Delphi 3.0 package or MS Visual C++ 5.0 for example. Search the internet
are ask your friends to get one. The newer it is the better.
Note that SDK help for Win 3.x won't help you as some functions are
obsolete, though most of them still exist for compatibility in Win95.

API parameter types


When you already have a SDK Help, you will surely notice that functions
return values and parameters have some strange types like VOID,
LPCSTR or DWORD. If you are familiar with C, then you know what
they mean. For the rest, here is a table taken from VB Books Online (topic
is Converting C Declarations to Visual Basic):
C language data type In Visual Basic declare as Call with
ATOM ByVal variable As Integer An expression that evaluates to an Integer
BOOL ByVal variable As Long An expression that evaluates to a Long
BYTE ByVal variable As Byte An expression that evaluates to a Byte
CHAR ByVal variable As Byte An expression that evaluates to a Byte
COLORREF ByVal variable As Long An expression that evaluates to a Long
DWORD ByVal variable As Long An expression that evaluates to a Long
HWND, HDC, HMENU, etc. (Windows handles)
An expression that evaluates to a Long
ByVal variable As Long

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

INT, UINT ByVal variable As Long An expression that evaluates to a Long


LONG ByVal variable As Long An expression that evaluates to a Long
LPARAM ByVal variable As Long An expression that evaluates to a Long
LPDWORD variable As Long An expression that evaluates to a Long
LPINT, LPUINT variable As Long An expression that evaluates to a Long
LPRECT variable As type Any variable of that user-defined type
LPSTR, LPCSTR ByVal variable As String An expression that evaluates to a String
LPVOID variable As Any Any variable (use ByVal when passing a string)
LPWORD variable As Integer An expression that evaluates to an Integer
LRESULT ByVal variable As Long An expression that evaluates to a Long
NULL As Any or ByVal variable As Long ByVal Nothing or ByVal 0& or vbNullString
SHORT ByVal variable As Integer An expression that evaluates to an Integer
VOID Sub procedure Not applicable
WORD ByVal variable As Integer An expression that evaluates to an Integer
WPARAM ByVal variable As Long An expression that evaluates to a Long
Notes.
You should notice that the BOOL type (boolean) evaluates to Long and
not Boolean. So, 0 refers to False and any other value to True.

HWND, HDC, HMENU, etc. - etc. means there also other types like these.
All of them begin with H and stand for handles for different type of
objects. For example HBITMAP is a bitmap handle, HBRUSH is a brush
handle and so on. They all evaluate to Long and should be passes ByVal.
Notice also that LPVOID is declared as variable As Any. There is a
separate topic dedicate to Any.
Some types begin with LP. It is an abbreviation of Long Pointer to. So
LPWORD is actually a memory location where the data is stored. No, you
won't have to call a function to get this address. When you pass your
argument ByRef (the defaul) you actually pass its address. The thing to
remember here is that, if you parameter type begins with LP - you should
pass it ByRef. By the way LPARAM is like Lparam and not LParam. It is
not a pointer. You must pass the actual value here, so it is passed ByVal.
There is also some strange type NULL. You know from VB so I won't
discuss it here. Just choose a way you will pass it when needed. In most of
the cases I see passing it as ByVal 0& or as vbNullString.
And lastly, VOID is used for functions return value to specify that there is
no such value. API doen not have Subs so this is the it implements them.
Just remember - if the function is declared as VOID - you must declare it
as Sub in your VB code.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Any
Some messages contain parameters declared as "Any". It means this
parameter can be a variety of types (you may pass an integer, a string, or a
user-defined type, or else). So, here is an example of a function
(SendMessage) which contains a parameter of type Any:
Public Declare Function SendMessage Lib
"User32" Alias "SendMessageA" _
(ByVal Hwnd as Long,
ByVal wMsg as Long, _
ByVal wParam as Long,
lParam as Any) as Long
lParam is declared ByRef (default) and as Any. Now, here are some rules
to follow when passing different type of values to this function as lParam.
If the value is Pass it As
numeric ByVal (as Long, or as Any)
Null ByVal (as Long, or as Any)
string ByRef (as String, or as Any)
Type ByRef (as Any)
array of Type ByRef (as Any)
If your function declaration looks like the one above, and you need to pass
a Long, write something like:
Call SendMessage(Me.Hwnd, WM_XXXX,
0&, ByVal LongValue)
Note that there is nothing in front of the first three parameter although
they are numeric values. This is so, because in the function declaration
they are declared as ByVal. The fourth parameter, though, is declared
ByRef (VB doesn't know what kind of values you are going to pass) and
you must explicitly specify ByVal in front of it.
Sometimes it's much simpler to just declare several versions of one
function and use a different one for different calls. You may declare
something like:
Public Declare Function SendMessageLng Lib "User32"
Alias "SendMessageA" _
(ByVal Hwnd as Long, ByVal
wMsg as Long, _
ByVal wParam as Long, ByVal
lParam as Long) as Long

or

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Public Declare Function SendMessageStr Lib "User32"


Alias "SendMessageA" _
(ByVal Hwnd as Long, ByVal
wMsg as Long, _
ByVal wParam as Long,
lParam as String) as Long
Notice that the parameter type does not change for API. The fourth
parameter is always a 4-byte Long value. When you pass a Long or Null
ByVal, a the 4-byte value is passed directly to the function. If you pass a
String or else, you pass it ByRef and so VB actually passes the address of
you variable, and it is a 4-byte value again.
Passing Parameter
Of course you know how to pass parameters, just put it in the function call
and its done! Well there are some details you should be aware of when
passing parameters to API function.
ByVal or ByRef. Usually you don't have to bother about these keywords
as VB API Text Viewer declares the function parameters as API wants
them and when you just enter your value, it is passed as is declared.
Generally, when a value is passed ByVal, the actual value is passed
directly to the function, and when passed ByRef, the address of the value
is passed. The only thing you may encounter is the Any type.

Passing strings to API function isn't difficult too. The API expects the
address of the first character of the string and reads ahead of this address
till it reaches a Null character. Sound bad, but this the way VB actually
handles strings. The only thing to remember is always to pass the String
ByRef.
The situation is slightly different when you expect some information to be
returned by the function. Here is the declaration of GetComputerName
API function:
Declare Function GetComputerName Lib
"kernel32" Alias "GetComputerNameA" _
(ByVal
lpBuffer As String, nSize As Long) As Long
The first parameter is a long pointer to string, and the second the length of
the string. If you just declare a variable as String and pass it to this
function, an error occurs. So, you need to initialize the string first. Here is
how to get the computername:
Dim Buffer As String
Buffer = Space(255)
Ret& =
GetComputerName(Buffer,

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Len(Buffer))
if Ret& > 0 then
CompName$ = Left(Buffer,
Ret&)
Here, the string is initialized as 255-spaces string. We pass it to the
function and give it the length too. The function returns 0 for an error or
the actual length of the computer name otherwise. CompName$ will
contain the computer name.
Some functions also expect arrays. Here an exmaple:
Declare Function SetSysColors Lib "user32"
Alias "SetSysColors" _
(ByVal
nChanges As Long, lpSysColor As Long, _

lpColorValues As Long) As Long


The last two parameter are arrays of Long. To pass an array to a function,
you pass just the first element. Here is a sample code:
Const COLOR_ACTIVECAPTION = 2
Const COLOR_INACTIVECAPTION = 3
Const COLOR_CAPTIONTEXT = 9
Const
COLOR_INACTIVECAPTIONTEXT = 19
Dim SysColor(3) As Long
Dim ColorValues(3) As Long
SysColor(0) = COLOR_ACTIVECAPTION
SysColor(1) =
COLOR_INACTIVECAPTION
SysColor(2) = COLOR_CAPTIONTEXT
SysColor(3) =
COLOR_INACTIVECAPTIONTEXT

ColorValues(0) = RGB(58, 158, 58) 'dark


green
ColorValues(1) = RGB(93, 193, 93) 'light
green
ColorValues(2) = 0 'black
ColorValues(3) = RGB(126, 126, 126) 'gray
Ret& = SetSysColors(4&, SysColor(0),
ColorValues(0))
This sample changes the system colors for the active window caption
background and text and for the inactive window caption background and
text.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Callbacks
A callback is a function you write and tell Windows to call for some
reason. You create your own function with a specified number and type of
parameters, then tell Windows that this function should be called for some
reason and its parameters filled with some info you need. Then Windows
calls you function, you handle the parameters and exit from the function
returning some kind of value.
A typical use of callbacks is for receiving a continuous stream of data
from Windows. Here is the declaration of a function that requires a
callback:

Declare Function EnumWindows Lib


"User32" _
ByVal lpEnumFunc As Long,
ByVal lParam As Long) As Long
The first parameter is the address of your callback function, and the
second is a whatever value you want. This value will be passed to your
function, so that you know what it is called for.
VB 5.0 has provided a useful operator called AddressOf which returns the
address of a function. It may be used only in front of a parameter when
you call a function and uses like
FuncP = AddressOf MyFunction
are wrong and cause error. So, you must call EnumWindows like that:
Success& = EnumWindows(AddressOf
cbFunc, 58&)
You must also write the callback function. There are different type of
callbacks that have a different sets of parameters. Description of this
parameter can be found in a SDK Help file or MS SDK documentation.
Here is the declaration for the callback:
Function cbFunc (ByVal Hwnd, ByVal
lParam) as Long
Here is a sample of callbacks:
Private Declare Function GetWindowText
Lib "user32" Alias "GetWindowTextA" _
(ByVal hwnd As Long, ByVal
lpString As String, _
ByVal cch As Long) As Long
Success& = EnumWindows(AddressOf
cbFunc, 58&)
Function cbFunc (ByVal Hwnd, ByVal
lParam) as Long

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

If lParam = 58 then 'enum windows


Str$ = Space(255)
Ret& = GetWindowText(Str$,
Len(Str$))
Debug.Print Left(Str$, Ret&)
End If
End Function
This sample enumerates the captions of all windows (no childs).
The Window Procedure
Windows does not know anything about events. These are shipped in VB
to hide the actual way Windows informs your window that something is
happening with him. VB gently serves as a interpreter and translates the
Windows language into VBs.

But the reallity is different and you will soon face it. Imagine you want to
know when the user highlights you menu item (not press, just highlight).
VB does not provide such event, but you've seen how other programs
display some text in the statusbar as you browse their menus. If they can,
why you don't.
OK, here is the rough reallity. Each window has a special procedure called
window procedure. It is actually a callback function. This function is sent
a message any time something happens with you window. Thus a message
(WM_COMMAND) is sent when the use highlights a menu item.

Why then I can't see this message? This is because VB creates the window
procedure instead of you. When Windows sends a message, this procedure
dispatches it to a certain event and converts its parametrs into some easier
to use parameters of the event. But, in some cases this procedures just
ignores some messages and can't receive the actual input. If you really
need to get this message, you must subclass your window, which
discussed in another topic.

Here is the declaration of a calback window procedure:


Function WindowProc(ByVal Hwnd As
Long, ByVal wMsg As Long, _
ByVal wParam As
Long, ByVal lParam As Long) As Long
The first parameter specifies the window handle, wMsg is a message
identifier (like WM_COMMAND or WM_MOUSEMOVE), wParam and
lParam are 32-bit values which meaning depends on the type of message
sent.
SubClassing

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

When you have already used the maximum VB offers you and want to do
something more, or just want to know something more about what's going
on with your window, now or then you will find the advantages of
subclassing.
Subclassing refers to changing the active window procedure with a new
one. Now this new procedure will receive all messages coming to your
window before the old one. But the old procedure still exists, it's not lost.
If you do not process a given message, you should call the old procedure
to process it.
Subclassing is done by calling SetWindowLong. This function changes a
specified attribute of the given window. Here is its declaration:
Declare Function SetWindowLong Lib
"user32" Alias "SetWindowLongA" _
(ByVal hwnd As Long,
ByVal nIndex As Long, _
ByVal dwNewLong As
Long) As Long
The first parameter specifies the window to be subclassed, the second
should be GWL_WNDPROC (-4) and the third should be the address of
the new window procedure. See Callbacks and The Window Procedure.
This function will be called literally every time your window has the focus
and something is going on and in some other cases (like changing some
system parameter by another process).
SetWindowLong return 0 if an error occurs, or the address of the old
window procedure. This address is especially important and you should
save it in a variable or else. It is used to call the old function when you do
not process a message (in fact you will process less than 1% of all
message and will let the old procedure handle the rest).
Calling the old window procedure is accomplished by CallWindowProc
API function. Here is the declaration:
Declare Function CallWindowProc Lib
"user32" Alias "CallWindowProcA" _
(ByVal lpPrevWndFunc
As Long, ByVal hWnd As Long, _
ByVal Msg As Long,
ByVal wParam As Long, _
ByVal lParam As Long)
As Long
The first parameter is the address of the old procedure and rest are just the
same as the four parameter you receive. Note that you may change some
of the values to control the message process. For example, when you
receive WM_MOUSEMOVE, you get the coordinates of the mouse from
lParam and change them to some other coordinates. Then the old window

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

procedure will think the mouse is not where it is actually and may for
example show a tooltip of some distant control or do some other funny
things.
The ruturn value you specify is also meaningful. It depends on the
message sent.
It is very important to return the original window procedure before ending
you program. It is usually done in Form_Unload. Here is how:
Ret& = SetWindowLong(Me.Hwnd,
GWL_WNDPROC, oldWndProcAddress)
If you miss this line when starting your program through VB, the result is
a crash of VB and loss of any unsaved data. Be careful.
Here is a simple example of subclassing:
Dim oldWndProc As Long
Private Sub Form_Load()
oldWndProc =
SetWindowLong(Me.Hwnd,
GWL_WNDPROC, AddressOf
MyWndProc)
End Sub

Private Sub Form_Unload()


Ret& = SetWindowLong(Me.Hwnd,
GWL_WNDPROC, oldWndProc)
End Sub
Function MyWndProc(ByVal Hwnd As
Long, ByVal wMsg as Long, _
ByVal wParam As
Long, ByVal lParam As Long)
Debug.Print wMsg & " " & wParam &
" " & lParam
Ret& = CallWindowProc(oldWndProc,
Hwnd, wMsg, wParam, lParam)

End Function

Handling Parameters
Sometimes the functions do not return the information you need the way
you want it. Typical example is combining two integer(2-byte) values
specifying the mouse position into one 4-byte value. Another case is
telling you that if bit 29 is on it means something. Also, you may receive a
Long value that is the address of a structure.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Combining or separating values does not need any description.


APIMacro.bas you may find on our site
(www.geocities.com/SiliconValley/Lab/1632/) contains all functions you
need to have.
To check if bit N of Value is on use the following:
If Value and (2^N) then ...
To set a bit on:
Value = Value Or 2^N
To set a bit off:
Value = Value And Not 2^N
If you set and get the state of a bit you know beforehand, its much faster to
replace 2^10 with 1024. This way VB won't have to calculate it itself (VB
"hates" ^).
If you receive a pointer to a type, then you must do a bit more. To get the
info, you use CopyMem function. Here is the declaration:
Declare Sub CopyMem Lib "kernel32"
Alias "RtlMoveMemory" _
(pDest As Any, pSource
As Any, ByVal ByteLen As Long)
If you receive a pointer to RECT type in the Long variable Addr, use the
following:
Dim Info As Rect
Call CopyMem(Info, ByVal Addr, len(Info))
Note the ByVal keyword. Now, if you need to put the information back,
use:
Call CopyMem(ByVal Addr, Info,
Len(Info))

Closing Words
I hope this tutorial has helped you understand how to control the power of
API functions and how to use them properly. But BEWARE! It's like the
fire, you let him out of control and you are lost. And, of course, never
forget that VB is designed for easy and safe programing and API foes
straight against. If you are looking for more control and power, better
move to VC++.
To expand your knowledge of APIs and get some expeience, be sure to
take a look at the sample on this site
(www.geocities.com/SiliconValley/Lab/1632/). Almost all of them are
dedicated to API and its advantages.

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html


Helpmate

Also, if you have any questions, something has left misunderstood, or


whatever it is, be sure to e-mail. I'll try to solve your problem.
Good luck, exploring the API !!!

No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html

Vous aimerez peut-être aussi