VB1
VB1
VB1
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 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
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?
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
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
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
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
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
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
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
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:
<Assembly: AssemblyKeyFile("widgetsusa.snk")>
<Assembly: AssemblyVersion("1.0.0.1")>
Imports System.Runtime.CompilerServices
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
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
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
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
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
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
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
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
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
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
module modMain
Sub Main( )
Dim Acct As Checking = New Checking
End Sub
end module
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
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
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
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
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
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
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
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
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
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");
[
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
<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
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
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
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.
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.
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
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
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.
<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
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.
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.
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
Implements IAccount
Implements IAccount2
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.
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
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
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.
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
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
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
BitwiseImp = bRet
End Function
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
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.
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.
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:
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.
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.
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
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.
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>
<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" />
<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>
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">
</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
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
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]
public DataSet GetAuthor(string sSSN)
{
OleDbDataAdapter oDBAdapter;
DataSet oDS;
[WebMethod(MessageName="GetBooksByAuthor",
Description="Find books by author's SSN.")]
public DataSet GetBooks(string sAuthorSSN)
{
OleDbDataAdapter oDBAdapter;
DataSet oDS;
return oDS;
}
[WebMethod]
public DataSet GetBooks( )
{
OleDbDataAdapter oDBAdapter;
DataSet 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.
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
actual values.
HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
Content-Length: length
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
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>
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;
/* Create a proxy. */
PubsWS oProxy = new PubsWS( );
/* 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);
}
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( )
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
<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>
</s:element>
</types>
<!-- 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>
</output>
</operation>
</binding>
</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
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
' Transform the XML document into an HTML document and display
myWebBrowser.Document.Write CStr(oDOM.transformNode(oXSL))
myWebBrowser.Document.Close
' Transform the XML document into an HTML document and display
myWebBrowser.Document.Write oDOM.transformNode(oXSL)
myWebBrowser.Document.Close
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>
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
Dim sB As String
oXMLHTTP.send sB
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" ...>
</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>
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.
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.
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.
Object Functionality
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.
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:
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.
Conn.open “DSN=myDB;UID=sa;Password=;”
objRec.ActiceConnection = conn
ObjRec.MoveNext
Wend
ObjRec.Close
Conn.Close
StrConnect = “DSN=myDB;UID=sa;Password=;”
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
'FileSystem, FreeSpace
rst.CursorLocation = aduseclient
rst.Fields.Append "Type",adBSTR
rst.Fields.Append "FileSystem",adBSTR
rst.Fields.Append "FreeSpace",adCurrency
rst.Open
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
s = ""
rst.movefirst
next
s = s & vbcrlf
rst.MoveNext
wend
msgbox s
'============================================================
'Pad(str, 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
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.
'
' do something here
'
rec1.Close
rec2.Close
rec3.Close
'
' do something here
'
rec1.Close
rec2.Close
rec3.Close
con.Close
com.ActiveConnection = _
"Provider=Microsoft.Jet.OLEDB.4.0;" _
& "Data Source=NWind.mdb;"
MsgBox com.ActiveConnection.ConnectionString
rst.Close
con.Provider = "SQLOLEDB"
con.Properties("Prompt") = adPromptAlways
con.Open
'
' the user will be prompted for the database
'
con.Close
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.
con.Open "Provider=SQLOLEDB;" _
& "Server=localhost;" _
& "Initial Catalog=Northwind;" _
& "User ID=sa;"
com.Parameters.Append par
com.Parameters.Append par
com.CommandText = _
"EXECUTE SalesByCategory 'Produce', '1997'"
'
' do something here
'
rst.Close
con.Close
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.
str.Open
str.Position = 0
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.
9. Nest transactions.
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
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
'
con.CommitTrans
con.RollbackTrans
con.CommitTrans
con.CommitTrans
con.Close
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.
con.Provider = "MSDataShape"
con.Open _
"Data Provider=Microsoft.Jet.OLEDB.4.0;" _
& "Data Source=Biblio.mdb;"
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
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
• 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.
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)
mdClaimValue = dVal
End Property
Client code:
Private WithEvents oServer As clsServer
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.
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.
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
Exit Function
SendReturnEMail_Err:
MsgBox Err.Number & vbCrLf & Err.Description & vbCrLf _
& Err.Source
End Function
'quit Excel
oXLApp.Quit
OutputToExcel = True
Exit Function
cmdExcel_Err:
MsgBox Err.Description & vbCrLf & Err.Number & _
vbCrLf & Err.Source
End Function
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.
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.
• 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
sValue = UCase(sValue)
testValue = sValue
End Function
• 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.
#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
sWindowTitle = String(512, 0)
EnumCallBackProc = 1
End Function
lstWindowTitles.Clear
lReturn = EnumWindows(AddressOf EnumCallBackProc, _
lstWindowTitles)
End Sub
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
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
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.
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()
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
adorRecords.Close
Set adorRecords = Nothing
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
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
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
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
lWinVersion = GetVersion()
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))
• 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
blnFlag = True
Do While blnFlag
lngCtr = lngCtr + 1
DoEvents
Loop
MsgBox "Loop interrupted after " & lngCtr & _
" iterations."
End Sub
blnFlag = False
End Sub
• 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.
• 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.
sErrDesc = String(256, 0)
lReturnLen = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM _
Or FORMAT_MESSAGE_IGNORE_INSERTS, _
lpNotUsed, lErrCode, 0&, sErrDesc, _
Len(sErrDesc), ByVal lpNotUsed)
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
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.
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)
Example
Dim sKeys() As String
Dim sFiltered() As String
Dim sMatch As String
Dim blnSwitch As Boolean
Dim oDict As Dictionary
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.
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.
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 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
ProgressBar1.Max = MaxProgress
ProgressBar1.Value = Progress
End Sub
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
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?
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?
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.
"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
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.
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.
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.
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
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
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
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
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
• 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.
• 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
[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).
• 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.
[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.
• 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
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:
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.
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.
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:
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.
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
Dim rs As ADODB.Recordset
Set rs = conn.Execute(yadayadayada)
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.
If (success) Then
If (more success) Then
If (greater success) Then
Foo = True
Else
Foo = False
End If
Else
Foo = False
End If
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.
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.
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.
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.
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.
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.
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.
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,
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
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
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
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
End Function
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
RaiseEvent Timer
End Function
oTimer.RaiseTimerEvent
End Sub
End Function
End Sub
Command1_Err:
MsgBox Err.Description
End Sub
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
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
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()
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
dynamic array.
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.
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.
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
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.
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,
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.
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
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'),
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!']
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.
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!
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).
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!
#ifdef UNICODE
#else
#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.
• vbFromUnicode
These constants convert the character array of the BSTR between Unicode
and ANSI.
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.
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
s = "help"
sp = StrPtr(s)
Debug.Print "StrPtr:" & sp
vp = VarPtr(s)
Debug.Print "VarPtr:" & vp
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
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
Dim i As Integer
Dim sString As String
Dim bBuf(1 To 10) As Byte
sString = "help"
' 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
' 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
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.
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:
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.
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
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:
Next i
Form1.lstMain.Refresh
End Sub
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
) 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)
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;
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
cBytes = LenB(sBSTR)
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
cBytes = LenB(sBSTR)
End Function
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
End Function
sBSTR = "help"
End If
End Sub
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
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
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
Next
End Sub
The output is:
Before:1760168
After:1760168
Test1
Before:1760168
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.
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
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
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.
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.
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
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 *
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.
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.
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
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,
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, _
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:
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.
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
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
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.
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.