vb1

234
Helpmate COM+ Programming with Visual Basic Developing COM+ Servers with COM, COM+, and .NET Introduction to .NET At the time I started writing this chapter, .NET had entered its Beta 1 cycle. Even though this product is only in Beta 1 (and if you have read the documentation shipped with the .NET SDK, you will see that there is at least a Beta 2 planned), I know that with the Microsoft marketing muscle, many of you may be feeling in some ways as if you are already behind for not having already converted all your applications to use .NET. The reality is that .NET is a brand new architecture; it is not the next version of COM+. What's more, all the Microsoft compilers that were written before have to be rewritten to emit code compatible with the new architecture. In many cases, the language constructs themselves have also been rewritten. Visual Basic, for example, has gone through many syntactical changes--so many that some may argue it is not the same language. In this chapter, you are first going to get an introduction to the .NET architecture, then you are going to get an overview of some of the new features in VB.NET, and after you have an understanding of how to use the features, you will learn about how to mix .NET components with COM+ components. Because I am currently using beta software, the information in this chapter is subject to change. There is no way that I can pretend that this chapter will give all the information necessary to be a .NET developer, but it is my hope that you will learn enough to satisfy your curiosity. The .NET Architecture Why are we talking about .NET and not the next version of COM or COM+? .NET in fact is a brand new architecture with few things related to the current architecture. So what is wrong with COM and why is Microsoft going in a different direction? Well, before we can point out the benefits of .NET over COM/COM+, let's talk about the architecture itself. First, how do you get .NET? .NET comes in two main parts. One of the parts is the .NET SDK. The .NET SDK team builds what was previously known as the Universal Runtime (URT) and is now called the Common Language Runtime (CLR). It is a runtime environment that includes a loader for .NET code, a verifier, certain utilities, and a number of .NET DLLs that compose what is called the .NET Common Type System. The SDK also includes four command-line compilers: one for VB.NET, one for a new language called C# (C-Sharp), a new version of the C++ compiler and linker that produces what is called managed C++ (or MC++), and one for the Intermediate Language (IL), which you will learn No license: PDF produced by PStill (c) F. Siegert - http://www.this.net/~frank/pstill.html

Upload: vijini-pathiraja

Post on 08-Apr-2015

180 views

Category:

Documents


1 download

TRANSCRIPT

Page 1: VB1

Helpmate

COM+ Programming with Visual BasicDeveloping COM+ Servers with COM, COM+, and .NET

Introduction to .NETAt the time I started writing this chapter, .NET had entered its Beta 1cycle. Even though this product is only in Beta 1 (and if you have read thedocumentation shipped with the .NET SDK, you will see that there is atleast 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 fornot having already converted all your applications to use .NET. The realityis that .NET is a brand new architecture; it is not the next version ofCOM+. What's more, all the Microsoft compilers that were written beforehave to be rewritten to emit code compatible with the new architecture. Inmany cases, the language constructs themselves have also been rewritten.Visual Basic, for example, has gone through many syntactical changes--somany that some may argue it is not the same language.In this chapter, you are first going to get an introduction to the .NETarchitecture, then you are going to get an overview of some of the newfeatures in VB.NET, and after you have an understanding of how to usethe features, you will learn about how to mix .NET components withCOM+ components. Because I am currently using beta software, theinformation in this chapter is subject to change. There is no way that I canpretend 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 satisfyyour curiosity.

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

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

Page 2: VB1

Helpmate

about shortly. A number of other language compilers are also beingdeveloped by third parties to emit .NET-compatible code, such asCobol.NET, Component Pascal, Eiffel, and others.The second part of .NET is called Visual Studio.NET. Visual Studio.NETis composed of the IDE that you use to write code, several programmingtools, and online documentation. Visual Studio.NET uses the .NET SDKcompilers to compile your program. In reality, if you like usingNotePad.exe, you do not need Visual Studio.NET; you could write yourVB programs in Notepad and then run the command-line compiler.One important thing to understand about .NET is that it is a lot more thanits name implies. At first glance, it may seem like a technology gearedtoward writing Internet applications. Although this is true in some sense, itis a lot more than that. .NET is primarily an architecture for writingapplications that are object-oriented in nature, and both hardware andoperating system agnostic. You may have heard similar claims fromanother language--Java. Under the covers, .NET is very different fromJava, but conceptually the two architectures have the same goal.

ILThe heart of .NET is the Intermediate Language (IL). IL is a hardware-independent object-oriented form of assembly language. The followingcode shows a "hello world" program written in IL:.assembly hello { }.assembly extern mscorlib { }.method static public void main( ) il managed {

.entrypoint

.maxstack 1ldstr "Hello World"call void [mscorlib]System.Console::WriteLine(class System.String)ret

}A line-by-line discussion of the preceding code is beyond the scope of thisbook; however, we will discuss some of the most interesting parts of thecode shortly. Notice for now that the code resembles assembly languagebut uses a different set of commands, and it does look like a much higher-level language than pure .asm.You could take the previous code and save it as a text file with Notepad,giving it the extension .IL (helloworld.il, for example). If you have the.NET SDK installed, you could run the IL compiler from the commandline by entering the following command:ILASM helloworld.ilThe output of that command would be the executable helloworld.exe. Youcould then run the program and witness "Hello World" appear on yourconsole.IL-generated code is processor and operating system independent. The ILsource code must be changed to native code before it is run. IL programsare not interpreted; they are instead converted to native code using one ofthree compilers provided in the .NET SDK.

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

Page 3: VB1

Helpmate

The default compiler is the Just-in-Time (JIT) compiler. The JIT compilertakes IL and first compiles the entry-point function and any code that thefunction needs; then as the code executes, any other code that that codeneeds is also compiled; and so on. Sometimes some of the earliercompiled code may be thrown out from memory to make room for othercode, then recompiled when needed.Another option for compilation is to use the EconoJIT compiler, which isdue to come out in a future release of the Platform SDK. The EconoJITcompiler does the same job as the JIT compiler, but it produces lessefficient code. Sometimes developers feel that having code compiled atruntime 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 JITcompiled than when it is compiled in a traditional way. The reason for thisis that the JIT compiler can take into consideration your hardware andoptimize the code to function well with it. If you think about it, when codeis precompiled from the factory, it follows a "one size fits all" approach; itis often optimized to run on a machine that has an Intel processor. If youran your program on a machine with an AMD processor, a JIT compilerwould be able to use the AMD extensions as needed. This is the way thatthe JIT compiler is supposed to work, and it produces high-qualitymachine code at the price of load time. The EconoJIT compiler, on theother hand, compiles faster at the cost of execution performance.The third type of compilation is called OptJIT. The OptJIT compiler is dueto come out in a future release, but the idea is that some third-partyvendors will emit a subset of IL called Optimized IL (OptIL). OptIL is ILwith instructions embedded into it that tell the OptJIT compiler how tooptimize its output for certain tasks. For example, the third-party languagemay be optimized to do mathematical calculations and would like thegenerated code to do mathematical calculations in a certain fashion. Thethird-party OptIL output would embed information in the IL that wouldtell the OptJIT compiler how to optimize calculation code when itgenerates 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 versionsof Windows (probably beyond Windows XP) will be IL ready. This meansthat the operating system may be able to see a text file with IL in it andrun it as is. However, Windows 2000 cannot do this, so IL must beembedded into an executable or a DLL. The ILASM compiler can take theIL and build a PE file around it. PE stands for portable executable and isthe format that .EXE files and .DLL files use. The PE wrapper that theILASM compiler generates has code to invoke the runtime loader found inmswks.dll and other files shipped with the .NET SDK. The PE file has theIL embedded in it. The IL may be embedded as "text," although someformatting of the text is done to make it easier to parse, or it may beprejitted (turned into native code). Microsoft will ship some files that have

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

Page 4: VB1

Helpmate

IL already changed to native code or prejitted. The runtime is able to runcompiled IL code or text IL code.Most of the time, you will not be writing IL code from scratch. Instead,you will use your language of choice. A number of language compilershave been rewritten to generate IL instead of native code. Visual Basic isone of these languages. In addition Microsoft has created a brand newlanguage called C# (C-Sharp). C# is a C++-like language that in manyways also resembles Visual Basic. It eliminates a number of features fromC++ that, although they provided a lot of "power," also produced a lot ofconfusion. For example, C# does not have pointers. It also does not havemacros or templates. The successor to VB 6 is VB.NET. VB.NET is acompletely new version of Visual Basic. Many things have changed, andlater in this chapter you will learn about some of the new features.

AssembliesAlong with a new form of assembly language and a new set of compilers,Microsoft has also redefined what it means to be an application. If youthink about the current operating system boundaries, there are two mainentities: processes and threads. Threads give us an order of execution. Youmay recall from Chapter 5 that a program may launch another thread inorder to do two tasks seemingly simultaneously. A process determinesprimarily a memory boundary. Two processes do not share memory. Theirmemory is isolated, and although it is possible to share memory usinglow-level functions, that is not the standard. However, more than amemory boundary, the process also serves as a security boundary. InChapter 10, you learned that each process in the operating system runsunder a certain set of credentials.Microsoft has redefined what it means to be a process. In fact, the newworld does not address processes per se; the new world uses assemblies.You're probably wondering whether assemblies are DLLs or EXEs. Theanswer is that assemblies are neither (it is almost better to forget thatEXEs and DLLs ever existed). In many ways, processes are bettermatched to a new boundary in the .NET architecture known asAppDomains.As you already know, the runtime runs IL. A developer may declare onefile or a number of files containing IL as being part of an assembly. Anassembly is the smallest unit that can be versioned; it determines theboundary for which classes are made public or private; it is also a unit thatcan be secured. Because IL files are packaged in EXEs and DLLs, anassembly can be a single EXE, a single DLL, or a combination of EXEsand DLLs. An assembly may also contain other files, such as resourcefiles and even help files. Figure 11-1 shows the relationship betweenEXEs, DLLs, and assemblies.Figure 11-1. The relationship between modules, assemblies, EXEs, and DLLs

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

Page 5: VB1

Helpmate

Each image file containing valid IL is called a module. To be an assembly,one of the files in the group must have an assembly manifest. Theassembly manifest is created with an assembly definition. If you look atthe "hello world" example presented earlier, the code begins with thedirective .assembly hello. Without the .assembly directive, the assemblerwould produce only a module. Having the .assembly directive does twothings: it declares an assembly with the name hello, and it creates theassembly manifest.A manifest is metadata, which is a fancy word for descriptive text. Youcan think of the manifest as the type library for the assembly. It definesproperties of the assembly such as the version number of the assembly andthe culture (or locale) that the assembly was built for. The manifest alsolists all the modules, all the files, and all the external assemblies on whichthe assembly depends. For example, if your assembly requires data access,you will need to reference the Microsoft.Data assembly. In the earlier ILexample, the second line of code, .assembly extern mscorlib, tells the loaderthat the assembly uses a system assembly known as mscorlib. The reasonthe code needs to reference this assembly is because it uses a class calledSystem.Console. This class has a method called WriteLine that the ILcode uses to output "Hello World" to the console.As you may suspect, Microsoft provides a number of prebuilt assembliesthat you may use. These assemblies contain a series of public classes,interfaces, and attributes (you will learn about attributes shortly). Thesesets of classes, interfaces, and attributes constitute the .NET CommonType System.

The Common Type SystemIf you think about the type system in Visual Basic 6, you may separate thetypes into two main groups: objects and intrinsic types. We can say thatintrinsic types include things like integers, strings, doubles, and so on,while object types refer to classes you define. C++ has its own typesystem. It also includes some native types like int, double, short, and long,and it includes object types--classes that you define. C++ also has severalclass libraries, among them the Microsoft Foundation Classes (MFC) andthe Active Template Library. If you look at a third-generation languagelike Delphi, for example, you see that that language also has its own typesystem. A common problem was making these type systems communicatewith one another. COM+ handled this by letting each compiler decide howthey were going to map types to a few C++ types. If you wanted yourcomponent to be VB compatible, you had to figure out what subset of allthe C++ types mapped neatly to VB types, and the VB compiler had tolook at a C++ interface definition and translate it as best it could to VB.To resolve most of the issues of compatibility between languages,Microsoft is also introducing a common type system. I say most, becauseit turns out that every type that can be represented in the type system is notnecessarily available to every language. Instead Microsoft defines theCommon Language Subset (or CLS) to be a subset of the type system that

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

Page 6: VB1

Helpmate

every language should support. However, for the most part, having acommon type system means that every language that produces IL knowshow to use types declared in any other language. Three things make thetype system particularly interesting:

• Every object has the same root object: System.Object.

• Classes are self-describing. Through a set of classes that definereflection, a developer can find out from a running object all theinformation about the class that was used to generate that object.

• Microsoft decided to distinguish between reference types and valuetypes. A reference type is a reference to an object that is allocatedon the heap. A value type is an object allocated on the stack. Theconcept of value types is nothing new; after all, in VB 6 we havethings like Integers, Doubles, Singles, and UDTs. These are allexamples of value types.

What makes value types in the new type system different is that they alsoderive ultimately from System.Object. In other words, even value typesare classes with methods, fields, and interface implementations. Forexample, when you dim a variable as type Integer, VB turns thatdeclaration into a variable of type System.Int32. System.Int32 is a class.What distinguishes a value type from other classes is that value types arederived from a class called System.ValueType. Sometimes it may benecessary to take a value type and cast it to a variable of typeSystem.Object (think of System.Object as the VB 6 Variant type or theVB 6 Object type). However, System.Object declarations are referencetypes, and the runtime treats them differently from value types. To allowthis conversion, the runtime supports an operation known as boxing.Boxing means that the system duplicates the data stored in the value typeand creates a copy of the object on the heap. The reverse procedure, inwhich a value type is created from a reference type and the data isreplicated once again, is called unboxing. You are going to see an examplelater on in the chapter.To make it possible for every object, including value types, to derive fromSystem.Object, the CLR uses inheritance. There are two types ofinheritance in the system: class inheritance and interface inheritance. Youcan inherit from only a single class, and, in fact, every class must inheritfrom at least one class, System.Object. On the other hand, you canimplement any number of interfaces. That stated, let me complicate thingsby saying that interfaces are also classes derived from System.Object.However, they are a special type of class marked as abstract, and everymember in the interface is really a pointer to a function (the concept ofvtables to vptrs is still the same).

Why .NET and Not COM+ 2.0?

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

Page 7: VB1

Helpmate

The first question that people often have is why Microsoft had to come upwith a different component technology. Why not improve COM+? Let'stalk about some of the limitations in COM and how .NET addresses them.One problem with COM+ was the lack of a common type system. Wehave talked a little about this problem. To summarize, each languageinvolved in COM had its own type system, and the best the compilerscould do was match a type from one language to another by the amount ofmemory that the type consumed and the semantics of the type. With .NET,we have a common type system, each language creates types that followthe 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 serverexposed. C++ developers relied primarily on header files that describedthe set of interfaces exposed by the server. VB relied on type libraries.Often an interface would originate from one of the Microsoft groups inC++ syntax. Then a developer would have to write a type library forVisual Basic that had VB-friendly syntax. .NET uses a better approach.Assemblies expose types, and an assembly can be referenced directlywhen building a new assembly. Thus, if you create a program (anassembly) that relies on a database class, for example, when you compileyour assembly, you will tell the compiler to reference the databaseassembly. Visual Studio.NET will give you an easy way to tell thecompiler what assemblies you need. In fact, there is almost no differencevisually between referencing a type library in VB 6 and referencing anassembly. Later on you will see how to expose a class in an assembly andhow to reference the assembly in another assembly.A third problem with COM was that the architecture did not have perfectknowledge of the types in your process. For example, there was nothing inCOM that told the operating system what COM servers your clientprogram was dependent on at load time. The type library told COM aboutwhat types your server exposed, but the client relied on CreateObject orNew. So there was no way for the OS to know at load time if your EXEneeded a server that wasn't available in the system. The OS didn't knowuntil it executed the line of code that tried to create a type in the serverwhether that server was available. With .NET, the manifest contains a listof not only the types that your server exposes, but also the types that theserver needs. The CLR loader verifies that it can find all the assembliesthat your assembly is dependent on before running your program.Another related problem concerned versioning. How many versions ofADO 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 hadanother version on the system, there was no way to use it side-by-side withthe first one. You would have to register the new version, and that wouldoverride the InProcServer32 key to point exclusively to the new version, andevery program would use the new version. One problem resulting fromthis approach was that there was no guarantee that existing client

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

Page 8: VB1

Helpmate

programs could use the new version of ADO. Nothing in your clientprocess told the OS what version of ADO your program was dependent onand whether you could use a new version or not.In contrast, .NET has an improved versioning scheme. When you build aclient assembly, the manifest tells the loader the version of each assemblythat you are dependent on. In addition, .NET recognizes shared assembliesand private assemblies. Private assemblies are used with just yourapplications. Something like ADO.NET would be a shared assembly--anassembly that many assemblies count on. Shared or public assemblies aresigned with a private and public encryption key. The process of signingthe code produces an originator. Once your assembly has an originator,you may put it in the global access cache (GAC). The GAC physicallylives under the WINNT\Assembly directory and stores a copy of all sharedassemblies. The CLR loader looks at the list of assemblies you arereferencing, and if it does not find a private assembly that is a later versionthan the version the client was compiled against, then it will try to find theassembly in the GAC. The GAC can store multiple versions of the sameassembly. For example, you may have ADO.NET Version 1, 2, and 3 inthe GAC. When you build an assembly, the manifest will contain areference not only to the assembly name, but also to the assembly'sversion number. It could happen that your application requires one versionof the shared assembly and another assembly requires another version, andit could happen that the same process may be running both versions at thesame time. Therefore, .NET now gives you the capability of having side-by-side versions of shared components. Later in this chapter, I will showyou 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 thetypes you used internally was knowing when to release an object frommemory. You can tell if a programmer has done COM+ at the C++ level ifyou ask about circular references. As VB developers, we do not have todeal with things like reference counting and circular references directly,but C++ COM+ developers do. A common problem with COMcomponents 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 areference to object A. B will never be released from memory as long as Ais holding a reference to it, and A will never be released because B isholding a reference to it. This is known as a circular reference..NET does not use reference counting to determine when an object'smemory should be reclaimed. Instead the system implements garbagecollection. Managed code can no longer ask for a chunk of memorydirectly. To take advantage of garbage collection and other services, yourcode creates an instance of a type or of an array of types, and the memoryneeded is allocated by the runtime in a managed heap. When there is nomore memory to hand out, the system will release memory for objects nolonger in use. The garbage collection system differs from referencecounting in the way that memory is cleaned up--no longer is memory

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

Page 9: VB1

Helpmate

reclaimed as long as there are no references to the object. Instead, thesystem waits to release memory until there is need for more memory.There are also commands for telling the garbage collector to collectmemory immediately.You should now have a basic understanding of the .NET architecture andhow .NET improves on the existing COM+ architecture. Shortly, you aregoing to learn about how to have COM+ components using .NETcomponents and vice versa. But before talking about interoperability andabout the new features in VB.NET, let's talk about how to compile andversion .NET assemblies.

Developing AssembliesThere are two ways to create VB.NET applications. One way is to use thenext version of Visual Studio, Visual Studio.NET. Visual Studio.NET is adevelopment environment built on top of the .NET SDK. The .NET SDKis packaged separately from the Visual Studio.NET environment. TheSDK includes a C# command-line compiler, a VB.NET command-linecompiler, and the DLLs and EXEs necessary to run your .NETapplications.I have decided that in order to make the information in this chapter last, Iam not going to use the Visual Studio.NET designer. One reason is thatthe product has not been released yet. Another reason is that the product isnot yet stable. A third reason is that, for the first time, Visual Basic has atrue command-line compiler that can be used in conjunction with NMAKEand MakeFiles. You may be familiar with MakeFiles if you have everworked with C++. MakeFiles are text files that tell NMAKE.EXE how tobuild your program. For all these reasons, I have decided to use the secondmost 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 acommand prompt. Let's start with a simple Hello World application to geta 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 FunctionEnd ClassThis code declares a class called HelloSupport. At first it seems strange tocreate a class for just one function, but in VB.NET every function isexposed 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 inthe class become shared, very much like in the preceding example.So what is Shared, anyway? Classes in VB.NET have two types ofmethods: instance methods and shared (or static) methods. VB 6 classmethods were instance methods: you had to create an instance of the classto use them, and the method performed a task on the data for that instance

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

Page 10: VB1

Helpmate

only. Shared methods are new to VB.NET. They can be executed withoutcreating an instance of the class. The only limitation is that if your classhas both shared methods and instance methods, you cannot call theinstance methods from within the shared methods. You can call sharedmethods only from within shared methods. If you need to call the instancemethod, then you have to create an instance of your class and make themethod call just as any other function outside the class would. You alsocannot use any of the member fields in the class from shared methodsunless the fields are also marked as shared. In a module, the VB.NETcompiler turns all functions to shared.The GetGreeting function returns a string that says "Hello x", where x isthe string that the calling program passed to it. Save the preceding file ashellogreeting.vb. Then run the VB command-line compiler, VBC.EXE. Ifyou installed the SDK, the path to the VBC.EXE compiler should bereflected in the environment so that you can run it from any folder. Open acommand window and switch to the directory where you saved the files,then enter the following command:vbc /t:library hellogreeting.vbThis command creates a DLL file with the name hellogreeting.dll. If youlook at the command line, you will notice that the first option is the /tswitch, which tells the compiler the target type. VB.NET is able toproduce Windows applications, console applications, DLLs, and modulesthat can be linked with other modules to produce a multimodule assembly.They can be packaged as EXEs or as DLLs. An assembly is the smallestunit 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 tocreate an executable that can use the function in the DLL. Create a newfile in Notepad and enter the following text:class Helloworld Shared Sub Main( ) Dim sName As String = "World" Dim sGreeting As String = HelloSupport.GetGreeting(sName) System.Console.WriteLine(sGreeting) End Sub End ClassSave the file as helloworld.vb. To compile the program, use the followingcommand line:vbc /t:exe helloworld.vb /r:hellogreeting.dllThe preceding code declares a class called Helloworld. The class has aprocedure called Sub Main. The Sub Main function is the equivalent ofSub Main in VB 6. The only difference is that the function must bedeclared 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 nowdeclare variables and assign them a value all in the same line, which iswhat I have done in the first line of code inside Sub Main. The function

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

Page 11: VB1

Helpmate

declares two variables: sName holds the name that the GetGreetingfunction will use for the greeting, and sGreeting holds the response fromthe GetGreeting function. Notice that to use the GetGreeting function, youhave to qualify it with the name of the class. Because the GetGreetingfunction was declared as a shared function, you do not have to create aninstance of the class to use it. The code in Sub Main then prints thegreeting to the console using the System.Console.WriteLine function.System.Console.WriteLine is a new function in the CLR. The function ispart of the System assembly. Microsoft has defined a set of classes thatgives support for the operating system functionality. Not only are theseclasses a replacement for using the WIN32 API functions directly, but alsofor other COM libraries that Microsoft ships, such as MSXML and ADO.The runtime has assemblies that Microsoft has included for XML, forHTTP communication, for data access, for threading, for IISprogramming--for practically anything you can think of. There is still away to call WIN32 APIs directly using the Interop classes; however,jumping outside of the runtime is a costly operation, and you arediscouraged from doing so.To build the executable, you must specify the name of the DLL (and thepath to the DLL) that your executable needs to resolve all functions. Theresult of running the previous command-line command is theHelloworld.exe image. If you run helloworld.exe, you should see thephrase "Hello World" displayed in the command prompt.Microsoft ships a disassembler with the SDK called ILDASM.EXE. Let'srun ILDASM.EXE on the resulting executable to see how thehelloworld.exe assembly references the hellogreeting.dll assembly. Runthe following command from the command prompt:ILDASM.EXE HelloWorld.EXEYou should see the window in Figure 11-2.Figure 11-2. ILDASM program

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

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

Page 12: VB1

Helpmate

.assembly extern hellogreeting{ .hash = (12 09 10 58 91 53 C7 13 7D 3D 53 87 A4 62 79 4F // ...X.S..} =S..byO 14 63 47 99 ) // .cG. .ver 1:0:0:0}.assembly hellowor ld as " hellowor ld"{ .hash algorithm 0x00008004 .ver 1:0:0:0}.module helloworld.exe// MVID: {4C781D06-B3E4-4376-A19D-E068C05D7A39}The manifest shows a list of assemblies that your assembly is dependenton. That is nothing new; after all, before COM, DLLs had a list ofimported types that you could see with the depends.exe program.However, even with the depends.exe tool, the system had no idea whatDLLs you were loading dynamically in code. For example, the OS knewthat your VB program was dependent on MSVBVM60.DLL (the VBruntime), but it had no knowledge of any other DLLs you might have beenusing through Declare statements, not to mention any COM DLLs youloaded with CreateObject. What is also interesting is the differencebetween private assemblies and public assemblies. If you notice, themanifest shows that you are dependent on Version 1.0.0.0 of thehellogreeting assembly. The version number for this assembly is notcrucial because hellogreeting is not a public assembly; it is meant to beused only with this application. If you look at the rest of the manifest,however, you will notice that the helloworld assembly is also dependenton the mscorlib assembly and the Microsoft.VisualBasic assembly. Theselatter assemblies are public assemblies; the creator went through theprocess of creating a public assembly, and because it is a shared assembly,the rules for using the assembly with our program are more stringent. Forexample, we have to tell the runtime whether our assembly can use anewer version of Microsoft Visual Basic or whether it must have the sameassembly it was built with.

Versioning AssembliesTo illustrate the concept of versioning in the CLR, let's turn thehellogreeting assembly into a shared assembly, install it to the GAC, thenbuild a second version of the assembly and create a configuration file tocontrol which version of hellogreeting the helloworld assembly will use.The first step is to digitally sign the code with a public/private digital key.It is impossible for us to discuss the encryption algorithm in this chapter,but the basic idea is that with a public/private key, encryption is done withthe private portion of the key and decryption is done with the publicportion. The various compilers first run an algorithm over the manifestinformation and produce what is called a hash. Then they use thepublic/private key file (from now on referred to as a private key file) andrun an encryption algorithm through this hash to encrypt it. The signature

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

Page 13: VB1

Helpmate

is then embedded into the image. Also, the manifest for the signedassembly includes an originator field. This field advertises the public keyto the world. Anyone can therefore use this public key to decrypt theencrypted manifest hash, run the hash over the manifest once again, anddetect if there was any tampering to the manifest itself. In addition, anyclient programs referencing the assembly have a token of the public keyknown as the public token. This token is the result of a one-way hashalgorithm run through the public key. Anyone can run the same algorithm.This step is done to verify that the assembly the runtime has found isreally the assembly that the client program was compiled against.To generate the private key, you run sn.exe, the strong name utility thatships 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 upentirely, although tools like VS.NET look for the .SNK extension. I'musing the name of a fictional company, since a company would likely useits name for the filename and use this key to sign every shared assembly itproduces.Once you have generated the private key file, now you can sign yourassembly with that code. Modify the hellogreeting.vb source code asfollows:<Assembly: AssemblyKeyFile(" widgetsusa.snk" )><Assembly: AssemblyVersion(" 1.0.0.0" )> Impor ts System.Runtime.CompilerServices Public class HelloSupport Shared Public Function GetGreeting(ByVal sName As String) As String return "Hello " + sName End FunctionEnd ClassNotice that there are three new lines of code at the beginning. The firstline of code uses an assembly attribute. Attributes are specified with anglebrackets and are classes derived from System.Attribute. They are specialclasses that provide tools with configuration information. They can bespecified at the assembly level, the module level, the class level, and evenat the method level. The first attribute specifies the name of the privatekey file. The second attribute specifies the version number of theassembly. Notice that the third line in the code asks the compiler to useanother assembly called System.Runtime.CompilerServices. We need toinclude a reference to that assembly, because that is where theAssemblyKeyFile and AssemblyVersion attributes are declared. Thecompiler will look for these attributes and, if it finds the AssemblyKeyFileattribute, it will run a cryptographic algorithm on the manifest for the codeusing the private/public key file stored in the file you specified. Thevisible effect of signing your code is that you will now see an originator

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

Page 14: VB1

Helpmate

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

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

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

Page 15: VB1

Helpmate

<Assembly: AssemblyKeyFile("widgetsusa.snk")><Assembly: AssemblyVersion("1.0.0.1")> Imports System.Runtime.CompilerServices Public class HelloSupport Shared Public Function GetGreeting(ByVal sName As String) As String return " New Hello " + sName End FunctionEnd ClassNext, rebuild with the same version number as before. If you run theprogram again, you will notice that the client program still outputs "HelloWorld"--it uses the assembly in the cache. What if we recompile with theversion number of 1.0.0.1? Then the story changes. The client programuses the local assembly instead of the one in the GAC. We can move thenew version to the GAC as follows:gacutil -i hellogreeting.dllThis is the exact same command as before--nothing interesting, unless youlook at the assembly list in WINNT\Assembly once again (see Figure 11-4).Figure 11-4. Global Assembly cache with two versions of the assembly

You can see that there are two versions of the assembly in the cache. Ifyou run the client program again, it would use the latest version. However,let's make another change to the code:<Assembly: AssemblyKeyFile("widgetsusa.snk")><Assembly: AssemblyVersion("2.0.0.0")> Imports System.Runtime.CompilerServices Public class HelloSupport Shared Public Function GetGreeting(ByVal sName As String) As String return " New Hello 2.0.0.0" + sName End FunctionEnd ClassThis time, build the assembly as Version 2.0.0.0. Once you build the DLL,if you run the client once again, the client will use the version of the DLLin your local directory. The change occurs when you move this version tothe GAC and delete the local copy. The GAC, according to our examples,so far contains three versions of the DLL. The latest version is 2.0.0.0. Ifyou run the client program this time, you should see that the clientprogram continues to use Version 1.0.0.1. What has happened is that theCLR is conscious of the difference between a major/minor release and abuild/revision release. If your version number differs only bybuild/revision number, then by default the CLR assumes you can use alater version. On the other hand, if your version number differs bymajor/minor numbers, then the CLR assumes that you are better off withyour previous version--it only gives you the version that matches themajor/minor numbers exactly.

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

Page 16: VB1

Helpmate

In either case, by default, if you have a private copy in your directory, theCLR 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 aconfiguration file. The following code shows an application configurationfile:<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 executablebut 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 Nameattribute. The Name attribute points to the name of the assembly that needsto 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 fromthe GAC. If you look back at Figure 11-4, you will see the Originatorfield. The third attribute is Version, the version number. This attribute letsyou redirect any requests for a certain revision number. In this case, weare redirecting any requests. The next attribute is VersionNew; this is theversion number we would like to use. Finally, there is aUseLatestBuildRevision attribute, which is set to yes in our example. Thisattribute 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, itwould use that one.What if you wanted to use Version 1.0.0.0? After all, that is exactly theversion that the client program was built in. By default, the resolver usesthe 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 theUseLatestBuildRevision attribute to no, as shown in the followingconfiguration 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 adifferent subtag named <AppBindingMode>, as illustrated in the followingcode:<BindingPolicy> <BindingMode> <AppBindingMode Mode="safe"/> </BindingMode></BindingPolicy>This code shows the <AppBindingMode> subtag's Mode attribute set to safe;by default, it is set to normal. When you use safe mode, the resolver tries

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

Page 17: VB1

Helpmate

to locate an assembly with the exact same version number as the one usedto compile the client program. Thus, running helloworld would producethe original output, "HelloWorld." If you were to set the mode equal tonormal or just delete the configuration file, you would obtain the secondoutput, "New Hello World." To obtain the Version 2.0.0.0 output, youwould have to use the redirection technique shown earlier and redirect anyassembly (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 andhow to turn private assemblies into shared assemblies. Now let's discusssome of the more exciting language features in VB.NET.

VB.NET FeaturesNow that you know the basics of running the VB compiler and thedifference between private assemblies and shared assemblies, let's discusssome of the new features in VB.NET. In many ways, VB.NET is adifferent language. The language has been extended to have a number ofobject-oriented features that it did not have before, such as methodinheritance, method overloading, and exception handling.

Inher itanceIn Chapter 2, you learned the difference between interface inheritance andcode inheritance. VB 6 enabled you to do only one type of inheritance--interface inheritance. VB.NET adds support for the second type ofinheritance, code inheritance. The way it works is that you first create abase class. In our ongoing example of a banking application, let's supposethat your design accounted for two main classes: a Checking class and aSavings class. It is likely that these two classes have functionality incommon. In fact, a better design may be to create a base class namedAccount from which these two classes are derived. The following codeshows an application that declares three classes: Account, Checking, andSavings. The Checking and Savings classes inherit all their functionalityfrom the Account class:Public class Account Dim m_Balance As Decimal Public Sub MakeDeposit(ByVal Amount As Decimal) m_Balance += Amount End Sub Public ReadOnly Property Balance( ) As Decimal Get return m_Balance End Get End PropertyEnd Class Public Class CheckingInherits AccountEnd Class

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

Page 18: VB1

Helpmate

Public Class SavingsInherits AccountEnd Class Public Class App Shared Sub Main( ) Dim Acct As Account = new Checking Call Acct.MakeDeposit(5000) System.Console.WriteLine(Acct.Balance) End Sub End ClassThe Account class has a member variable, called m_Balance, that stores aDecimal (Decimal replaces the VB 6 Currency type). The class has aMakeDeposit method that accepts an amount in its parameter and adds theamount to the balance. The class also has a Balance property that reportsthe internal balance. The code then declares two subclasses, Checking andSavings, that inherit all their functionality from the Account class. The restof the code declares the class that will host the Sub Main function. In SubMain we are declaring a class of type Account and assigning an instanceof the class Checking. Why can we assign an instance of Checking to avariable 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; Accountclasses are not Checking classes.The fact that Checking is derived from Account means that we can sendan instance of Checking to a function that receives an Account as itsparameter. Consider the following code:Module GenFunctions Public sub ReportBalance(ByVal AnyAccount As Account) System.Console.WriteLine(AnyAccount.Balance) End SubEnd ModuleThe preceding code defines a module. Even though it seems as if themodule provides a way to export standalone functions without having todeclare a class, in reality, VB changes the module to a class and makesany functions inside of it shared. The previous code declares a functioncalled ReportBalance that reports the balance of any class derived fromAccount. As expected, you can send the function an instance of theChecking class or an instance of the Savings class. In fact, the followingcode should work as expected:Public Class App Shared Sub Main( ) Dim check As Checking = new Checking check.MakeDeposit(500) Dim sav As Savings = new Savings sav.MakeDeposit(100) Call ReportBalance(check) Call ReportBalance(sav) End Sub

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

Page 19: VB1

Helpmate

End ClassSuppose that there is a request for the Savings class to work differently.The MakeDeposit method should add $10 to every deposit (this is a verygood Savings account). That means that we have to change thefunctionality of the MakeDeposit method. The following code shows howyou might do this. First you have to modify the MakeDeposit method inthe Account class as follows:Public class Account Dim m_Balance As Decimal Public Overr idable Sub MakeDeposit(ByVal Amount As Decimal) m_Balance += Amount End SubNotice that there is a change in the MakeDeposit method--it has thekeyword Overridable. It is necessary for the developer writing the Accountclass to have foresight and add the word Overridable to any methods thatmay be modified in subclasses. The code for the Savings class has alsobeen modified as follows:Public Class SavingsInherits Account Public Overrides Sub MakeDeposit(ByVal Amount As Decimal) Amount += 10 MyBase.MakeDeposit(Amount) End SubEnd ClassIn the Savings class we add the MakeDeposit method with the attributeOverrides. The code adds 10 to Amount, then forwards the call to the baseimplementation 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 fromat most one class, and all classes must derive from System.Object. If youdo not specify a class to derive from in code, then the compilerautomatically makes System.Object the base class.Suppose that we add to the Account class a withdrawal method thatsubtracts 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 SubIn the preceding example, MakeWithdrawal subtracts the amount from thebalance only if the resulting balance is greater than or equal to 0. What ifthe MakeWithdrawal method in the Checking account needs to workdifferently? Suppose that the Checking account allows a client tooverdraw the account up to $1,000. If you were to write the followingcode, you would get an error:Public Class CheckingInherits Account Public Overrides Sub MakeWithdrawal(ByVal Amount As Decimal) If m_Balance - Amount >= -1000 Then m_Balance -= Amount

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

Page 20: VB1

Helpmate

End If End SubEnd ClassThe code is attempting to access the m_Balance variable, which is markedas Private in the Account class. The problem is that m_Balance must beprivate to the client but public to the derived class. For this reason,VB.NET has a third category named Protected. You must change thedeclaration of the m_Balance variable as follows:Public class Account Protected m_Balance As DecimalYou can specify that it is illegal to write a derived class from your classwith the NotInheritable attribute. NotInheritable results in what the runtimerefers to as a sealed class. For example, we could have said that theChecking class cannot be inherited as follows:Public NotInheritable Class CheckingOn the other hand, we may want to prevent a developer from creatinginstances of the Account class directly. It may be our rule that a developermust create instances of a derived class. This is done with the MustInheritattribute, as in the following:Public MustInherit Class AccountThis 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 ispossible also to force a developer into not only creating derived classes,but also overriding certain methods. For example, we could have statedthat every derived class must override the MakeWithdrawal method. Ofcourse, that change produces a number of changes. If a developer mustalways override the MakeWithdrawal method in the Account class, thenthe MakeWithdrawal method should not have any code in the Accountclass, and the compiler should issue an error if there is code. In addition,since everyone must override the method, a client cannot just create aninstance of the class. Therefore, the class must also be marked with theMustInherit attribute. Suppose that we marked every method with theMustOverride method; we would have the equivalent of an interface.An interface is a class in which every method must be implemented in aconcrete class. There is a shorthand for defining interfaces in VB.NETusing the keyword Interface instead of the Class keyword. The IAccountinterface 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 DecimalEnd InterfaceThe preceding definition shows the new way of defining interfaces inVisual Basic. Notice that the methods in the interface do not have an EndSub statement (or its equivalent). Also notice that there is no Publicattribute--that is because all the methods must be public in an interface.Another interesting thing is that there is method overloading in an

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

Page 21: VB1

Helpmate

interface (a feature you will learn about shortly). This code is roughlyequivalent 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 DecimalEnd ClassThe only difference is that interfaces do not have a constructor--code thatis executed when an instance of a class is instantiated. The fact that theydo not have constructors means that they cannot be subclassed except byan entity that would also not have a constructor. This means you cannotinherit from an interface unless you are an interface. Classes can producesubclasses, and interfaces can produce subinterfaces (if that were a term).For example, interfaces can derive from other interfaces, as in thefollowing example:Public Interface IAccount2Inherits IAccount Sub CloseAccountEnd InterfaceYou are still required to implement the entire interface. If you implementthe preceding interface in a class, the class will support both the IAccountand IAccount2 interface. Let's take a look at how implementing an interfacehas changed slightly:Public Interface ISaveToDiskPublic Sub Save( )End Interface Public Class CheckingImplements ISaveToDiskPublic Sub Bark( ) Implements ISaveToDisk.SaveEnd SubEnd ClassNotice that you now have to specify in each method implementation whatmethod in the interface you are implementing. You do this by declaringthe method, then adding Implements Interface.Method at the end of themethod declaration.

Method Over loadingA new feature in VB is the ability to do method overloading. Methodoverloading involves having several implementations of the same method,each method with a different set of parameters. For example, you maywant to add to the Account class a second MakeDeposit method that takesas a parameter an instance of a second Account object. The idea is that theclient can transfer money from one account to another by just calling theMakeDeposit method in the Account receiving the money and passing theAccount where the money will come from as the parameter. As in C++,you cannot overload a method if the only difference is the return value--atleast one of the parameters must be different, or the number of parametersmust be different. The following code shows the two MakeDepositmethods:

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

Page 22: VB1

Helpmate

Public Overridable Overloads Sub MakeDeposit(ByVal Amount As Decimal) m_Balance += Amount End Sub Public Overridable Overloads Sub MakeDeposit(ByVal Source As Account) m_Balance += Source.Balance Source.m_Balance = 0 End SubYou do not have to override every form of the overloaded method in aderived class.

Value Versus Reference TypeBy default, to use a class, you must declare a variable of the type of classyou wish to use and create a new instance of the class. In VB 6 there was adifference between a UDT and a class. The same is true for VB.NET. InVB.NET you can define a UDT using the keyword Structure, as in thefollowing:Public Structure MyPointPublic x As LongPublic y As LongEnd StructureStructures are classes that are derived from a class calledSystem.ValueType. Ordinarily, when you create a class that is not derivedfrom System.ValueType, the variable that holds the instance of the objectreally contains a pointer to a vptr that points to the location in memorywhere your data is stored. You must create an instance of the class withthe New operator. If your class is derived from System.ValueType, theruntime treats your class differently. With a structure, you do not have tocall New--when you Dim a variable of the structure type, the systemallocates space for the structure automatically and the variable points tothe storage directly. If you send in an instance of a structure as a parameter(marked as ByVal), the contents of the structure are put on the stack, andthe receiving procedure gets a copy of the structure. If you pass a classinstance to a function as a parameter (also marked as ByVal), then you arepassing the pointer to the class' storage, and the receiving side gets a copyof the pointer. In the case of the structure, any changes made to themembers of the structure do not change the original values in the caller'sstructure. In the case of the class reference, if you change the value of aclass variable, the changes affect the caller's instance of the class, sinceboth the caller and the function share the same instance of the class. Otherexamples of classes that derive from System.ValueType are the Integer,Decimal, and Boolean classes--any basic datatype. (Yes, Integer, Decimal,and Boolean are actually classes in the runtime that are ultimately derivedfrom System.Object.)Structures have more functionality than Types had in VB 6. Structures cannow have methods and can implement interfaces. Perhaps the mostinteresting new feature of structures is Boxing. Boxing creates a clone ofyour structure whenever a reference type is needed. Consider thefollowing code:Public Interface IAccount

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

Page 23: VB1

Helpmate

Sub MakeDeposit(ByVal Amount As Decimal) ReadOnly Property Balance( ) As DecimalEnd Interface Public Structure AccountInfoImplements IAccount Public m_Balance As Decimal Public Sub MakeDeposit(ByVal Amount As Decimal) Implements _ IAccount.MakeDeposit m_Balance += Amount End Sub Public ReadOnly Property Balance( ) As Decimal Implements _ IAccount.Balance Get return m_Balance End Get End PropertyEnd Structure module modMain Sub Main( ) Dim Acct As AccountInfo Acct.MakeDeposit(500) Dim AcctBoxed1 As IAccount AcctBoxed1 = Acct AcctBoxed1.MakeDeposit(300) Dim AcctBoxed2 As IAccount AcctBoxed2 = AcctBoxed1 AcctBoxed2.MakeDeposit(300) System.Console.WriteLine("Acct.Balance=" & Acct.Balance) System.Console.WriteLine("AcctBoxed1.Balance=" & AcctBoxed1.Balance) End Subend moduleThis code defines an interface called IAccount. There should be no surprisesin the interface definition if you have been following along in this book.The interface, as usual, has a MakeDeposit method and a Balanceproperty. I am implementing the interface in a structure calledAccountInfo. This structure has a member called m_Balance to store thebalance. It also implements both the MakeDeposit method and the Balanceproperty.The code example begins by allocating an instance of the AccountInfostructure. Remember that you do not have to call New to allocate astructure's memory. The code then calls the MakeDeposit method toincrease the Balance to 500. Next, the code declares a variable namedAcctBoxed1 of type IAccount. The code then uses the AcctBoxed1variable to make another deposit for $300. This is where it gets tricky. Inthe runtime, an interface is a reference type; it is not a value type, like theAccountInfo structure. So when you assign the reference type to the valuetype, the system creates a copy of the structure and assigns the pointer ofthe copy to the reference type variable. In the preceding example, aftersetting AcctBoxed1 to Acct, there will be two copies of the data membersin memory. The second copy has a starting balance of 500 because that's

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

Page 24: VB1

Helpmate

what the structure had before the copy was made. However, when you callthe MakeDeposit method through the structure, the values of the originalremain at 500, while the balance of the copy increases to 800. The codethen creates a second reference type, AcctBoxed2 from AcctBoxed1.Because both are reference types, AcctBoxed2 is set to point to the samememory as AcctBoxed1. Therefore, after calling MakeDeposit for thethird time, the original balance in Acct1 is still at $500, but the balance ofthe reference type is now at $1,100. In fact, when you output the values inthe last two lines of code, the value for Acct.Balance will be reported as500, and the value for AcctBoxed1.Balance will be 1100.

DelegatesVB 6 had a limitation with function pointers. It was possible to get theaddress of a function in memory with the AddressOf operator. The AddressOfoperator returned a Long with the location in memory of the function, butsomething that I always envied C++ for was that you couldn't take thatLong value and turn it back into a function. For example, C++ lets youdefine what is called a function pointer declaration. You can define afunction signature (function name, parameters, and return value) and usethe definition as a datatype. With this datatype, you can declare a variableto hold the address to a function with the same signature. Then you canmake 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 calleddelegates. Delegates are classes derived from System.Delegate. Thefollowing example shows how to define a delegate. Suppose that you wantto create a general function for our banking server that reports the balancebut that, instead of accepting Account or Checking or Savings, will acceptany class that has a subroutine to report the balance. To do this, we candefine a delegate with the signature for the ReportBalance function asfollows:Delegate Sub ReportBalanceSig( )The delegate declaration defines a datatype that can be set to the AddressOfany function with the same signature. Let's suppose that the BankServerapplication has the following classes:Public Class Checking Public Sub CheckingBalance( ) System.Console.WriteLine("CheckingBalance") End SubEnd ClassPublic Class Savings Public Sub SavingsBalance( ) System.Console.WriteLine("SavingsBalance") End SubEnd ClassNotice that the two classes, Checking and Savings, each has a method toreport the balance, CheckingBalance and SavingsBalance, respectively.These two methods serve the same purpose but do not have the same nameand are not implementing any interface. They do, however, have the same

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

Page 25: VB1

Helpmate

signature. Let's now define the ReportBalance function and the client codein 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 SubEnd moduleThe first function in modMain, ReportBalance, accepts as a parameter afunction of type ReportBalanceSig. It does not matter what the function iscalled; it just needs to have the same signature as the delegate. Notice thatthe code in ReportBalance simply calls the routine that was sent in. (Itlooks a little awkward because calling the function can be done by justwriting the name of the variable holding the function pointer.) The secondfunction in the module is Sub Main. In Sub Main we create an instance ofthe Checking class and then an instance of the Savings class. Then, thefunction calls ReportBalance, passing the address of the CheckingBalancefunction, followed by a second call to the ReportBalance function passingthe address of the SavingsBalance function.

Constructors and FinalizersThere is no more Class_Initialize or Class_Terminate. Every class notderived from System.ValueType and not defined as a structure or aninterface has a default constructor. The default constructor has the nameNew and takes no parameters; it is called when the developer uses the Newoperator. For example, the following code shows how to write code for thedefault constructor in the Checking class:Public Class Checking Private m_Balance As Decimal Public Sub New m_Balance = 0 End Sub Public Function ReportBalance( ) As Decimal return m_Balance End FunctionEnd Class module modMain Sub Main( ) Dim Acct As Checking = New Checking End Subend module

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

Page 26: VB1

Helpmate

In this code example, the runtime calls the New method in the class whenthe developer creates an instance of the class. In this case, this happens inSub Main.A more interesting feature is that you can now add parameterizedconstructors. For example, it may make more sense to require thedeveloper using the Checking class to create the class with an initialbalance as follows:Public Class Checking Private m_Balance As Decimal Public Sub New(ByVal InitialBal As Decimal) m_Balance = InitialBal End Sub Public Function ReportBalance( ) As Decimal return m_Balance End FunctionEnd Class module modMain Sub Main( ) Dim Acct As Checking = New Checking(500) End Subend moduleIn this code, there is a definition for a parameterized constructor. This isdone by adding a New function that receives a parameter, in this case theinitial balance. As soon as you add a parameterized constructor, thecompiler no longer adds the default constructor. This means that thedeveloper cannot just say New Checking without passing a parameter. Asyou can see in the code for Sub Main, the code creates an instance ofChecking passing in the initial balance of $500.As with other functions, you may overload the constructor. In fact, if youwish to have both the parameterized constructor and the defaultconstructor, you could rewrite the class as follows:Public Class Checking Private m_Balance As Decimal Public Overloads Sub New m_Balance = 0 End Sub Public Overloads Sub New(ByVal InitialBal As Decimal) m_Balance = InitialBal End SubThere are things to watch for when your class derives from a base class.Constructors are not inherited. If the base class has a default constructor,the system will call the base constructor first before calling yourconstructor. However, if the base class does not have a default constructor,then you must call a constructor for the base class in your constructor, and

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

Page 27: VB1

Helpmate

the call must be the first call in your constructor. The following codeshows 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 SubEnd Class Public Class Checking 'derived classInherits Account Public Sub New(ByVal InitialBal As Decimal) 'call the base class' constructor MyBase.New(InitialBal) End SubEnd Class module modMain Sub Main( ) Dim Acct As Account = new Checking(500) End Subend moduleThe Account class does not have a default constructor. Therefore, theruntime cannot call the base constructor for your class automatically. Youmust add a constructor to the derived class, then call the base constructorfor your class programmatically. You can refer to your direct base class byusing the MyBase object. Notice that you must call the base constructorfirst before doing anything else in the derived constructor.Just as you can write constructors for your classes, you can also writefinalizers. Finalizers are a little different from destructors because they donot necessarily happen when a client releases your object. When the clientcreates an instance of your class, the object becomes part of the globalmanaged heap. When all clients release their instances of your object, yourobject becomes marked for garbage collection. The garbage collector willcall your finalizer when it is time to destroy your object, and that mayhappen any time after all clients have released their references to yourobject but not sooner. To add a finalizer to the class, you must write aFinalize subroutine. (Interestingly, Finalize is an overridable method inSystem.Object.) The following code shows how to add a Finalize method:Public Class Checking Protected Overrides Sub Finalize( ) 'do cleanup code here End Sub End Class

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

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

Page 28: VB1

Helpmate

handling. With exception handling, you write try...catch blocks. The try partof a try...catch block contains the code that you want to execute. Outside ofthe try block, you write handlers for different kinds of exceptions.Exceptions are classes derived from System.Exception. Because everyexception is generated from System.Exeception, you can also write ageneral exception handler to handle any exception you may get. TheSystem.Exception class has properties that provide very rich errorinformation. Part of the try...catch block is the finally block. The finally blockexecutes at the end of the function whether an exception occurs or not.This block lets you do cleanup for the function. The following code showshow to add a try...catch...finally block:Public Interface IAccountEnd InterfacePublic Interface ISaveToDiskEnd InterfacePublic Class CheckingImplements IAccountEnd Classmodule 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 SubEnd moduleThis code defines two interfaces, IAccount and ISaveToDisk. The code alsodefines a class called Checking. The Checking class implements onlyIAccount. The code in Sub Main creates an instance of the Checking classby asking for the IAccount interface, then tries to convert the type toISaveToDisk using the new VB.NET command CType. Since the class doesnot support the second interface, the system generates an exception:System.InvalidCastException. The code places the cast attempt codewithin a Try block. Notice that there are different exception handlers afterthe code inside the Try block. The first exception handler handlesSystem.InvalidCastExceptions only. If the code generates this exception(and the previous code will), the line System.Console.WriteLine ("Thecast failed") is executed followed by the code in the Finally block. After thehandler for System.InvalidCastException, the code has a general exceptionhandler. This is done with a catch section that either has the word Catchby itself or by catching the System.Exception class.In VB 6, generating an error was done with Err.Raise. In VB.NET you cancreate your own exception class derived fromSystem.ApplicationException. System.ApplicationException is derived

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

Page 29: VB1

Helpmate

from System.Exception. Then use the Throw command to generate theexception. The following code shows you how to define your ownexception class and generate it:Public class MyException Inherits System.ApplicationException Sub New( ) MyBase.New("My Error Description") End SubEnd classModule modMain Sub Main( ) Dim e As MyException = new MyException Throw e End SubEnd Module

Mixing COM+ and .NETMany of you are feeling the urge to experiment with .NET componentsand like the new features in Visual Basic. Most likely, you have alreadymade an investment in COM and would like to know how to use yourexisting COM components in VB 6 with the new .NET components.Microsoft is very aware of this need and has built in capabilities formixing the two.As you know from the previous sections in this chapter, code that runs inthe CLR is managed code. It is in IL, and it is run by the commonlanguage runtime. The runtime has its own techniques for allocatingobjects and managing things like thread allocations and so forth. In the VB6 world, code was unmanaged. The compiler translated your code tonative code that the processor could run. Thus, if we are going to mixthese two worlds, we must create intermediary components that enable usto travel to and from managed space into unmanaged space. Let's addressthe idea of using VB.NET components from your Visual Basic 6 codefirst.

Using VB.NET Components with VB 6Microsoft provides a tool called tlbexp.exe with the .NET SDK thatenables you to create a type library (.TLB) file from an assembly manifestbut does not register it. A type library (especially an unregistered one)does not really provide the functionality of a COM server. If we were totry to use a managed DLL as a COM server, the managed DLL wouldhave to have the functionality of an in-process COM server. For example,it would have to have the four entry points to any COM DLL:DllRegisterServer, DllUnregisterServer, DllGetClassObject, andDllCanUnloadNow. It would also have to add the necessary keys to theregistry for the SCM to load the DLL. Remember that the SCM must findthe CLSID followed by the InprocServer32 key pointing to the DLL.Unfortunately for COM developers, CLR assembly DLLs do not haveCOM entry points, nor are they self-registering. However, Microsoft has

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

Page 30: VB1

Helpmate

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

Public Sub AddWidgetToInventory(ByVal Amount As Integer) m_Quantity += Amount End Sub

Public ReadOnly Property Quantity( ) As Integer Get Return m_Quantity End Get End Property

End ClassThis code declares a single class named Inventory. The Inventory class hasa private member named m_Quantity; it stores the number of widgets ininventory. The class also has a public method for adding widgets toinventory, cleverly named AddWidgetToInventory, as well as oneproperty for retrieving the quantity. You can save the preceding code asinventory.vb and compile it as a DLL with the following command:vbc /t:library inventory.vbLet's suppose that we would like to use the Inventory class in a VB 6client program. The best way to do this is to use the RegAsm.exe tool.From a command prompt, locate the inventory.dll .NET assembly andenter the following command:Regasm inventory.dll /tlb:inventory.tlbI mentioned earlier that Microsoft has another tool called tlbexp.exe thatcreates a type library. However, that tool does not automatically registerthe type library, nor does it add registry keys for a client program to beable 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 COMand, if you use the /tlb command-line switch, it also creates a type libraryfor all public classes in the assembly and registers the type library. Afteryou run the RegAsm.exe tool, you should be able to use your .NETassemblies from VB 6.RegAsm plays a trick with the registry. If you look at the registry, youshould 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 subkeyscontains the class identifier of our assembly. In fact, your CLSID shouldbe the same as mine, even if you typed the program from scratch. If this

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

Page 31: VB1

Helpmate

doesn't sound strange to you, then you may need to read Chapter 3 again.In VB 6, if two people compile the same program on different machines,VB generates different GUIDs by default unless there is an existing typelibrary that both machines could reference using project compatibility andbinary compatibility. VB 6 uses CoCreateGUID to generate a uniquenumber, and, because it was guaranteed to be unique, it would be differentfrom machine to machine (it would actually be different on the samemachine if you compiled twice with no compatibility).VB.NET does not use GUIDs in the same way as VB 6. They are presentjust 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 thename of the assembly (in our case, inventory) and the name of the class (inour case, also inventory). Therefore, if we name the class and theassembly 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 nametheir assemblies and classes the same. There is a solution to this--thedeveloper can assign the class a specific GUID using the GuidAttributeclass 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 intomanaged space. However, it also serves as a COM entry point. When aCOM client requests a class through DLLGetClassObject, MSCorEElooks at the other values in InprocServer32, in particular the Assembly valueentry. 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 anoriginator to the assembly by compiling it with a private key.There is a little inconvenience with using RegAsm.exe. If you notice, thereis no exact path to your assembly. You could manually set this path in theregistry (as seen in Figure 11-6), although this capability may be removedin 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 aprivate key and add it to the global assembly cache. Once the assembly isin the GAC, you do not have to specify a path to use the assembly. To usethe assembly from VB 6, all you have to do is create a program as usualand 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 forIDL. If you are hesitant to use IDL to version your components because itis yet a new syntax to learn and it is like C++, then you may want to

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

Page 32: VB1

Helpmate

define your interfaces and manage your GUIDs in VB.NET and use it forversioning purposes. Let's look at a short example of how to define yourinterfaces in VB.NET and manage the GUIDs using attributes. Examinethe 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 CurrencyEnd InterfaceThe code is in VB syntax. What makes this code different from regularVB is the use of attributes throughout the code. These attributes are usedby 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 isgenerated. Notice that the library name is the name of the assembly. TheLIBID for this library is the GUID specified with the Guid attribute at theAssembly level. The Guid attribute is part ofSystem.Runtime.InteropServices. Actually, all the attributes used in theVB.NET code are part of the same assembly. The interface also uses theGuid attribute to assign an IID to the interface. In addition to the Guidattribute, it uses the InterfaceType attribute to tell tlbexp that the interfaceshould be derived from IUnknown (the default is to make it a dualinterface).Once you compile the preceding code as a DLL, you can run the tooltlbexp.exe to generate the type library. Suppose you named your DLLbankinterfaces.dll; you can generate a type library entering the followingcommand in console mode:tlbexp.exe bankinterfaces.dllBy default, the tlbexp tool uses the root filename of the DLL to name thetype library; thus, the resulting file would be bankinterfaces.tlb. Theresulting type library source is as follows:// Generated .IDL file (by the OLE/COM Object Viewer)//// typelib filename: bankinterfaces.tlb [ uuid(3C53B8E3-81FC-4645-B65F-ACABE77A79D0), version(1.0)]library BankInterfaces{ // TLib : // TLib : OLE Automation : { 00020430-0000-0000-C000-000000000046} importlib("stdole2.tlb"); // Forward declare all types defined in this typelib interface IAccount;

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

Page 33: VB1

Helpmate

[ odl, uuid(1393732E-8D27-431A-A180-8EDA0E4499E2), oleautomation, custom({ 0F21F359-AB84-41E8-9A78-36D110E6D2F9} ,"BankInterfaces.IAccount") ] interface IAccount : IUnknown { HRESULT _stdcall MakeDeposit([in] CURRENCY Amount); [propget] HRESULT _stdcall Balance([out, retval] CURRENCY* pRetVal); } ;} ;Tlbexp.exe creates a type library but does not register it, but in Chapter 6,you learned that you could use regtlib.exe to register the type library. Afteryou register the type library, you could use it like any other type library inVisual Basic. First, add it to your project through the Project Referencesdialog box, then implement it in a concrete class. The interface methodsresulting from the preceding VB.NET code would look like the followingin VB 6:Option Explicit Implements IAccount Private Property Get IAccount_Balance( ) As CurrencyEnd Property Private Sub IAccount_MakeDeposit(ByVal Amount As Currency)End Sub

Using VB 6 Components with VB.NETGoing from .NET to VB 6, you needed to create a type library; going fromVB 6 to .NET, you must generate an assembly. In the previous section,you learned about tlbexp.exe. In this section, you'll learn about tlbimp.exe.Tlbimp.exe creates an assembly from the definition of a type library.Consider the following VB 6 code:Option Explicit Private m_Balance As Currency Public Sub MakeDeposit(ByVal Amount As Currency) m_Balance = m_Balance + AmountEnd Sub Public Function Balance( ) As Currency Balance = m_BalanceEnd FunctionThis is the usual Checking class. Let's say for the sake of argument thatthis code represents a class in BankServer.DLL. You can create anassembly that contains the definitions in your type library using tlbimp. To

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

Page 34: VB1

Helpmate

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

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

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

Page 35: VB1

Helpmate

<Assembly: System.Runtime.InteropServices.GuidAttribute("1D1D3D4C-52BE-46de-9100-8F5AEB8207C0")><Assembly:System.Runtime.CompilerServices.AssemblyKeyFileAttribute("complusservices.key")><Assembly: Microsoft.ComServices.Description("Book - Dotnet")><Assembly: Microsoft.ComServices.ApplicationName("Book - Dotnet")><Assembly: Microsoft.ComServices.ApplicationID("7319F24B-6DEA-4479-8027-1E8E1816C626")><Assembly:Microsoft.ComServices.ApplicationActivation(Microsoft.ComServices.ActivationOption.Server)> Imports System.Runtime.InteropServicesImports Microsoft.ComServices Interface <guidattribute("874A6DD2-E141-41fd-A379-E066E8E23921")> IAccount Sub MakeDeposit(ByVal Amount As Integer) ReadOnly Property Balance( ) As IntegerEnd Interface Public Class <guidattribute("FB03ABE6-982D-436e-919C-CA1D8BE1B71A"), _ Transaction(TransactionOption.Required)> _ Checking Inherits ServicedComponent Implements IAccount

Private m_Balance As Integer

Private Sub MakeDepositImpl(ByVal Amount As Integer) _ Implements IAccount.MakeDeposit m_Balance += Amount End Sub

Private ReadOnly Property BalanceImpl( ) As Integer Implements IAccount.Balance Get Return m_Balance End Get End Property

End ClassThe top portion of the code uses a number of Assembly attributes. The firstis GuidAttribute, which is used at the assembly level to control the LIBIDfor the type library that is generated. The second one is AssemblyKeyFile,which you should also be familiar with. One of the requirements to useyour assembly in COM+ is that it be a public assembly. As you know, thatmeans that you must assign an originator to the assembly. Thus, thisattribute points to a private key file. The other four attributes have to dowith COM+ application properties. If you remember from Chapter 7, youcan create COM+ applications programmatically using the catalog COMcomponents. When you create a COM+ application, you can specifyvarious attributes, such as the Application ID (this is a GUID thatrepresents the true name of the application). You can also specify the

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

Page 36: VB1

Helpmate

Application Name and give the application a description. Also, you canspecify things like the Activation property (Server or Library).All these properties can be set easily in VB.NET through attributes. Theadvantage of doing this is that when you run the RegSvcs tool on yourVB.NET DLL, the tool will automatically create the COM+ applicationfor you and add your components to it. All the COM+ attributes--not onlythe ones at the application level, but also the ones at the class interface andmethod levels--have been replicated as attributes. They are contained inthe Microsoft.ComServices namespace.The sample code also uses the Transaction attribute at the class level tospecify that this class requires transactions.Something that might look strange is that the Checking class implementsthe IAccount interface and makes the implementation methods private. Inaddition, it also gives each method a different name. This has nothing todo with the fact that I am going to use this interface in COM+. If youmake the implementation methods public, that means that there are twoways of calling the method: through the interface and through a classreference directly. If you make the implementation methods private, theonly way to reach the methods is through the interface. Notice also thatyou do not need to name the implementation methods the same as theinterface methods; you must only match the signatures and use theImplements directive at the end of the method. It is interesting that both ofthese aspects of implementing interfaces in VB.NET (making methodsprivate and giving them different names) are just like implementinginterfaces in VB 6--except that somehow it seems clearer in VB.NET.To compile the code, you use the standard vbc command at the commandprompt. Because the code uses a security key file, you have to create onewith sn.exe. Also, because the code uses the Microsoft.ComServicesassembly, you must reference this assembly in the command line. Thefollowing code shows how to compile the code in a command-promptwindow:vbc /t:library bankserver.vb /version:1.0.0.0 /r:Microsoft.ComServices.dllThe next step is to add the assembly to the GAC. To review, that is donewith the gacutil.exe tool. Once the assembly is in the GAC, you can runthe RegSvcs.exe tool as follows:RegSvcs /fc bankserver.dllThe /fc switch tells the tool to find an application or create one. The nameof the application is defined by the ApplicationName attribute in thepreceding source code. Optionally, you can enter an application name inthe command line after the assembly name. Figure 11-7 shows the endresult of running the RegSvcs tool.Figure 11-7. BankServer .NET application in Component Servicesadministration program

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

Page 37: VB1

Helpmate

Using the components is just like using any COM+ component. In fact,the following code should present no surprises: Dim acct As IAccount Set acct = CreateObject("bankserver.Checking") Call acct.MakeDeposit(500) MsgBox acct.BalanceOne 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 declarativeattribute, the attribute will be turned off by default. One nice thing aboutcreating COM+ components with .NET is that the resulting componentuses a different threading model than VB 6 components. If you recall, VB6 components use the apartment-threading model. VB.NET COMcomponents are marked as using the both-threading model. That meansthat VB.NET components run in the MTA by default instead of in theSTA. Also, VB.NET COM components can be pooled. Pooling VB.NETCOM components is beyond the scope of this chapter, but be aware thatliving in the MTA means that you must handle synchronization issues inyour methods.

SummaryIn this chapter, you have learned the basics of the .NET architecture. Youlearned some of the limitations in COM and why Microsoft has created thenew architecture. A number of languages are being written to support thenew architecture. These languages compile to a processor-independentform of assembly language known as Intermediate Language (IL). Two ofthe main languages for .NET are VB.NET and C#. Both of thesecompilers 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 clientassembly's manifest contains the version number of the referencedassembly. The runtime matches the major and minor numbers in theversion for the assembly. You can redirect the runtime to use a differentversion with a configuration file.To use an assembly from a VB 6 program, you use a tool calledRegAsm.exe. Alternatively, you can use the tlbexp.exe tool to create a typelibrary. However, RegAsm.exe does the job of adding keys to the registryto make the public classes in the assembly creatable from COM. It alsobuilds a type library and registers it. In addition to providing you with away to use .NET components from COM, you can use this functionalityfor versioning your existing COM components. To use COM componentsfrom .NET, you can use the tlbimp.exe tool to create an assembly fromyour type library.

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

Page 38: VB1

Helpmate

You can also add a .NET component to a COM+ application. In fact, itmakes good sense to mix COM with .NET components, because .NETcomponents do not have the same threading restrictions as COM+components. To use an assembly in COM+, you must first sign it and addit to the cache. Then you must run the tool RegSvcs to register the classesin the assembly, create a type library, and add it to the catalog. There areattributes in the Microsoft.ComServices assembly that enable you tospecify the declarative attributes of the COM+ application.

Versioning VB 6 Components with VB .NET:An Excuse to Use VB .NET Today

Many of us live in a world where we are not allowed to use beta versions for ourproduction code. In fact, it may be sometime after a final version is released beforemanagement allows us to start migrating code. By this time you have probably heard ofVB .NET and you may have downloaded the Visual Studio .NET beta, perhaps with thejustification that you intend to evaluate it for future releases. You may also be looking forany excuse to use it now.

It turns out that there is at least one compelling reason for using VB .NET today: to solvean existing VB 6 problem--versioning. This article shows you the problems withversioning 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 notbe dependent on the .NET SDK. Therefore, you can use it without shipping beta code toyour clients.

Versioning Components in VB 6The problem with versioning components in VB 6 boils down to one thing: VB 6 doesnot give you full control over your interface's GUIDs. The basic scenario is that youcreate 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'tcreate 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 theyare all compatible.The reason client programs stop working is because of numbers called GUIDs (globallyunique identifiers). GUIDs are 128-bit numbers that are assigned (in COM) to differentaspects of your component. An example of a GUID is { FAE3A31F-693C-4ca3-B0EC-0BD471042D52}. A typical VB ActiveX DLL or ActiveX EXE project has GUIDsassigned to three different parts of the project: to the type library, to each class, and toeach interface.

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

Page 39: VB1

Helpmate

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

Information about every public class in the project is grouped into a binary file called atype library. The type library file is embedded in the image of your COM server (ActiveXDLL or ActiveX EXE) as a Windows resource. To distinguish your type library fromother 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 6have default interfaces. This is a table of all the public functions in the class. Eachinterface receives a GUID as its unique name. According to COM rules, if you modifythe public methods in some way, you must assign a new GUID to the interface. BecauseVB 6 follows these same rules, if you modify a method in any way (add, remove, orchange a method), it assigns a new GUID to the interface. However, what happens if youkeep every method the same? It turns out that if you are not careful VB 6 will also changethe 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 createa client program to use the component. The client program builds a dependency on thecomponent's interface GUID and on the class's GUID. If VB changes various GUIDs inthe component, then the client program stops working.If you have not made any changes to the component's interface, you must tell VB not tochange the GUIDs of the default interface when you compile. You do this by setting theversion-compatibility property of your VB project to binary compatibility. When you turnon 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 amechanism for letting you add methods without breaking compatibility. That means yourclient code is safe as long as you only add methods. It is a different story if you removeor change a method. When you do so, COM says that you must change the GUIDs ofyour 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 everyinterface in the project, even the ones you haven't changed. So, if you have fourcomponents in one ActiveX DLL or ActiveX EXE and four client programs using thesecomponents, you have to recompile every single client, even if the component they areusing is not the one you changed. This may be OK once in a while, but what about whenyou are developing and testing? What if you are deploying the application in a companywhere it would be difficult to change every single client?Languages like C++ enable you to take full control over the interface GUIDs. Althoughthat means you have to know when to change them, it also means you do not have tochange all the GUIDs each time you make a change to a single interface. In C++, projectinterfaces 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 VBwas to learn IDL and use it to define your interfaces, then compile the IDL into a typelibrary and use it in your VB project.

Enter VB .NET

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

Page 40: VB1

Helpmate

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

Internally, your COM classes look little like .NET classes. However, Microsoft providesa tool that enables you to use .NET classes through COM. The way this tool works is thatit produces type libraries that can be used from VB 6. If all you have in a VB .NETproject is the definition of the interfaces, then the type library produced will not have anydependency on the compiled .NET code. That means you can use the type library andcompile it into your code without your clients needing to install the .NET SDK. What'smore, 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 withoutusing the technique, then build a VB .NET project, and then change the original VB 6project so that it uses this technique.

Original VB 6 ProjectLet's suppose that you have a banking application. In this application you have twoclasses: a checking class and a savings class. A good design for such an applicationwould be to separate the methods these two classes have in common into a singleinterface called IAccount. The following code shows the definition of the IAccountinterface:'VB 6 IAccount interfacePublic Sub MakeDeposit(ByVal Amount As Currency)End SubPublic Property Get Balance() As CurrencyEnd PropertyIn VB 6, interfaces are declared inside class modules. In this case, the class modulewould be called IAccount. Interface methods do not have code, just the definition of themethods. Also, the class's instancing property is normally set to 2--PublicNotCreatable, to letclient programs know that the class is not a creatable entity. Interfaces serve as a way tocommunicate with the functionality of a class. After defining the interface, you wouldimplement it in a concrete class such as Checking and Savings. The following codeshows what the Checking class may look like:'VB 6 Checking classImplements IAccountPrivate m_Balance As Currency

Public Sub IAccount_MakeDeposit(ByVal Amount As Currency) m_Balance = m_Balance + AmountEnd Sub

Public Property Get IAccount_Balance() As Currency IAccount_Balance = m_Balance

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

Page 41: VB1

Helpmate

End PropertyThe Checking class uses the Implements command to adopt the IAccount interface. To usethe interface, a client program would declare a variable as IAccount and use New to createan instance of Checking as follows:Dim Acct As IAccountSet Acct = New CheckingCall Acct.MakeDeposit(5000)Msgbox Acct.BalanceNotice 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 Checkingclass. If your server project has a number of interfaces, then each client program wouldhave dependencies on one or more of those interfaces, and on one or more of the concreteclasses that implement them. If you were to change one of the methods in any of theinterfaces, then VB 6 would change the GUIDs to all the interfaces and every clientprogram would stop working. Let's see how VB .NET can help manage those GUIDsmore efficiently.

VB .NET ProjectAs a replacement for IDL, you can use VB .NET to get more control over your interfaces.Plus, because VB .NET is similar to VB 6, you do not have to learn a different syntax.Let's begin a VB .NET project; you should see the dialog box in Figure 1.

Figure 1. New project dialog boxChoose the class library project. You can name the new project BankInterfaces. Whenyou create a new library project, VB will create a class file named class1.vb. This filecontains some default code for the definition of a class: Class1. You can delete all thedefault code and define your IAccount interface as follows:'VB .NET InterfacePublic Interface IAccount Sub MakeDeposit(ByVal Amount As Decimal) ReadOnly Property Balance() As DecimalEnd InterfaceRename the class1.vb file to Account.vb. The next step is to assign GUIDs to the interfaceand to the project. This is done using the GuidAttribute. An attribute is a class that can beused to add information to an assembly, a module, an interface, a class, a field, a method,or a parameter in a method. The following code shows the interface code with attributes:Impor ts System.Runtime.InteropServices

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

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

Page 42: VB1

Helpmate

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

Figure 2. guidgen.exeThere is a second instance of the Guid attribute in the code. However, it was the wizardthat added the second instance automatically. It is in the file AssemblyInfo.vb. If youexamine 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 nowthink of the assembly as the project. Putting the Guid attribute at the assembly levelenables 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 MarshalAsattribute. The MarshalAs attribute is necessary because the datatype Decimal has a numberof possible interpretations in the COM world. (By default Decimal gets converted to awide character string.) The MarshalAs attribute enables you to specify the correct typeconversion. If you are wondering how in the world one figures out when to use MarshalAsand 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 fromthe Project menu. The resulting DLL will be called BankInterface.DLL and you can find it inthe Project\Bin directory.

Creating the Type LibraryWhen you build a VB .NET library project, you create what is known in the run-time asan assembly. Assemblies are not COM servers, and that means that they do not haveembedded type libraries. However, the Microsoft .NET SDK ships with a tool calledtlbexp.exe. This tool can read an assembly and create a type library from the definitionsin the assembly. The best way to do this is to locate the DLL in a command prompt andrun the tlbexp.exe program using BankInterfaces.DLL as the command-line parameter. Fromthe command line enter the following:tlbexp BankInterfaces.dllFigure 3 shows the output generated from running tlbexp.

Figure 3. Running tlbexp to generate a type libraryIf you look at the files in the directory you will see that there is a new file calledBankInterfaces.tlb. To use this file in your VB 6 project you must first register the typelibrary with a command available in Visual Studio 6 called regtlib.exe. From thecommand line enter the following:regtlib BankInterfaces.tlb

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

Page 43: VB1

Helpmate

Once you register the type library file, you can incorporate it in your VB 6 project usingthe Project Reference dialog. In VB 6 choose References from the Project menu, thenselect BankInterfaces from the list as shown in Figure 4.Figure 4. Project References dialogThe tlb file contains a declaration of the IAccount interface. With the BankInterfacesreference in place, you can get rid of the IAccount class in the VB project. The Checkingand Savings classes can be used as before. You can then build the VB 6 COM server andmodify the client project slightly.

Reversing the ProcessEarlier I mentioned that certain .NET datatypes have different interpretations in the COMworld, and that to specify the conversion type you must use the MarshalAs attribute. I alsomentioned that an easy way to know what datatypes required this attribute was to reversethe process. Reversing the process means taking an existing type library and generatingan assembly that can be used in .NET. With this mechanism you can examine how VBtypes translate to .NET and which types require special handling.

Reversing the process is done with a tool called tlbimp.exe, also included in the .NETSDK. Tlbimp takes a type library file, or a DLL that contains a type library, and generatesan assembly that can be used from .NET. Using that approach, I've created a VB typelibrary 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 TypeInteger ShortSingle SingleByte ByteVariant MarshalAs(UnmanagedType.Struct) ObjectLong IntegerDouble DoubleCurrency MarshalAs(UnmanagedType.Currency) DecimalString MarshalAs(UnmanagedType.BStr) StringBoolean BooleanDate DateObject MarshalAs(UnmanagedType.IDispatch) ObjectArray*

<MarshalAs(UnmanagedType.SafeArray,SafeArraySubType:=UnmanagedType.*enter type here*)>

*An example of an Array declaration is the following: Sub MyMethod(ByRefstrs() As String). The strs parameter in the MyMethod declaration is of typeSafeArray(Bstr). A SafeArray(Bstr) is an array of strings. To specify a SafeArrayparameter you use the MarshalAs attribute passing SafeArray as the type, thenadding SafeArraySubType:= the type of theSafeArray. In the case of theMyMethod declaration, the second parameter to the attribute would beSafeArraySubType:=UnmanagedType.Bstr.

New Client Version

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

Page 44: VB1

Helpmate

The VB 6 client code can remain mostly as is, however, you must reference the new typelibrary as well, like you did in the server project. Also, to completely get rid of anydependencies the client project has on the VB 6 generated GUIDs, you must change theNew command in the code to use CreateObject instead, as seen below. (Changing from Newto CreateObject eliminates the dependency on the CLSIDs.)Dim Acct As IAccountSet Acct = CreateObject("BankServer.Checking")Call Acct.MakeDeposit(5000)Msgbox Acct.BalanceWith the VB .NET-generated type library and the CreateObject command you now havefull control over GUIDs. That means you no longer have to worry about how you setyour 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 GUIDsWith full control comes more responsibility. While you are developing, it is notnecessary to follow COM rules to the letter. You may change, add, or delete a methodand keep the same GUID. Once you release the server and client to the outside world, itis 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 interfaceand assign to the new interface a new GUID.For example, let's suppose the requirements change and your company now needs anextra parameter in the MakeDeposit method, like AmountAvailable. Instead of modifying theexisting method (which would break existing clients) you should add a new interface tothe VB .NET project called IAccount2 and then implement both IAccount and IAccount2in the Checking class. The following code shows the definition of the IAccount2 interfacein 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)> DecimalEnd 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)> DecimalEnd InterfaceNotice that the code uses a different GUID for the IAccount2 interface. Of course youwould have to save the project, rebuild it and re-export the type library with tlbexp.exe.Then you would modify the server code as follows:'VB 6 Checking class

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

Page 45: VB1

Helpmate

Implements IAccountImplements IAccount2

Private m_Balance As CurrencyPr ivate m_Available As Cur rency

Public Sub IAccount_MakeDeposit(ByVal Amount As Currency) m_Balance = m_Balance + AmountEnd Sub

Public Property Get IAccount_Balance() As Currency Balance = m_BalanceEnd Property

Public Sub IAccount2_MakeDeposit(ByVal Amount As Cur rency, ByValAmountAvailable As Cur rency) m_Balance = m_Balance + Amount m_Available = m_Available + AmountAvailableEnd Sub

Public Proper ty Get IAccount2_Balance() As Cur rency IAccount2_Balance = IAccount_BalanceEnd Proper ty

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

VB .NET Language in a Nutshell

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

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

Page 46: VB1

Helpmate

In this section, we outline the changes made to the Visual Basic languagefrom Version 6 to Visual Basic .NET. These language changes were madeto bring VB under the umbrella of the .NET Framework and allow aCommon Language Runtime for all languages in Visual Studio .NET. Insome sense, the changes made to the VB language were to bring thelanguage component of VB (as opposed to the IDE component) more inline 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 willnot necessarily discuss how VB 6 handles a given language feature, unlessthe contrast is specifically helpful. You can assume that if a VB .NETlanguage feature is described in this chapter, there has been a change in itsbehavior since VB 6.

Data TypesThere have been fundamental changes to data types in VB .NET, whichwe outline in this section. The most important change is that all of thelanguages under the .NET umbrella (VB, C#, and Managed C++) nowimplement a subset of a common set of data types, defined in the .NETFramework's Base Class Library (BCL). We say subset because VB .NETdoes not implement all of these data types. In any case, each data type inthe BCL is implemented either as a class or as a structure (which is similarto a class) and, as such, has members. The VB .NET data types arewrappers for the corresponding BCL data type. While this need notconcern the VB programmer, it can be used in some cases to expose a bitmore functionality from a data type. For more on data types, see Chapter2.Now let us consider the specifics.

Strings

As you may know, in VB 6, strings were implemented as a data typeknown as the BSTR. A BSTR is a pointer to a character array that ispreceded 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 doesnot have fixed-length strings, as does VB 6. Thus, the following code isillegal:Dim Name As String * 30Note, though, that strings in VB .NET are immutable. That is, althoughyou do not have to declare a string's length in advance, once a value isassigned to a string, its length cannot change. If you change that string, the.NET Common Language Runtime actually gives you a reference to a newString object. (For more on this, see Chapter 2.)

Integer/Long data type changes

VB .NET defines the following signed-integer data types:Short

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

Page 47: VB1

Helpmate

The 16-bit integer data type. It is the same as the Int16 data type in theBase Class Library.IntegerThe 32-bit integer data type. It is the same as the Int32 data type in theBase Class Library.LongThe 64-bit integer data type. It is the same as the Int64 data type in theBase 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 datatype.

• The VB 6 Long data type has become the VB .NET Integer datatype.

Variant data type

VB .NET does not support the Variant data type. The Object data type isVB .NET's universal data type, meaning that it can hold data of any otherdata type. According to the documentation, all of the functionality of theVariant data type is supplied by the Object data type.We cannot resist the temptation to add that there are several penaltiesassociated with using a universal data type, including poor performanceand poor program readability. Thus, while VB .NET still provides thisopportunity through the Object data type, its use is not recommendedwhenever it can be avoided.The VarType function, which was used in VB 6 to determine the type ofdata stored in a variant variable (that is, the variant's data subtype), nowreports the data subtype of the Object type instead. In addition, theTypeName function, which can be used to return a string that indicates thedata type of a variable of type Object, is still supported.

Other data type changes

Here are some additional changes in data types:

• The Deftype statements (DefBool, DefByte, etc.), which were used todefine the default data type for variables whose names began withparticular letters of the alphabet, are not supported in VB .NET.

• The Currency data type is not supported in VB .NET. However, inVB .NET, the Decimal data type can handle more digits on bothsides of the decimal point, and so it's a superior replacement. InVB .NET, Decimal is a strong data type; in VB 6, it was a Variantsubtype, and a variable could be cast as a Decimal only by callingthe CDec conversion function.

• In VB 6, a date is stored in a Double format using four bytes. InVB .NET, the Date data type is an 8-byte integer data type whoserange of values is from January 1, 1 to December 31, 9999.

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

Page 48: VB1

Helpmate

Var iables and Their DeclarationThe changes in variable declarations and related issues are described here.

Variable declaration

The syntax used to declare variables has changed for VB .NET, making itmore flexible. Indeed, these are long awaited changes.In VB .NET, when multiple variables are declared on the same line, if avariable is not declared with a type explicitly, then its type is that of thenext variable with an explicit type declaration. Thus, in the line:Dim x As Long, i, j, k As Integer, s As Stringthe variables i, j, and k have type Integer. (In VB 6, the variables i and jwould have type Variant, and only the variable k would have type Integer.)When declaring external procedures using the Declare statement, VB .NETdoes not support the As Any type declaration. All parameters must have aspecific type declaration.

Variable initialization

VB .NET permits the initialization of variables in the same line as theirdeclaration (at long last). Thus, we may write:Dim x As Integer = 5to declare an Integer variable and initialize its value to 5. Similarly, wecan declare and initialize more than one variable on a single line:Dim x As Integer = 6, y As Integer = 9

Variable scope changes

In VB 6, a variable that is declared anywhere in a procedure has procedurescope; that is, the variable is visible to all code in the procedure.In VB .NET, if a variable is defined inside a code block (a set ofstatements that is terminated by an End..., Loop, or Next statement), then thevariable has block-level scope; that is, it is visible only within that block.For example, consider the following VB .NET code:Sub Test( ) If x <> 0 Then Dim rec As Integer rec = 1/x End If MsgBox CStr(rec)End SubIn this code, the variable rec is not recognized outside the block in whichit 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 ofthe entire procedure, even if the variable's scope is block-level. Thisimplies that if a block is entered more than once, a block-level variablewill 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 wellas the default lower bound of arrays whose lower bound was not explicitly

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

Page 49: VB1

Helpmate

specified. In VB .NET, the lower bound of every array dimension is 0 andcannot be changed. The following examples show how to declare a one-dimensional array, with or without an explicit size and with or withoutinitialization:' Implicit constructor: No initial size and no initializationDim Days( ) As Integer ' Explicit constructor: No initial size and no initializationDim Days( ) As Integer = New Integer( ) { } ' Implicit constructor: Initial size but no initializationDim Days(6) As Integer ' Explicit constructor: Initial size but no initializationDim Days( ) As Integer = New Integer(6) { } ' Implicit constructor: Initial size implied by initializationDim Days( ) As Integer = { 1, 2, 3, 4, 5, 6, 7} ' Explicit constructor, Initial size and initializationDim Days( ) As Integer = New Integer(6) { 1, 2, 3, 4, 5, 6, 7}Note that in the declaration:Dim ArrayName(X) As ArrayTypethe number X is the upper bound of the array. Thus, the array has sizeX+1.Multidimensional arrays are declared similarly. For instance, the followingexample declares and initializes a two-dimensional array:Dim X(,) As Integer = { { 1, 2, 3} , { 4, 5, 6} }and the following code displays the contents of the array:Debug.Write(X(0, 0))Debug.Write(X(0, 1))Debug.Writeline(X(0, 2))Debug.Write(X(1, 0))Debug.Write(X(1, 1))Debug.Write(X(1, 2)) 123456In VB .NET, all arrays are dynamic; there is no such thing as a fixed-sizearray. The declared size should be thought of simply as the initial size ofthe 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 arraydeclaration, but only for array resizing. All arrays must be declaredinitially 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...EndType structure.In VB .NET, the Type statement is not supported. Structures are declaredusing the Structure...End Structure construct. Also, each member of the

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

Page 50: VB1

Helpmate

structure must be assigned an access modifier, which can be Public,Protected, Friend, Protected Friend, or Private. (The Dim keyword is equivalentto Public in this context.)For instance, the VB 6 user-defined type:Type RECT Left As Long Top As Long Right As Long Bottom As LongEnd Typeis defined in VB .NET as:Structure RECT Public Left As Long Public Top As Long Public Right As Long Public Bottom As LongEnd StructureActually, the VB .NET Structure type is far more reaching than its VB 6user-defined type predecessor. Indeed, structures have many properties incommon with classes; for instance, structures can have members(properties and methods). We discuss structures in detail in Chapter 2.

Boolean and Bitwise OperatorsEqv and Imp, two infrequently used Boolean and bitwise operators that arepresent in VB6, have been removed from VB .NET.In VB6, Eqv is the logical equivalence operator. As a Boolean operator, itreturns True if both expressions are either True or False, but it returns False ifone is True while the other is False. As a bitwise operator, it returns 1 ifboth bits are the same (that is, if both are 1 or both are 0), but it returns 0 ifthey are different. In VB .NET, Eqv can be replaced with the equalscomparison operator for logical operations. However, for bitwiseoperations, you'll have to resort to a bit-by-bit comparison, as thefollowing code fragment shows:Public Function BitwiseEqv(x1 As Byte, X2 As Byte) _ As Long

Dim b1, b2, bRet As ByteDim iCtr as Integer For iCtr = 0 to len(x1) * 8 - 1 b1 = x1 and 2^iCtr b2 = x2 and 2^iCtr if b1 = b2 then bRet += 2^iCtrnext BitwiseEqv = bRet End FunctionIn VB6, Imp is the logical implication operator. As a Boolean operator, itreturns True unless its first expression is True while the second is False. As abitwise operator, it returns 1 unless the bit in the first expression is 1 while

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

Page 51: VB1

Helpmate

the bit in the second expression is 0. In VB .NET, Imp can be replacedwith a combination of the Not and Or operators for logical operations. Forexample, the code fragment:bResult = (Not bFlag1) Or bFlag2is equivalent to the VB6 statement:bResult = bFlag1 Imp bFlag2For bitwise operations, a bit-by-bit comparison is again necessary, as thefollowing code fragment shows:Public Function BitwiseImp(x1 As Byte, X2 As Byte) As Long

Dim b1, b2, bRet As ByteDim iCtr as Integer For iCtr = 0 to len(x1)*8 - 1 b1 = Not(x1) and 2^iCtr b2 = x2 and 2^iCtr if b1 Or b2 then bRet += 2^iCtr end Ifnext BitwiseImp = bRet End Function

Changes Related to ProceduresVB .NET features a number of changes to the way in which proceduresare defined and called, most of which tend to make the language morestreamlined and consistent.

Calling a procedure

In VB 6, parentheses are required around arguments when makingfunction calls. When calling a subroutine, parentheses are required whenusing the Call statement and proscribed when not using the Call statement.In VB .NET, parentheses are always required around a nonemptyargument list in any procedure call--function or subroutine. (In subroutinecalls, the Call statement is optional.) When calling a parameterlessprocedure, empty parentheses are optional.

Default Method of Passing Arguments

In VB 6, if the parameters to a function or subroutine were not explicitlyprefaced with the ByVal or ByRef keywords, arguments were passed tothat routine by reference, and modifications made to the argument in thefunction or subroutine were reflected in the variable's value once controlreturned to the calling routine. In VB .NET, on the other hand, if theByRef or ByVal keyword is not used in a parameter, the argument ispassed to the routine by value, and modifications made to the argument inthe function or subroutine are discarded once control returns to the callingprogram.

Optional arguments

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

Page 52: VB1

Helpmate

In VB 6, a procedure parameter can be declared as Optional withoutspecifying a default value. For optional Variant parameters, the IsMissingfunction can be used to determine whether the parameter is present.In VB .NET, an optional parameter must declare a default value, which ispassed to the procedure if the calling program does not supply anargument for that parameter. The IsMissing function is not supported. Thefollowing example shows an optional parameter declaration:Sub Calculate(Optional ByVal Switch As Boolean = False)

Return statement

In VB .NET, the Return statement is used to return control to the callingprogram from a function or subroutine. The GoSub statement is notsupported. Note that the Return statement is used to return a value from afunction.The following function illustrates the Return statement:Public Function Test( ) As Integer If MsgBox("Return", MsgBoxStyle.YesNo) = MsgBoxResult.Yes Then Return 0 Else MsgBox("Continue") Return 1 End IfEnd Function

Passing property parameters in procedures

Consider passing a property to a procedure by reference, as in:Sub ShrinkByHalf(ByRef lSize As Long) lSize = CLng(lSize/2)End Sub Call ShrinkByHalf(Text1.Height)In VB 6, when passing the value of a property by reference, the property isnot updated. In other words, passing a property by reference is equivalentto passing it by value. Hence, in the previous example, the propertyText1.Height will not be changed.In VB .NET, passing a property by reference does update the property, soin this case, the Text1.Height property will be changed. Note, however,that the value of the property is not changed immediately, but rather whenthe called procedure returns.

ParamArray parameters

In VB 6, if the ParamArray keyword is used on the last parameter of aprocedure declaration, the parameter can accept an array of Variantparameters. In addition, ParamAarray parameters are always passed byreference.In VB .NET, ParamArray parameters are always passed by value, and theparameters in the array may be of any data type.

Miscellaneous Language Changes

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

Page 53: VB1

Helpmate

VB .NET includes several miscellaneous changes that include the formatof line numbers, the lack of support for the GoTo and GoSub statements,and the replacement of the Wend keyword by End While.

Line numbers

Visual Basic .NET requires that every line number be followedimmediately by a colon (:). A statement can optionally follow the colon. InVB 6, line labels, which were used in particular for error handling by theOn Error GoTo statement, had to be followed immediately by a colon, butline numbers did not.

On GoTo

The On...GoSub and On...GoTo constructions are not supported. However,VB .NET still supports the On Error GoTo statement.

While

The While...Wend construction loops through code while a specifiedcondition is True. VB .NET retains that construction, but replaces the Wendkeyword with the End While statement. The Wend keyword is not supported.

GoSub and Return statements

In VB .NET, the GoSub statement is not supported.As remarked earlier, in VB .NET, the Return statement is used to returncontrol to the calling program from a function or subroutine. The VB 6Exit Sub and Exit Function statements continue to be supported in VB .NET;however, the advantage of the Return statement is that it allows you tospecify the function's return value as an argument to the Return statement.Changes to Programming ElementsVB .NET has removed support for several programming elements becausethe underlying .NET Framework class library and the Common LanguageRuntime (CLR) contain equivalent functionality. Here are the victims andtheir replacements. (We discuss the class library and CLR in Chapters and.)

ConstantsThe Microsoft.VisualBasic.Constants class in the Base Class Librarydefines a number of constants, such as the familiar vbCrLf constant, sothese can be used as always. However, some constants, such as the colorconstants vbRed and vbBlue, are no longer directly supported. Indeed, thecolor constants are part of the System.Drawing namespace's Colorstructure, so they are accessed as follows:Me.BackColor = System.Drawing.Color.BlanchedAlmondIn most cases, to access a particular constant that is not a field in theMicrosoft.VisualBasic.Constants class, you must designate theenumeration (or structure) to which it belongs, along with the constantname. For example, the vbYes constant in VB 6 continues to exist as anintrinsic constant in VB .NET. However, it has a counterpart in theMsgBoxResult enumeration, which can be accessed as follows:

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

Page 54: VB1

Helpmate

If MsgBoxResult.Yes = MsgBox("OK to proceed?", ...For a list of all built-in constants and enums, see Appendix D.

Str ing FunctionsThe LSet and RSet functions have been replaced by the PadLeft andPadRight methods of the System.String class. For instance, the followingcode pads the name "Donna" with spaces on the left to make the totalstring length equal to 15:Dim sName As String = "Donna"Msgbox(sName.PadLeft(15))The String function has been removed from the language. In its place, wesimply declare a string and initialize it, using syntax such as:Dim str As New String("A"c, 5)which will define a string containing five As. Note the use of the modifierc in "A"c to define a character (data type Char), as opposed to a String oflength 1. This is discussed in more detail in Chapter 2.

EmptinessIn VB 6, the Empty keyword indicates an uninitialized variable, and theNull keyword is used to indicate that a variable contains no valid data. VB.NET does not support either keyword, but uses the Nothing keyword inboth of these cases.According to the documentation: "Null is still a reserved word in VisualBasic .NET 7.0, even though it has no syntactical use. This helps avoidconfusion with its former meanings." Whatever.In addition, the IsEmpty function is not supported in VB .NET.

Graphical FunctionalityThe System.Drawing namespace contains classes that implement graphicalmethods. For instance, the Graphics class contains methods such asDrawEllipse and DrawLine. As a result, the VB 6 Circle and Line methodshave been dropped.Note that the VB 6 PSet and Scale methods are no longer supported andthat there are no direct equivalents in the System.Drawing namespace.

Mathematical FunctionalityMathematical functions are implemented as members of the Math class ofthe System namespace. Thus, the VB 6 math functions, such as thetrigonometric functions, have been dropped. Instead, we can usestatements such as:Math.Cos(1)Note also that the Round function has been replaced by Round method ofthe System.Math class.

DiagnosticsThe System.Diagonstics namespace provides classes related toprogramming diagnostics. Most notably, the VB 6 Debug object is gone,but its functionality is implemented in the System.Diagnostics.Debug

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

Page 55: VB1

Helpmate

class, which has methods such as Write, WriteLine (replacing Print),WriteIf, and WriteLineIf. (You won't believe it, but there is still nomethod to clear the Output window!)

MiscellaneousHere are a few additional changes to consider:

• The VB 6 DoEvents function has been replaced by the DoEventsmethod of the Application class of the System.Windows.Formsnamespace.

• The VB 6 IsNull and IsObject functions have been replaced by theIsDBNull and IsReference methods of the Information class of theMicrosoft.VisualBasic namespace. Since this namespace isimplicitly loaded by VB as part of the project template when aproject is created in Visual Studio, no Imports statement is required,and the members of its classes can be accessed withoutqualification.

• Several VB 6 functions have two versions: a String version and aVariant version. An example is provided by the Trim$ and Trimfunctions. In VB .NET, these functions are replaced by a singleoverloaded function. Thus, for instance, we can call Trim usingeither a String or Object argument.

Obsolete Programming ElementsThe following list shows some of the programming elements that havebeen removed from Visual Basic .NET:As AnyRequired all parameters to have a declared data type.Atn functionReplaced by System.Math.Atan.Calendar propertyHandled by classes in the System.Globalization namespace.Circle statementUse methods in the System.Drawing namespace.Currency data typeReplaced by the Decimal data type.Date functionReplaced by the Today property of the DateTime structure in the Systemnamespace.Date statementReplaced by the Today statement.Debug.Assert methodReplaced by the Assert method of the Debug class of theSystem.Diagonistics namespace.Debug.Print methodReplaced by the Write and WriteLine methods of the Debug class of theSystem.Diagonistics namespace.

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

Page 56: VB1

Helpmate

Deftype statementsNot supported.DoEvents functionReplaced by the DoEvents method of the Application class inSystem.Windows.Forms namespace.Empty keywordReplaced by the Nothing keyword.Eqv operatorUse the equal sign.GoSub statementNot supported.Imp operatorA Imp B is logically equivalent to (Not A) Or B.Initialize eventReplaced by the constructor method.Instancing propertyUse the constructor to specify instancing.IsEmpty functionNot supported because the Empty keyword is not supported.IsMissing functionNot supported because every optional parameter must declare a defaultvalue.IsNull functionNot supported. The Null keyword is replaced by Nothing.IsObject functionReplaced by the IsReference function.Let statementNot supported.Line statementUse the DrawLine method of the Graphics class in the System.Drawingnamespace.LSet statementUse the PadLeft method of the String class in the System namespace.Null keywordUse Nothing.On...GoSub constructionNot supported. No direct replacement.On...GoTo constructionNot supported. No direct replacement. On Error... is still supported,however.Option Base statementNot supported. All arrays have lower bound equal to 0.Option Private Module statementUse access modifiers in each individual Module statement.Property Get, Property Let, and Property Set statementsReplaced by a new unified syntax for defining properties.

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

Page 57: VB1

Helpmate

PSet methodNot supported. No direct replacement. See the System.Drawingnamespace.Round functionUse the Round method of the Math class of the System namespace.RSet statementUse the PadRight method of the String class in the System namespace.Scale methodNot supported. No direct replacement. See the System.Drawingnamespace.Set statementNot supported.Sgn functionUse Math.Sign.Sqr functionUse Math.Sqrt.String functionUse the String class constructor with parameters.Terminate eventUse the Destroy method.Time function and statementInstead of the Time function, use the TimeOfDay method of the DateTimestructure of the System namespace. Instead of the Time statement, use theTimeOfDay statement.Type statementUse the Structure statement.Variant data typeUse the Object data type.VarType functionUse the TypeName function or the GetType method of the Object class.Wend keywordReplaced by End While.Structured Exception HandlingVB .NET has added a significant new technique for error handling. Alongwith the traditional unstructured error handling through On Error Gotostatements, VB .NET adds structured exception handling, using theTry...Catch...Finally syntax supported by other languages, such as C++. Wediscuss this in detail in Chapter 7.Changes in Object-OrientationAs 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 atruly object-oriented programming language. Whatever your thoughts mayhave been on this matter, it seems clear that VB .NET is an object-orientedprogramming language by any reasonable definition of that term.

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

Page 58: VB1

Helpmate

Here are the main changes in the direction of object-orientation. Wediscuss these issues in detail in Chapter 3.

Inher itanceVB .NET supports object-oriented inheritance (but not multipleinheritance). This means that a class can derive from another (base) class,thereby inheriting all of the properties, methods, and events of the baseclass. Since forms are also classes, inheritance applies to forms as well.This allows new forms to be created based on existing forms. We discussinheritance in detail in Chapter 3.

Over loadingVB .NET supports a language feature known as function overloading. Theidea is simple and yet quite useful. We can use the same name fordifferent functions (or subroutines), as long as the functions can bedistinguished by their argument signature. The argument signature of afunction (or subroutine) is the sequence of data types of the arguments ofthe function. Thus, in order for two functions to have the same argumentsignature, they must have the same number of arguments, and thecorresponding arguments must have the same data type. For example, thefollowing declarations are legal in the same code module because theyhave different argument signatures:Overloads Sub OpenFile( ) ' Ask user for file to open and open itEnd Sub Overloads Sub OpenFile(ByVal sFile As String) ' Open file sFileEnd Sub

Object CreationVB 6 supports a form of object creation called implicit object creation. Ifan object variable is declared using the New keyword:Dim obj As New SomeClassthen the object is created the first time it is used in code. Morespecifically, the object variable is initially given the value Nothing, andthen every time the variable is encountered during code execution, VBchecks to see if the variable is Nothing. If so, the object is created at thattime.VB .NET does not support implicit object creation. If an object variablecontains Nothing when it is encountered, it is left unchanged, and no objectis 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 SomeClassAs a shorthand, we can also write:Dim obj As New SomeClassIf the object's class constructor takes parameters, then they can beincluded, as in the following example:

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

Page 59: VB1

Helpmate

Dim obj As SomeClass = New SomeClass(argument1, argument2,...)As a shorthand, we can also write:Dim obj As New SomeClass(argument1, argument2,...)For details on class constructors, see Chapter 3.

Proper tiesThere have been a few changes in how VB handles properties, particularlywith respect to default properties and property declarations.

Default properties

As you know, you can use default properties in VB 6. For instance, if txt isa textbox control, then:txt = "To be or not to be"assigns the string "To be or not to be" to the default Text property of thetextbox txt.However, there is a price to pay for default properties: ambiguity. Forexample, if txt1 and txt2 are object variables referencing two TextBoxcontrols, what does:txt1 = txt2mean? Are we equating the default properties or the object variables? InVB 6, this is interpreted as equating the default properties:txt1.Text = txt2.Textand we require the Set statement for object assignment:Set txt1 = txt2In VB .NET, default properties are not supported unless the property takesone or more parameters, in which case there is no ambiguity.As Microsoft points out, default properties occur most commonly withcollection classes. For example, in ActiveX Data Objects (ADO), theFields collection of the Recordset object has a default Item property thatreturns a particular Field object. Thus, we can write:rs.Fields.Item(1).Valueor, since the default Item property is parameterized:rs.Fields(1).ValueAlthough we may not be used to thinking of this line as using defaultproperties, it does.Thus, in VB .NET, the line:txt1 = txt2is an object assignment. To equate the Text properties, we must write:txt2.Text = txt1.TextSince it is no longer needed, the Set keyword is not supported under VB.NET, nor is the companion Let keyword.This settles the issue of equating object variables. For object variablecomparison, however, we must use the Is operator, rather than the equalsign, as in:If txt1 Is txt2 Thenor:If Not (txt1 Is txt2) Then

Property declarations

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

Page 60: VB1

Helpmate

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

.NET Framework EssentialsBy Thuan Thai & Hoang LamJune 20010-596-00165-7, Order Number: 1657320 pages, $29.95

Chapter 6Web ServicesWeb Services allow access to software components through standard webprotocols such as HTTP and SMTP. Using the Internet and XML, we cannow create software components that communicate with others, regardlessof language, platform, or culture. Until now, software developers haveprogressed toward this goal by adopting proprietary componentizedsoftware methodologies, such as DCOM; however, because each vendorprovides its own interface protocol, integration of different vendors'components is a nightmare. By substituting the Internet for proprietarytransport formats and adopting standard protocols such as SOAP, WebServices 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 andprovide examples of a web service provider and several web serviceconsumers.

Web Services in PracticeYou may have heard the phrase "software as services" and wonderedabout its meaning. The term service, in day-to-day usage, refers to what

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

Page 61: VB1

Helpmate

you get from a service provider. For example, you bring your dirtyclothing to the cleaner to use its cleaning service. Software, on the otherhand, is commonly known as an application, either off-the-shelf, shrink-wrapped, or a custom application developed by a software firm. Youtypically buy the software (or in our case, build the software). It usuallyresides on some sort of media such as floppy diskette or CD and is sold ina shrink-wrapped package through retail outlets.How can software be viewed as services? The example we are about todescribe might seem far-fetched; however, it is possible with currenttechnology. Imagine the following. As you grow more attached to theInternet, you might choose to replace your computer at home withsomething like an Internet Device, specially designed for use with theInternet. Let's call it an iDev. With this device, you can be on the Internetimmediately. If you want to do word processing, you can point your iDevto a Microsoft Word service somewhere in Redmond and type awaywithout the need to install word processing software. When you are done,the document can be saved at an iStore server where you can later retrieveit. Notice that for you to do this, the iStore server must host a softwareservice to allow you to store documents. Microsoft would charge you aservice fee based on the amount of time your word processor is runningand which features you use (such as the grammar and spell checkers). TheiStore service charges vary based on the size of your document and howlong it is stored. Of course, all these charges won't come in the mail, butrather through an escrow service where the money can be piped from andto your bank account or credit card.This type of service aims to avoid the need to upgrade of your MicrosoftWord application. If you get tired of Microsoft Word, you can choose touse a different software service from another company. Of course, thedocument that you store at the iStore server is already in a standard dataformat. Since iStore utilizes the iMaxSecure software service from acompany called iNNSA (Internet Not National Security Agency), thesecurity of your files is assured. And because you use the documentstorage service at iStore, you also benefit from having your documentauthenticated and decrypted upon viewing, as well as encrypted at storingtime.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 acentralized authentication service that you can incorporate into your websites. For sites using the Passport authentication service, it's no longernecessary 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 providedseparately today. Identification and authentication is the goal of the

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

Page 62: VB1

Helpmate

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

Web Services FrameworkWeb Services combine the best of both distributed componentization andthe World Wide Web. It extends distributed computing to broader rangesof client applications. The best thing is that it does it by seamlesslymarrying and enhancing existing technologies.

Web Services ArchitectureWeb Services are distributed software components that are accessiblethrough standard web protocols. The first part of that definition is similarto that of COM/DCOM components. However, it is the second part thatdistinguishes Web Services from the crowd. Web Services enable softwareto interoperate with a much broader range of clients. While COM-awareclients can understand only COM components, Web Services can be

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

Page 63: VB1

Helpmate

consumed by any application that understands how to parse an XML-formatted stream transmitted through HTTP channels. XML is the keytechnology used in Web Services and is used in the following areas of theMicrosoft .NET Web Services framework:Web Service wire formatsThe technology enabling universal understanding of how to perform dataexchanges between the service provider and consumer; the format of datafor the request and response.Web Service description in WSDL (Web Services Description Language)The language describing how the service can be used. Think of this as theinstructions on the washing machine at the laundromat telling you whereto put quarters, what buttons to push, etc.Web Service discoveryThe process of advertising or publishing a piece of software as a serviceand allowing for the discovery of this service.Figure 6-1 depicts the architecture of web applications using WindowsDNA, while Figure 6-2 shows .NET-enabled web applicationsarchitecture. As you can see, communication between components of aweb application does not have to be within an intranet. Furthermore,intercomponent communication can also use HTTP/XML.

Web Services Wire FormatsYou may have heard the phrase "DCOM is COM over the wire." WebServices are similar to DCOM except that the wire is no longer aproprietary communication protocol. With Web Services, the wire formatsrely on more open Internet protocols such as HTTP or SMTP.A web service is more or less a component running on the web server,exposed to the world through standard Internet protocols. Microsoft .NETWeb Services currently supports three protocols: HTTP GET, HTTPPOST, and SOAP (Simple Object Access Protocol), explained in the nextsections. Because these protocols are standard protocols for the Web, it isvery easy for the client applications to use the services provided by theserver.

HTTP GET and HTTP POST

As their names imply, both HTTP GET and HTTP POST use HTTP astheir underlying protocol. The GET and POST methods of the HTTPprotocol have been widely used in ASP (Active Server Pages), CGI, andother server-side architectures for many years now. Both of these methodsencode request parameters as name/value pairs in the HTTP request. TheGET method creates a query string and appends it to the script's URL onthe server that handles the request. For the POST method, the name/valuepairs are passed in the body of the HTTP request message.

SOAP

Similar to HTTP GET and HTTP POST, SOAP serves as a mechanism forpassing messages between the clients and servers. In this context, the

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

Page 64: VB1

Helpmate

clients are web services consumers, and the servers are the web services.The clients simply send an XML-formatted request message to the serverto get the service. The server responds by sending back yet another XML-formatted message. The SOAP specification describes the format of theseXML requests and responses. It is simple, yet extensible, because it isbased on XML.SOAP is different than HTTP GET and HTTP POST because it uses XMLto format its payload. The messages being sent back and forth have abetter structure and can convey more complex information compared tosimple name/value pairs in HTTP GET/POST protocols. Anotherdifference is that SOAP can be used on top of other transport protocols,such as SMTP in addition to HTTP.

Web Services Descr iption (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 theweb service supports. This web service description document is found inan XML schema called WSDL (Web Services Description Language).Remember that type libraries and IDL scripts are used to describe a COMcomponent. Both IDL and WSDL files describe an interface's method callsand the list of in and out parameters for the particular call. The only majordifference between the two description languages is that all descriptions inthe WSDL file are done in XML.In theory, any WSDL-capable SOAP client can use the WSDL file to get adescription of your web service. It can then use the information containedin that file to understand the interface and invoke your web service'smethods.

WSDL Structure

The root of any web service description file is the <definitions> element.Within this element, the following elements provide both the abstract andconcrete description of the service:TypesA container for datatype definitions.MessageAn abstract, typed definition of the data being exchanged between the webservice 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 messagecontains zero or more <part> parameters. Each parameter associates with aconcrete type defined in the <types> container element.Port TypeAn abstract set of operations supported by one or more endpoints.OperationAn abstract description of an action supported by the service. Eachoperation specifies the input and output messages defined as <message>elements.

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

Page 65: VB1

Helpmate

BindingA concrete protocol and data-format specification for a particular porttype. Similar to port type, the binding contains operations, as well as theinput and output for each operation. The main difference is that withbinding, we are now talking about actual transport type and how the inputand output are formatted.ServiceA collection of network endpoints--ports. Each of the web service wireformats defined earlier constitutes a port of the service (HTTP GET,HTTP POST, and SOAP ports).PortA single endpoint defined by associating a binding and a network address.In other words, it describes the protocol and data-format specification tobe used as well as the network address of where the web service clientscan bind to for the service.The following shows a typical WSDL file structure:<definitions name="" targetNamespace="" xmlns:...> <types>...</types> <message name="">...</message> ... <portType name=""> <operation name=""> <input message="" /> <output message="" /> </operation> ... </portType> ... <binding name=""> <protocol:binding ...> <operation name=""> <protocol:operation ...> <input>...</input> <output>...</output> </operation> ... </binding> ... <service name=""> <port name="" binding=""> <protocol:address location="" /> </port> ... </service></definitions>

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

Page 66: VB1

Helpmate

The <types> element contains physical type descriptions defined in XMLSchema (XSD). These types are being referred to from the <message>elements.For each of the web methods in the web service, there are two messagesdefined for a particular port: input and output. This means if a web servicesupports all three protocols: SOAP, HTTP GET, and HTTP POST, therewill be six <message> elements defined, one pair for each port. The namingconvention used by the Microsoft .NET autogenerated WSDL is:MethodName + Protocol + { In, Out}For example, a web method called GetBooks( ) will have the followingmessages:<message name="GetBooksSoapIn">...</message><message name="GetBooksSoapOut">...</message><message name="GetBooksHttpGetIn">...</message><message name="GetBooksHttpGetOut">...</message><message name="GetBooksHttpPostIn">...</message><message name="GetBooksHttpPostOut">...</message>For each protocol that the web service supports, there is one <portType>element defined. Within each <portType> element, all operations arespecified as <operation> elements. The naming convention for the port typeis:WebServiceName + ProtocolTo continue our example, here are the port types associated with the webservice that we build later in this chapter, PubsWS:<portType name="PubsWSSoap"> <operation name="GetBooks"> <input message="GetBooksSoapIn" /> <output message="GetBooksSoapOut" /> </operation></portType> <portType name="PubsWSHttpGet"> <operation name="GetBooks"> <input message="GetBooksHttpGetIn" /> <output message="GetBooksHttpGetOut" /> </operation></portType> <portType name="PubsWSHttpPost"> <operation name="GetBooks"> <input message="GetBooksHttpPostIn" /> <output message="GetBooksHttpPostOut" /> </operation></portType>We have removed namespaces from the example to make it easier to read.While the port types are abstract operations for each port, the bindingsprovide concrete information on what protocol is being used, how the datais being transported, and where the service is located. Again, there is a<binding> element for each protocol supported by the web service:<binding name="PubsWSSoap" type="s0:PubsWSSoap"> <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document" />

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

Page 67: VB1

Helpmate

<operation name="GetBooks"> <soap:operation soapAction="http://tempuri.org/GetBooks" style="document" /> <input> <soap:body use="literal" /> </input> <output> <soap:body use="literal" /> </output> </operation></binding> <binding name="PubsWSHttpGet" type="s0:PubsWSHttpGet"> <http:binding verb="GET" /> <operation name="GetBooks"> <http:operation location="/GetBooks" /> <input> <http:urlEncoded /> </input> <output> <mime:mimeXml part="Body" /> </output> </operation></binding> <binding name="PubsWSHttpPost" type="s0:PubsWSHttpPost"> <http:binding verb="POST" /> <operation name="GetBooks"> <http:operation location="/GetBooks" /> <input> <mime:content type="application/x-www-form-urlencoded" /> </input> <output> <mime:mimeXml part="Body" /> </output> </operation></binding>For SOAP protocol, the binding is <soap:binding>, and the transport isSOAP messages on top of HTTP protocol. The <soap:operation> elementdefines the HTTP header soapAction, which points to the web method. Bothinput and output of the SOAP call are SOAP messages.For both the HTTP GET and HTTP POST protocols, the binding is<http:binding> with the verb being GET and POST, respectively. Becausethe GET and POST verbs are part of the HTTP protocol, there is no needfor the extended HTTP header like soapAction for SOAP protocol. The onlything we need is the URL that points to the web method; in this case, the<soap:operation> element contains the attribute location which is set to/GetBooks.The only real difference between the HTTP GET and POST protocols isthe way the parameters are passed to the web server. HTTP GET sends theparameters in the query string, while HTTP POST sends the parameters inthe form data. This difference is reflected in the <input> elements of theoperation GetBooks for the two HTTP protocols. For the HTTP GET

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

Page 68: VB1

Helpmate

protocol, the input is specified as <http:urlEncoded />, whereas for the HTTPPOST 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 onlything left to discuss is the <service> element, which defines the portssupported by this web service. For each of the supported protocol, there isone <port> element:<service name="PubsWS"> <port name="PubsWSSoap" binding="s0:PubsWSSoap"> <soap:address location="http://.../PubsWs.asmx" /> </port> <port name="PubsWSHttpGet" binding="s0:PubsWSHttpGet"> <http:address location="http://.../PubsWs.asmx" /> </port> <port name="PubsWSHttpPost" binding="s0:PubsWSHttpPost"> <http:address location="http://.../PubsWs.asmx" /> </port> </service>Even though the three different ports look similar, their binding attributesassociate the address of the service with a binding element defined earlier.Web service clients now have enough information on where to access theservice, through which port to access the web service method, and how thecommunication messages are defined.Although it is possible to read the WSDL and manually construct theHTTP[1] conversation with the server to get to a particular web service,there are tools that autogenerate client-side proxy source code to do thesame thing. We show such a tool in "Web Services Consumers" later inthis chapter.

Web Services DiscoveryEven though advertising of a web service is important, it is optional. Webservices can be private as well as public. Depending on the businessmodel, some business-to-business (B2B) services would not normally beadvertised publicly. Instead, the web service owners would providespecific instructions on accessing and using their service only to thebusiness partner.To advertise web services publicly, authors post discovery files on theInternet. Potential web services clients can browse to these files forinformation about how to use the web services--the WSDL. Think of it asthe yellow pages for the web service. All it does is point you to where theactual web services reside and to the description of those web services.The process of looking up a service and checking out the servicedescription is called Web Service discovery. There are two ways of

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

Page 69: VB1

Helpmate

advertising the service: static and dynamic. In both of these, XMLconveys the locations of web services.

Static discovery

Static discovery is easier to understand because it is explicit in nature. Ifyou want to advertise your web service, you must explicitly create the.disco discovery file and point it to the WSDL.[2] All .disco files contain aroot element discovery as shown in the following code sample. Note thatdiscovery is in the namespace http://schemas.xmlsoap.org/disco, which is referredto as disco in this sample.<?xml version="1.0" ?><disco:discovery xmlns:disco="http://schemas.xmlsoap.org/disco"></disco:discovery>Inside the discovery element, there can be one or more of contractRef ordiscoveryRef elements. Both of these elements are described in thenamespace http://schemas.xmlsoap.org/disco/scl. The contractRef tag is used toreference an actual web service URL that would return the WSDL or thedescription of the actual web service contract. The discoveryRef tag, on theother hand, references another discovery document.This XML document contains a link to one web service and a link toanother discovery document:<?xml version="1.0" ?><disco:discovery xmlns:disco="http://schemas.xmlsoap.org/disco" xmlns:scl=" http://schemas.xmlsoap.org/disco/scl" ><scl:contractRef ref=" http://yourWebServer/yourWebService.asmx?WSDL" /><scl:discoveryRef ref=" http://yourBrotherSite/hisWebServiceDirectory.disco" /></disco:discovery>This sample disco file specifies two different namespaces: disco, which is anickname for the namespace, specified at http://schemas.xmlsoap.org/disco; andscl, which points to http://schemas.xmlsoap.org/disco/scl, where the schema forthe service discovery and service contract language is described. ThecontractRef element specifies the URL where yourWebService WSDL canbe obtained. Right below that is the discoveryRef element, which links to thediscovery file on yourBrotherSite web site. This linkage allows forstructuring networks of related discovery documents.

Dynamic discovery

As opposed to explicitly specifying the URL for all web services your sitesupports, you can enable dynamic discovery, which enables all webservices underneath a specific URL on your web site to be listedautomatically. For your web site, you might want to group related webservices under many different directories and then provide a singledynamic discovery file in each of the directory. The root tag of thedynamic discovery file is dynamicDiscovery instead of discovery.<?xml version="1.0" ?><dynamicDiscovery xmlns="urn://schemas-dynamic:disco.2000-03-17" />You can optionally specify exclude paths so that the dynamic mechanismdoes not have to look for web services in all subdirectories underneath the

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

Page 70: VB1

Helpmate

location of the dynamic discovery file. Exclude paths are in the followingform:<exclude path="pathname" />If you run IIS as your web server, you'd probably have something like thefollowing 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 flexibleconfiguration. For example, you can provide static discovery documents ateach of the directories that contain web services. At the root of the webserver, provide a dynamic discovery document with links to all staticdiscovery documents in all subdirectories. To exclude web services frompublic viewing, provide the exclude argument to XML nodes to excludetheir directories from the dynamic discovery document.

UDDI

Universal Description, Discovery, and Integration (UDDI) BusinessRegistry is like a yellow pages of web services. It allows businesses topublish their services and locate web services published by partnerorganizations so that they can conduct transactions quickly, easily, anddynamically with their trading partner.Through UDDI APIs, businesses can find services over the web that matchtheir criteria (e.g., cheapest fare), that offer the service they request (e.g.,delivery on Sunday), and so on. Currently backed by software giants suchas Microsoft, IBM, and Ariba, UDDI is important to Web Servicesbecause it enables access to businesses from a single place.

The System.Web.Services NamespaceNow that we have run through the basic framework of Microsoft .NETWeb Services, let us take a look inside what the .NET SDK provides us inthe System.Web.Services namespace.There are only a handful of classes in the System.Web.Servicesnamespace:WebServiceThe base class for all web services.WebServiceAttributeAn attribute that can be associated with a Web Service-derived class.WebMethodAttributeAn attribute that can be associated with public methods within a WebService-derived class.WebServicesConfiguration

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

Page 71: VB1

Helpmate

Information needed for the Web Service runtime.WebServicesConfigurationSectionHandlerInformation needed for the Web Service runtime.The two most important classes in the System.Web.Services namespacefor creating web services are the WebService base class andWebMethodAttribute. We make use of these classes in the next section,where we implement a Web Service provider and several Web Serviceconsumers.WebService is the base class from which all web services inherit. Itprovides properties inherent to legacy ASP programming such asApplication, Server, Session, and a new property, Context, which nowincludes Request and Response.The WebMethodAttribute class allows you to apply attributes to eachpublic method of your web service. Using this class, you can assignspecific values to the following attributes: description, session stateenabling flag, message name, and transaction mode. See the followingsection for an example of attribute setting in C# and VB.The WebServiceAttribute class is used to provide more attributes aboutthe web service itself. You can display a description of the web service, aswell as the namespace to which this web service belongs. In this book, wedo not discuss helper classes dealing with the runtime of web services.

Web Services ProviderIn this section, we describe how to develop a web service, first from thepoint of view of service providers and then of the consumers.Web Services providers implement web services and advertise them sothat the clients can discover and make use of the services. Because webservices run on top of HTTP, there must be a web server application ofsome sort on the machine that hosts the web services. This web serverapplication can be Microsoft Internet Information Services (IIS), Apache,or any other program that can understand and process the HTTP protocol.In our examples, we use Microsoft IIS, since that is the only web servercurrently supported by .NET.

Web Service Provider ExampleWe will be building a web service called PubsWS to let consumers getinformation from the sample Pubs database. All data access will be donethrough ADO.NET, so make sure you've read Chapter 5 before attemptingthe examples.Creating a web service is a three-step process.

1. Create a new asmx file for the web service. This must contain the<% webservice ... %> directive, as well as the class that provides theweb service implementation. To the Web Service clients, this asmxfile is the entry point to your Web Service. You need to put this ina virtual directory that has the executescripts permission turned on.

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

Page 72: VB1

Helpmate

2. Inherit from the WebService class of the System.Web.Servicesnamespace. This allows the derived class to access all the normalASP objects exposed in the WebService base class. From thispoint, you can use these ASP objects as if you were developing anASP-based application.[3] It is highly recommended that youspecify a namespace for your web service before publishing itpublicly because the default namespace, http://tempuri.org/, willnot uniquely identify your web service from other web services. Todo this, all you have to do is to tag the web service class with theNamespace attribute, specifying your own namespace.

3. Tag the public methods with WebMethod attributes to make webmethods--public methods of a distributed component that areaccessible via the Web. You don't have to tag a method asWebMethod unless you want that method to be published as a webmethod.

The following C# code demonstrates a simple web service that exposesfour methods to Internet clients. We emphasize "Internet" because anyonethat can access this asmx file on the web server can access these methods,as opposed to your COM component, which can be accessed only byCOM clients:<%@ WebService Language=" C#" Class=" PubsWS.PubsWS" %> namespace PubsWS{ using System; using System.Data; using System.Data.OleDb; using System.Web; using System.Web.Services; [WebService(Namespace="http://Oreilly/DotNetEssentials/")] public class PubsWS : WebService { private static string m_sConnStr = "provider=sqloledb;server=(local);database=pubs;uid=sa;pwd=;"; [WebMethod(Descr iption=" Returns a DataSet containing all authors." )] public DataSet GetAuthors( ) { OleDbDataAdapter oDBAdapter; DataSet oDS; oDBAdapter = new OleDbDataAdapter("select * from authors", m_sConnStr); oDS = new DataSet( ); oDBAdapter.Fill(oDS, "Authors"); return oDS; } [WebMethod] public DataSet GetAuthor(string sSSN)

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

Page 73: VB1

Helpmate

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

[WebMethod(MessageName=" GetBooksByAuthor " , Descr iption=" Find books by author 's SSN." )] public DataSet GetBooks(string sAuthorSSN) { OleDbDataAdapter oDBAdapter; DataSet oDS; oDBAdapter = new OleDbDataAdapter( "select * from titles inner join titleauthor on " + "titles.title_id=titleauthor.title_id " + "where au_id='" + sAuthorSSN + "'", m_sConnStr); oDS = new DataSet( ); oDBAdapter.Fill(oDS, "Books"); oDBAdapter = new OleDbDataAdapter("select * from authors " + "where au_id='" + sAuthorSSN + "'", m_sConnStr); oDBAdapter.Fill(oDS, "Author"); return oDS; } [WebMethod] public DataSet GetBooks( ) { OleDbDataAdapter oDBAdapter; DataSet oDS; oDBAdapter = new OleDbDataAdapter("select * from titles" , m_sConnStr); oDS = new DataSet( ); oDBAdapter.Fill(oDS, "Books"); return oDS; } } // end PubsWS}If you are familiar with ASP, you may recognize the usage of the @symbol in front of keyword WebService. This WebService directivespecifies the language of the web service so that ASP.NET can compilethe web service with the correct compiler. This directive also specifies theclass that implements the web service so it can load the correct class andemploy reflection to generate the WSDL for the web service.

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

Page 74: VB1

Helpmate

Because PubsWS also uses ADO.NET's OLE DB provider for its data-access needs, we have to add a reference to System.Data andSystem.Data.OleDb, in addition to the System, System.Web, andSystem.Web.Services namespaces.Class PubsWS inherits from WebService with the colon syntax that shouldbe familiar to C++ or C# developers:public class PubsWS : WebServiceThe four methods that are tagged with WebMethod attributes areGetAuthors( ), 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 methodwould be declared as:Public Function <WebMethod( )> GetAuthor(sSSN As String) As DataSetBy adding [WebMethod] in front of your public method, you make thepublic method callable from any Internet client. What goes on behind thescenes is that your public method is associated with an attribute, which isimplemented as a WebMethodAttribute class. WebMethodAttribute hassix properties:BufferResponse (boolean)Controls whether or not to buffer the method's response.CacheDurationSpecifies the length of time, in seconds, to keep the method response incache. The default is not to hold the method response in cache (0 seconds).DescriptionProvides additional information about a particular web method.EnableSession (boolean)Enables or disables session state. If you don't intend to use session statefor the web method, you might want to disable this flag so that the webserver does not have to generate and manage session IDs for each useraccessing this web method. This might improve performance. This flag istrue by default.MessageNameDistinguishes web methods with the same names. For example, if youhave two different methods called GetBooks (one method retrieves allbooks while the other method retrieves only books written by a certainauthor) and you want to publish both of these methods as web methods,the system will have a problem trying to distinguish the two methods sincetheir names are duplicated. You have to use the MessageName property tomake sure all service signatures within the WSDL are unique. If theprotocol is SOAP, MessageName is mapped to the SOAPAction requestheader and nested within the soap:Body element. For HTTP GET and HTTPPOST, it is the PathInfo portion of the URI (as inhttp://localhost//PubsWS/PubsWS.asmx/GetBooksByAuthor).TransactionOptionCan be one of five modes: Disabled, NotSupported, Supported, Required,and RequiresNew. Even though there are five modes, web methods canonly participate as the root object in a transaction. This means both

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

Page 75: VB1

Helpmate

Required and RequiresNew result in a new transaction being created forthe web method. The Disabled, NotSupported, and Supported settingsresult in no transaction being used for the web method. TheTransactionOption property of a web method is set to Disabled by default.To set up these properties, pass the property name and its value as a name= value pair: [WebMethod(Description="Returns a DataSet containing all authors.")] public DataSet GetAuthors( )You can separate multiple properties with a comma: [WebMethod(MessageName="GetBooksByAuthor", Description="Find books by author's SSN.")] public DataSet GetBooks(string sAuthorSSN)

Web.Config

If you set up your web services from scratch, you might also need toprovide the configuration file (web.config) in the same directory as yourasmx file. This configuration file allows you to control various applicationsettings about the virtual directory. The only thing we recommenddefinitively is to set the authentication mode to None to make our webservices development and testing a little easier. When you release yourweb services to the public, you would probably change this setting toWindows, Forms, or Passport instead of None:<configuration> <system.web> <authentication mode="None" /> </system.web></configuration>The following list shows the different modes of authentication:FormsBasic Forms authentication is where unauthenticated requests areredirected to a login form.WindowsAuthentication is performed by IIS in one of three ways: basic, digest, orIntegrated Windows Authentication.PassportUnauthenticated requests to the resource are redirected to Microsoft'scentralized authentication service. When authenticated, a token is passedback and used by subsequent requests.

Web Services ConsumersNow that you have successfully created a web service, let's take a look athow this web service is used by web clients.Web Services clients communicate with web services through standardweb protocols. They send and receive XML-encoded messages to andfrom the web services. This means any application on any platform canaccess the web services as long as it uses standard web protocols andunderstands the XML-encoded messages. As mentioned earlier, there arethree protocols that the web clients can employ to communicate with the

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

Page 76: VB1

Helpmate

servers (web services): HTTP GET, HTTP POST, and SOAP. Wedemonstrate next how to build client applications that utilize each of theseprotocols. These web services-client applications are done in both VB6and .NET languages, such as C# and VB.NET, to demonstrate the cross-language/cross-platform benefits of Web Services. For example, you canreplace the example in VB6 with Perl running on Unix, and the webservices should still be serving.

HTTP GET ConsumerLet's look at how it is done using HTTP GET first, since it is the simplest.In the examples that follow, we use localhost as the name of the webserver running the service and PubsWS as the virtual directory. If you havedeployed the sample web service on a remote server, you'll need tosubstitute the name of the server and virtual directory as appropriate.If you point your web browser at the web service URL(http://localhost/PubsWS/PubsWS.asmx), it will give you a list ofsupported methods. To find out more about these methods, click one ofthem. This brings up a default web service consumer. This consumer,autogenerated through the use of reflection, is great for testing your webservices' methods.[4] It uses the HTTP GET protocol to communicate withthe web service. This consumer features a form that lets you test themethod (see Figure 6-3), as well as descriptions of how to access themethod via SOAP, HTTP GET, or HTTP POST.Here is the description of the GET request and response supplied by thedefault consumer:The following is a sample HTTP GET request and response. The placeholders shownneed to be replaced with

actual values. GET /PubsWS/PubsWS.asmx/GetAuthor?sSSN=string HTTP/1.1Host: localhost HTTP/1.1 200 OKContent-Type: text/xml; charset=utf-8Content-Length: length <?xml version="1.0" encoding="utf-8"?><DataSet xmlns="http://tempuri.org/"> <schema xmlns="http://www.w3.org/2001/XMLSchema">schema</schema>xml</DataSet>Using HTTP GET protocol, the complete URL to invoke the web method,along with parameters, can be the following:http://localhost/PubsWS/PubsWS.asmx/GetAuthor?sSSN=172-32-1176Here is the response, including HTTP response headers and the raw XML(note how the response includes the serialized schema and data from theDataSet object):Cache-Control: private, max-age=0Date: Tue, 08 May 2001 20:53:16 GMTServer: Microsoft-IIS/5.0

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

Page 77: VB1

Helpmate

Content-Length: 2450Content-Type: text/xml; charset=utf-8Client-Date: Tue, 08 May 2001 20:53:16 GMTClient-Peer: 127.0.0.1:80 <?xml version="1.0" encoding="utf-8"?><DataSet xmlns="http://tempuri.org/"> <xsd:schema id="NewDataSet" targetNamespace="" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:element name="NewDataSet" msdata:IsDataSet="true"> <xsd:complexType> <xsd:choice maxOccurs="unbounded"> <xsd:element name="SelectedAuthor"> <xsd:complexType> <xsd:sequence> <xsd:element name="au_id" msdata:DefaultValue="NULL" type="xsd:string" minOccurs="0" msdata:Ordinal="0" /> <xsd:element name="au_lname" msdata:DefaultValue="NULL" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="au_fname" msdata:DefaultValue="NULL" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> <xsd:element name="phone" msdata:DefaultValue="NULL" type="xsd:string" minOccurs="0" msdata:Ordinal="3" /> <xsd:element name="address" msdata:DefaultValue="NULL" type="xsd:string" minOccurs="0" msdata:Ordinal="4" /> <xsd:element name="city" msdata:DefaultValue="NULL" type="xsd:string" minOccurs="0" msdata:Ordinal="5" /> <xsd:element name="state" msdata:DefaultValue="NULL" type="xsd:string" minOccurs="0" msdata:Ordinal="6" /> <xsd:element name="zip" msdata:DefaultValue="NULL" type="xsd:string" minOccurs="0" msdata:Ordinal="7" /> <xsd:element name="contract" msdata:DefaultValue="NULL" type="xsd:boolean" minOccurs="0" msdata:Ordinal="8" /> </xsd:sequence> </xsd:complexType> </xsd:element> </xsd:choice> </xsd:complexType> </xsd:element> </xsd:schema> <NewDataSet xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:updg="urn:schemas-microsoft-com:xml-updategram"> <updg:sync> <msdata:unchanged> <SelectedAuthor updg:id="f5237587-4918-44c6-b5cb-51a84e6af4e3"

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

Page 78: VB1

Helpmate

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

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

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

Page 79: VB1

Helpmate

The other way to use the proxy is more flexible. You can compile the C#source file into a dynamic link library (DLL) and then add a reference tothis DLL to any project you want to create. This way you can even have aVB project use the DLL.Below is the command line used to compile the C# proxy source into aDLL. Notice that the three references are linked to PubsWS.cs so that theresulting PubsWS.DLL is self-contained (type the entire command on oneline):csc /t:library /r:system.web.services.dll /r:system.xml.dll /r:system.data.dll PubsWS.csRegardless of how you choose to use the proxy, the client application codewill still look the same. Consider the next two code examples containingC# and VB code. For both languages, the first lines create an instance ofthe proxy to the web service, PubsWS. The second lines invoke theGetBooks web method to get a DataSet as the result. The remaining linesbind the default view of the table Books to the data grid, add the data gridto a form, and display the form. Note that these examples use theWindows Forms API, which we'll discuss in Chapter 8.Here is the C# web service client, TestProxy.cs :using System;using System.Drawing;using System.Windows.Forms;using System.Data; public class TestProxy{ public static void Main( ) { /* Create a proxy. * / PubsWS oProxy = new PubsWS( ); /* Invoke GetBooks( ) over SOAP and get the data set. */ DataSet oDS = oProxy.GetBooks( ); /* Create a data grid and connect it to the data set. */ DataGrid dg = new DataGrid( ); dg.Size = new Size(490, 270); dg.DataSource = oDS.Tables["Books"].DefaultView; /* Set the properties of the form and add the data grid. * / Form myForm = new Form( ); myForm.Text = "DataGrid Sample"; myForm.Size = new Size(500, 300); myForm.Controls.Add(dg); /* Display the form. * / System.Windows.Forms.Application.Run(myForm);

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

Page 80: VB1

Helpmate

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

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

Non-.NET ConsumersThis section shows how to develop non-.NET web service consumersusing HTTP GET, HTTP POST, and SOAP protocols. Because we cannotjust create the proxy class from the WSDL and compile it with the clientcode directly, we must look at the WSDL file to understand how toconstruct and interpret messages between the web service and the clients.We trimmed down the WSDL file for our PubsWS web service to showonly types, messages, ports, operations, and bindings that we actually usein the next several web service-client examples. In particular, we will haveour VB6 client access the following:

Web method Protocol

GetBooks( ) HTTP GET protocol

GetAuthor(ssn) HTTP POST protocol

GetBooksByAuthor(ssn) SOAP protocol

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

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

Page 81: VB1

Helpmate

<?xml version="1.0" encoding="utf-8"?><definitions xmlns:... xmlns:s0="http://Oreilly/DotNetEssentials/" targetNamespace="http://Oreilly/DotNetEssentials/" > <types> <!-- This datatype is used by the HTTP POST call --> <s:element name="GetAuthor"> <s:complexType> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="sSSN" nillable="true" type="s:string" /> </s:sequence> </s:complexType> </s:element> <!-- This datatype is used by the HTTP POST call --> <s:element name="GetAuthorResponse"> <s:complexType> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="GetAuthorResult" nillable="true"> <s:complexType> <s:sequence> <s:element ref="s:schema" /> <s:any /> </s:sequence> </s:complexType> </s:element> </s:sequence> </s:complexType> </s:element> <!-- This datatype is used by the SOAP call --> <s:element name="GetBooksByAuthor"> <s:complexType> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="sAuthorSSN" nillable="true" type="s:string" /> </s:sequence> </s:complexType> </s:element> <!-- This datatype is used by the SOAP call --> <s:element name="GetBooksByAuthorResponse"> <s:complexType> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="GetBooksByAuthorResult" nillable="true"> <s:complexType> <s:sequence> <s:element ref="s:schema" /> <s:any /> </s:sequence> </s:complexType> </s:element> </s:sequence> </s:complexType>

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

Page 82: VB1

Helpmate

</s:element> <!-- This datatype is used by the HTTP GET call --> <s:element name="GetBooks"> <s:complexType /> </s:element> <!-- This datatype is used by the HTTP GET call --> <s:element name="GetBooksResponse"> <s:complexType> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="GetBooksResult" nillable="true"> <s:complexType> <s:sequence> <s:element ref="s:schema" /> <s:any /> </s:sequence> </s:complexType> </s:element> </s:sequence> </s:complexType> </s:element> <!-- This datatype is used by the HTTP GET/POST responses --> <s:element name="DataSet" nillable="true"> <s:complexType> <s:sequence> <s:element ref="s:schema" /> <s:any /> </s:sequence> </s:complexType> </s:element> </types> <!-- These messages are used by the SOAP call --> <message name="GetBooksByAuthorSoapIn"> <part name="parameters" element="s0:GetBooksByAuthor" /> </message> <message name="GetBooksByAuthorSoapOut"> <part name="parameters" element="s0:GetBooksByAuthorResponse" /> </message> <!-- These messages are used by the HTTP GET call --> <message name="GetBooksHttpGetIn" /> <message name="GetBooksHttpGetOut"> <part name="Body" element="s0:DataSet" /> </message> <!-- These messages are used by the HTTP POST call --> <message name="GetAuthorHttpPostIn"> <part name="sSSN" type="s:string" /> </message> <message name="GetAuthorHttpPostOut"> <part name="Body" element="s0:DataSet" /> </message>

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

Page 83: VB1

Helpmate

<!-- SOAP por t --> <portType name="PubsWSSoap"> <operation name="GetBooks"> <documentation>Find books by author's SSN.</documentation> <input name="GetBooksByAuthor" message="s0:GetBooksByAuthorSoapIn" /> <output name="GetBooksByAuthor" message="s0:GetBooksByAuthorSoapOut" /> </operation> </portType> <!-- HTTP GET por t --> <portType name="PubsWSHttpGet"> <operation name="GetBooks"> <input message="s0:GetBooksHttpGetIn" /> <output message="s0:GetBooksHttpGetOut" /> </operation> </portType> <!-- HTTP POST por t --> <portType name="PubsWSHttpPost"> <operation name="GetAuthor"> <input message="s0:GetAuthorHttpPostIn" /> <output message="s0:GetAuthorHttpPostOut" /> </operation> </portType> <!-- SOAP binding --> <binding name="PubsWSSoap" type="s0:PubsWSSoap"> <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document" /> <operation name="GetBooks"> <soap:operation soapAction="http://Oreilly/DotNetEssentials/GetBooksByAuthor" style="document" /> <input name="GetBooksByAuthor"> <soap:body use="literal" /> </input> <output name="GetBooksByAuthor"> <soap:body use="literal" /> </output> </operation> </binding> <!-- HTTP GET binding --> <binding name="PubsWSHttpGet" type="s0:PubsWSHttpGet"> <http:binding verb="GET" /> <operation name="GetBooks"> <http:operation location="/GetBooks" /> <input> <http:urlEncoded /> </input> <output> <mime:mimeXml part="Body" />

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

Page 84: VB1

Helpmate

</output> </operation> </binding> <!-- HTTP POST binding --> <binding name="PubsWSHttpPost" type="s0:PubsWSHttpPost"> <http:binding verb="POST" /> <operation name="GetAuthor"> <http:operation location="/GetAuthor" /> <input> <mime:content type="application/x-www-form-urlencoded" /> </input> <output> <mime:mimeXml part="Body" /> </output> </operation> </binding> <!-- The whole Web Service and address bindings --> <service name="PubsWS"> <port name="PubsWSSoap" binding="s0:PubsWSSoap"> <soap:address location="http://localhost/PubsWS/PubsWS.asmx" /> </port> <port name="PubsWSHttpGet" binding="s0:PubsWSHttpGet"> <http:address location="http://localhost/PubsWS/PubsWS.asmx" /> </port> <port name="PubsWSHttpPost" binding="s0:PubsWSHttpPost"> <http:address location="http://localhost/PubsWS/PubsWS.asmx" /> </port> </service> </definitions>In both the HTTP GET and HTTP POST protocols, you pass parametersto the web services as name/value pairs. With the HTTP GET protocol,you must pass parameters in the query string, whereas the HTTP POSTprotocol packs the parameters in the body of the request package. Todemonstrate this point, we will construct a simple VB client using bothHTTP GET and HTTP POST protocols to communicate with the PubsWSweb service.Let's first create a VB6 standard application. We need to add a reference toMicrosoft XML, v3.0 (msxml3.dll ), because we'll use the XMLHTTPobject to help us communicate with the web services. For demonstrativepurposes, 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 thecaptions GET and POST, as well as the names cmdGet and cmdPost,respectively. After that, drag the WebBrowser object from the toolbar ontothe form, and name the control myWebBrowser. If you make the

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

Page 85: VB1

Helpmate

WebBrowser navigate to about:blank initially, you will end up withsomething like Figure 6-5.Figure 6-5. VB client form to test Web Services

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

' Call the web service to get an XML document Set oXMLHTTP = New XMLHTTP oXMLHTTP.open " GET" ,_ " http://localhost/PubsWS/PubsWS.asmx/GetBooks" , _ False oXMLHTTP.send Set oDOM = oXMLHTTP.responseXML

' Create the XSL document to be used for transformation Set oXSL = New DOMDocument oXSL.Load App.Path & "\templateTitle.xsl"

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

Set oXSL = Nothing Set oDOM = Nothing Set oXMLHTTP = NothingEnd Sub Pr ivate Sub cmdPost_Click( ) Dim oXMLHTTP As XMLHTTP Dim oDOM As DOMDocument Dim oXSL As DOMDocument

' Call the web service to get an XML document Set oXMLHTTP = New XMLHTTP oXMLHTTP.open " POST" , _ " http://localhost/PubsWS/PubsWS.asmx/GetAuthor " , _ False oXMLHTTP.setRequestHeader " Content-Type" , _ " application/x-www-form-ur lencoded" oXMLHTTP.send "sSSN=172-32-1176" Set oDOM = oXMLHTTP.responseXML

' Create the XSL document to be used for transformation Set oXSL = New DOMDocument oXSL.Load App.Path & "\templateAuthor.xsl"

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

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

Page 86: VB1

Helpmate

myWebBrowser.Document.Close Set oXSL = Nothing Set oDOM = Nothing Set oXMLHTTP = NothingEnd SubThe two subroutines are similar in structure, except that the first one usesthe HTTP GET protocol and the second one uses the HTTP POSTprotocol to get to the PubsWS web service. Let's take a closer look at whatthe two subroutines do.For the HTTP GET protocol, we use the XMLHTTP object to point to theURL for the web method, as specified in the WSDL. Since the GetBooksweb method does not require any parameters, the query string in this caseis empty. The method is invoked synchronously because the asyncparameter to XMLHTTP's open method is set to false. After the methodinvocation is done, we transform the XML result using templateTitle.xsland display the HTML on the myWebBrowser instance on the form. Figure6-6 displays the screen of our web services testing application afterinvoking the GetBooks web method at URLhttp://localhost/PubsWS/PubsWS.asmx/ through HTTP GET protocol.For the HTTP POST protocol, we also point the XMLHTTP object to theURL for the web method--in this case, method GetAuthor. Because this isa POST request, we have to specify in the HTTP header that the request iscoming over as a form by setting the Content-Type header variable toapplication/x-www-form-urlencoded. If this variable is not set, XMLHTTP bydefault passes the data to the server in XML format.Another difference worth noticing is that the GetAuthor method requires asingle parameter, which is the SSN of the author as a string. Since this is apost request, we are going to send the name/value pair directly to theserver in the body of the message. Because the Content-Type header hasbeen set to application/x-www-form-urlencoded, the server will know how toget to the parameters and perform the work requested. This time, we usetemplateAuthor.xsl to transform the XML result to HTML and display it.Figure 6-7 shows our application after invoking the GetAuthor webmethod of PubsWS web service through HTTP POST protocol.Figure 6-7. VB client form after calling GetAuthor

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

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

Page 87: VB1

Helpmate

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

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

Page 88: VB1

Helpmate

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

' Call the web service to get an XML document Set oXMLHTTP = New XMLHTTP oXMLHTTP.open "POST", "http://localhost/PubsWS/PubsWS.asmx", False

Dim sB As String sBody = "" & _ "<soap:Envelope" & _ " xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance""" & _ " xmlns:xsd=""http://www.w3.org/2001/XMLSchema""" & _ " xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"">" & _ "<soap:Body>" & _ "<GetBooksByAuthor xmlns=""http://Oreily/DotNetEssential/"">" & _ "<sAuthorSSN>213-46-8915</sAuthorSSN>" & _ "</GetBooksByAuthor>" & _ "</soap:Body>" & _ "</soap:Envelope>" oXMLHTTP.setRequestHeader " Content-Type" , " text/xml" oXMLHTTP.setRequestHeader " SOAPAction" , "http://Oreilly/DotNetEssentials/GetBooksByAuthor"

oXMLHTTP.send sB Set oDOM = oXMLHTTP.responseXML

' Create the XSL document to be used for transformation Set oXSL = New DOMDocument oXSL.Load App.Path & "\templateAuthorTitle.xsl"

' Transform the XML document into an HTML document myWebBrowser.Document.Write oDOM.transformNode(oXSL) myWebBrowser.Document.Close Set oXSL = Nothing Set oDOM = Nothing Set oXMLHTTP = NothingEnd SubThis method is structurally similar to the ones used for HTTP GET andHTTP POST; however, it has some very important differences. In SOAP,you have to set the Content-Type to text/xml instead of application/ x-www-form-urlencoded as for the HTTP POST. By this time, it should be clear to youthat only HTTP POST and SOAP care about the Content-Type because they

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

Page 89: VB1

Helpmate

send the data in the body of the HTTP request. The HTTP GET protocoldoes not really care about the Content-Type because all of the parameters arepackaged into the query string. In addition to the difference in format ofthe data content, you also have to refer to the WSDL to set the SOAPActionheader variable to the call you want. Looking back at the SOAP section ofthe WSDL, if you want to call the GetBooks(sAuthorSSN) method of the webservice, you will set the SOAPAction header variable tohttp://Oreilly/DotNetEssentials/GetBooksByAuthor. On the other hand, if youwant to call the GetBooks( ) method instead, the SOAPAction variable hasto be set to http://Oreilly/DotNetEssentials/GetBooks. The reason the namespaceis http://Oreilly/DotNetEssentials/ is because we set it up as the attribute of thePubsWS web service class.After setting up the header variables, we pass the parameters to the serverin the body of the message. Whereas HTTP POST passes the parametersin name/value pairs, SOAP passes the parameters in a well-defined XMLstructure:<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 Bodyinside an Envelope. With the previously specified request, the responseSOAP message looks like this:<?xml version="1.0"?><soap:Envelope ...namespace omitted...> <soap:Body> <GetBooksByAuthorResult xmlns="http://Oreilly/DotNetEssentials/"> <result> <xsd:schema id="NewDataSet" ...> <... content omitted ...> </xsd:schema> <NewDataSet xmlns=""> <Books> <title_id>BU1032</title_id> <title>The Busy Executive's Database Guide</title> <... more ...> </Books> <Books> <title_id>BU2075</title_id> <title>You Can Combat Computer Stress!</title> <... more ...> </Books> <Author> <au_id>213-46-8915</au_id> <au_lname>Green</au_lname> <au_fname>Marjorie</au_fname> <phone>415 986-7020</phone>

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

Page 90: VB1

Helpmate

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

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

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

Page 91: VB1

Helpmate

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

Web Services and Secur ityThis section demonstrates how to incorporate security into your webservice. We will do so in two ways: system security and applicationsecurity. System-level security allows for restricting access to the webservices from unauthorized clients. It is done in a declarative fashion,whereas application-level security is more flexible. With system-levelsecurity, you will most likely have the list of authorized clients' IPaddresses that you will let access your web service through the use ofsome configuration-management tools. With application-level security,you will incorporate the authentication into your web service, thusproviding a more flexible configuration.

System Secur ityBecause web services communication is done through HTTP, you canapply system-level security on web services just as you do for other webpages or resources on your web site.There are a number of different ways you can secure your web services.For a B2B solution, you can use the IIS Administration Tool to restrict orgrant permission to a set of IP addresses, using the Internet ProtocolSecurity (IPSec) to make sure that the IP address in the TCP/IP header isauthenticated. When you rely only on the client to provide the IP in theTCP/IP header, hackers can still impersonate other host IPs whenaccessing your web services. IPSec authenticates the host addresses usingthe Kerberos authentication protocol. You can also use a firewall torestrict access to your web services for your partner companies. For abusiness-to-consumer (B2C) scenario, you can take advantage of theauthentication features of the HTTP protocol.To show how to use the authentication feature of the HTTP protocol tosecure your web services, let's revisit the example web service we have inthis chapter, PubsWS. All we have to do to secure PubsWS web service isgo to the IIS Admin Tool and choose to edit the File Security properties forthe PubsWS.asmx. Instead of keeping the default setting, which leaves thisfile accessible to all anonymous users, we change this setting to BasicAuthentication. After this change, only users that pass the authentication canmake use of the web service.For real-life situations, of course, we are not just going to use the BasicAuthentication method because it sends the username and password inclear text through the HTTP channel. We would choose other methods,

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

Page 92: VB1

Helpmate

such as Secure Sockets Layer (SSL) underneath Basic Authentication, sothat the data passed back and forth is secure. Available methods include:Basic AuthenticationSends the username and password to the web server in clear text. IISauthenticates the login against the database of users for the domain.Basic over SSL AuthenticationSimilar to Basic Authentication, except that the username and passwordare sent with Secure Sockets Layer (SSL) encryption.Digest AuthenticationUses a hashing technique, as opposed to SSL encryption, to send clientcredentials securely to the server.Integrated Windows AuthenticationGood for intranet scenarios only. Uses the login information of the clientfor authentication.Client Certificates AuthenticationRequires each of the clients to obtain a certificate that is mapped to a useraccount. The use of client-side digital certificates is not widespread at thistime.

Application Secur ityA less systematic way of securing your web services involves takingsecurity into your own hands. You can program your web services so thatall of their methods require an access token, which can be obtained fromthe web service after sending in the client's username and password. Theclient credentials can be sent to the server through SSL, which eliminatesthe risk of sending clear-text passwords across the wire. Through this SSLchannel, the server returns an access token to the caller, who can use it toinvoke all other web service methods. Of course, all of the other webmethods that you publish have to have one parameter as the token. Asimple pseudocode example of a bank account web service can be asfollows:Web Service Bank Account Web Methods: Login(user id, password) returns access token or nothing Deposit(access token, account number, amount, balance) returns T/F Withdraw(access token, account number, amount, balance) returns T/FThe only method that should be on SSL is the Login method. Once thetoken is obtained, it can be used for other web methods. Of course, youshould be able to make sure that subsequent calls using this token arecoming from the same IP as theLogin( ) call. You can also incorporate an expiration timestamp on thisaccess token to ensure that the token only exists in a certain time frameuntil a renewal of the access token is needed.The Microsoft .NET Cryptographic Services can be very useful if youchoose this route. DES, RC2, TripleDES, and RSA encryption/decryptionalgorithms are supported along with hashing methods such as SHA and

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

Page 93: VB1

Helpmate

MD5. These implementations in the .NET library enable developers toavoid low-level grunt work and focus on the application logic.

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

1. Current Microsoft .NET SOAP implementation runs on top of HTTP.2. If you use Visual Studio.NET to create your web service, the discoveryfile is created automatically.3. You will have to get to the Request and Response objects through theContext property of the WebService class.4. A simply Reflection example can be found in the section .5. wsdl.exe generates the proxy source code similar to the way IDLcompilers generate source files for DCOM proxies. The only difference isthat WSDL is the language that describes the interface of the softwarecomponent, which is XML-based.

ActiveX Data Objects (ADO)

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

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

Page 94: VB1

Helpmate

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

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

Object Functionality

Connection Manages the connection with the data source

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

Recordset Contains the data that is retrieved from the data source

These objects present an interface in the form of properties and methods that can be queried andmanipulated. 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 theInternet.

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 oldertechnologies, ADO is more flexible, ADO code is easier to write, read and maintain. ADO is builton top of OLE DB and is capable of accessing any sort of data that is wrapped and exposed by anappropriate OLE DB provider.

Connectionless RecordsetsConnectionless 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 importantfeature provided by the ADO is the introduction of the principle that recordsets are creatableobjects. With ADO you can access any sort of structured data. Recordset is the most used objectin 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 pointerposition within the recordset. Whenever you employ ADO, you are using the recordsets to carrydata back and forth. Recordsets always contains data, but this data does not necessarily match atable’s records.

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

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

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

Page 95: VB1

Helpmate

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

DISPLAYING DATA WITH A RECORDSET (USING A CONNECTION OBJECT)

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

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

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

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

objRec.ActiceConnection = conn

objRec.open “select * from table1”

while not objRec.EOF

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

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

ObjRec.MoveNext

Wend

ObjRec.Close

Conn.Close

DISPLAYING DATA WITH A RECORDSET (WITHOUT A CONNECTION OBJECT)

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

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

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

while not objRec.EOF

Response.write objRec(“ fname”) &

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

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

Page 96: VB1

Helpmate

objRec.MoveNext

wend

objRec.Close

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

Now that you have seen the examples demonstrating the usage of Recordset objects with thedatabase, 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 norelationship to an OLE DB data source. The code generates a recordset that reads driveinformation through the FileSystem Scripting Object. So, you will learn not only how to create anew recordset (connectionless recordset) but also, how to use the FileSystem Scripting Object.The code shown below is written in VBScript. The ASP version is also provided with this article.See the related documents.

CODE

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

'Name: ConnectionlessRS.vbs

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

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

'Constants

Const adUseClient = 3

const adCurrency = 6

const adBSTR = 8

'Local Variables

dim fso, rst, drives

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

Page 97: VB1

Helpmate

'Creates the main objects of the script

set fso = CreateObject("scripting.filesystemobject")

set rst = CreateObject("ADODB.Recordset")

'Gets the collection of available drives

set drives = fso.Drives

'Prepares the Recordset structure: Root, Volume, Type

'FileSystem, FreeSpace

rst.CursorLocation = aduseclient

rst.Fields.Append "Root", adBSTR

rst.Fields.Append "Volume", adBSTR

rst.Fields.Append "Type",adBSTR

rst.Fields.Append "FileSystem",adBSTR

rst.Fields.Append "FreeSpace",adCurrency

rst.Open

'Fills the recordset out with drive information

for each drv in drives

rst.AddNew

if drv.isready then

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

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

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

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

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

end if

next

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

Page 98: VB1

Helpmate

'Displays the recordset

s = ""

rst.movefirst

while not rst.EOF

for each fld in rst.Fields

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

next

s = s & vbcrlf

rst.MoveNext

wend

msgbox s

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

'Pad(str, numChars)

'Pads the specified string with the specified trailing blanks

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

Function Pad(str, numChars)

str = str & space(numChars)

Pad= Left(str,numchars)

end function

CODE DETAILS

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

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

Page 99: VB1

Helpmate

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

I've been using ActiveX Data Objects (ADO) since version 1.5--that's a lot of versionsago. Many changes have taken place in this time. With each version I've learned moreabout 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 alwayskeep in mind; some are techniques you might not have known about; and a couple arejust classified as essential knowledge when developing with ADO.

1. Share Connection objects.

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

Set rec1 = New ADODB.Record rec1.Open "localstart.asp", _ "URL=http://localhost/"

Set rec2 = New ADODB.Record rec2.Open "global.asa", _ "URL=http://localhost/"

Set rec3 = New ADODB.Record rec3.Open "iisstart.asp", _ "URL=http://localhost/"

' ' do something here '

rec1.Close rec2.Close rec3.Close

Set rec1 = Nothing Set rec2 = Nothing Set rec3 = Nothing

To save resources, you should use one Connection object and pass it to eachobject that requires an active connection: Dim con As ADODB.Connection

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

Set con = New ADODB.Connection con.Open "URL=http://localhost/"

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

Page 100: VB1

Helpmate

Set rec1 = New ADODB.Record rec1.Open "localstart.asp", con

Set rec2 = New ADODB.Record rec2.Open "global.asa", con

Set rec3 = New ADODB.Record rec3.Open "iisstart.asp", con

' ' do something here '

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

Set rec1 = Nothing Set rec2 = Nothing Set rec3 = Nothing Set con = Nothing

2. Read the ConnectionString property.

You can always read the ConnectionString property of any Connection object that isopen, including one returned from the Recordset, Command, or Record object’sActiveConnection property. Dim com As ADODB.Command Dim rst As ADODB.Recordset

Set com = New ADODB.Command

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

com.CommandText = "SELECT * FROM Customers"

Set rst = com.Execute

MsgBox com.ActiveConnection.ConnectionString

rst.Close

Set rst = Nothing Set com = Nothing

When the above code is run, you will get the following output displayed in amessage box:

Provider=Microsoft.Jet.OLEDB.4.0; Password=""; User ID=Admin; Data Source=NWind.mdb;

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

Page 101: VB1

Helpmate

Mode=Share Deny None; Extended Properties=""; Jet OLEDB:System database=""; Jet OLEDB:Registry Path=""; Jet OLEDB:Database Password=""; Jet OLEDB:Engine Type=4; Jet OLEDB:Database Locking Mode=0; Jet OLEDB:Global Partial Bulk Ops=2; Jet OLEDB:Global Bulk Transactions=1; Jet OLEDB:New Database Password=""; Jet OLEDB:Create System Database=False; Jet OLEDB:Encrypt Database=False; Jet OLEDB:Don't Copy Locale on Compact=False; Jet OLEDB:Compact Without Replica Repair=False; Jet OLEDB:SFP=False

You can now parse this string to find out specific information about theconnection, such as if the database will be encrypted when it is compacted (JetOLEDB:Encrypt Database property).

3. Use dynamic properties.

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

Set con = New ADODB.Connection

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

con.Open

' ' the user will be prompted for the database '

con.Close

Set con = Nothing

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

4. Choose your cursor location wisely.

When choosing your cursor location, you need to consider what services areimportant for your connection.

If services from a data provider are what you seek, you will need to use a server-side cursor. These are the services offered with the driver for a data source, andthey are usually very flexible. In addition, by keeping a server-side cursor you

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

Page 102: VB1

Helpmate

don't always have to move all of the data to the client, as you would with a client-side server.

On the other hand, local cursor services, such as the Microsoft Data ShapingService for OLE DB, offer services only available if you choose a client-sidecursor. For these services to be manipulated, they require data to reside on thelocal machine, as with the Data Shaping Service.

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

5. Choose your cursor type wisely.

Equally as important as choosing the location of your cursor is choosing the typeof cursor you want to use.

There are four types of cursors, each with its own set of advantages anddisadvantages.

The Static cursor provides a snapshot of the data at a given moment.Modifications, including additions and deletions from other users, are notavailable in this type of cursor. The Static cursor is good for reporting, where youneed a nonchanging view of data, but it is not necessarily the fastest. Becausechanges to data are not shown, a copy of the source at a given moment needs to becreated and maintained by the service provider for each connection using a Staticcursor.

A Forward Only cursor is identical to a Static cursor except you can only moveforward through the data, without moving back. This will improve theperformance, as compared to the Static cursor, but it still requires the data sourceto maintain a temporary copy of itself so that changes by other users do not affectyour data.

The Dynamic cursor allows you to see changes and deletions by other users andyou are allowed to move freely throughout the Recordset. Unlike the Static or theForward Only cursors, the Dynamic cursor does not require that the data sourcemaintains a still image of the data, and thus, a Dynamic cursor can be faster thanthe first two cursors.

The last cursor type is the Keyset cursor. The Keyset cursor is very similar to theDynamic cursor, except you can't see records added by other users. Recordsdeleted by other users also become inaccessible in a Keyset cursor. As with aDynamic cursor, changes by other users are also visible. A Keyset cursor can befaster than a Dynamic cursor because it doesn't have to constantly look to see ifnew records are added or deleted (because added records are not seen and deletedrecords become inaccessible).

A cursor type for each season--take your pick.

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

Page 103: VB1

Helpmate

6. Create parameters manually.

Microsoft technical writers and anyone else who writes about ADO will beat yousilly with this one, so I don’ t want to be any different.

When performance is an issue (and when is it not?), manually define yourparameters.

Dim con As ADODB.Connection Dim com As ADODB.Command Dim par As ADODB.Parameter Dim rst As ADODB.Recordset

Set con = New ADODB.Connection

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

Set com = New ADODB.Command

Set com.ActiveConnection = con

Set par = com.CreateParameter _ ("CategoryName", _ adVarWChar, _ adParamInput, _ 15)

com.Parameters.Append par

Set par = com.CreateParameter _ ("OrdYear", _ adVarWChar, _ adParamInput, _ 4)

com.Parameters.Append par

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

Set rst = com.Execute

' ' do something here '

rst.Close con.Close

Set com = Nothing Set rst = Nothing Set con = Nothing

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

Page 104: VB1

Helpmate

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

7. Create buffers with Stream objects.

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

Dim str As ADODB.Stream

Set str = New ADODB.Stream

str.Open

str.WriteText "This is a message that I " str.WriteText "would like to keep in " str.WriteText "memory.", adWriteLine

str.WriteText "This will be the second " str.WriteText "line.", adWriteLine

MsgBox "The buffer has " & _ str.Size & " characters."

str.Position = 0

MsgBox "The buffer contains: " & str.ReadText

str.Close

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

8. Check for warnings.

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

The Connection.Open, Recordset.CancelBatch, Recordset.Resync, and Recordset.UpdateBatchmethods as well as the Recordset.Filter property can all generate warnings.

To detect a data-provider warning (or error), call the Connection.Errors.Clear methodprior to using any of the above methods to begin an operation. After the operationis complete, use the Count property of the Errors collection to determine if therewere any warnings.

9. Nest transactions.

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

Page 105: VB1

Helpmate

With the Jet OLE DB provider, you can nest transactions up to five levels. Usingmultiple-level transactions gives you unprecedented control over your data.

Dim con As ADODB.Connection Dim iLevel As Integer

Set con = New ADODB.Connection

con.CursorLocation = adUseClient

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

con.BeginTrans

' ' change 1 '

con.BeginTrans

' ' change 2 '

con.BeginTrans

' ' change 3 ' iLevel = con.BeginTrans

' ' change 4 '

MsgBox "Level " & iLevel

con.CommitTrans

con.RollbackTrans

con.CommitTrans

con.CommitTrans

con.Close

Set con = Nothing

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

10. Don’ t underestimate data shaping.

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

Page 106: VB1

Helpmate

My last tip for you is do not underestimate the power of the Microsoft DataShaping Service for OLE DB.

Data shaping allows you to create aggregations with multiple SQL statements tocreate hierarchical recordsets, where individual fields can point to entire childrenrecordsets.

For instance, if you took two tables from the Biblio database, Publishers andTitles, you could create the following SQL statement to join them in onerecordset.

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 sizeof 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 cursorservice, where they are linked in a hierarchical fashion using Chapters as fieldtypes to access child recordsets.

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

Page 107: VB1

Helpmate

Dim con As ADODB.Connection Dim rstPubs As ADODB.Recordset Dim rstTitles As ADODB.Recordset

Dim sShape As String

Set con = New ADODB.Connection Set rstPubs = New ADODB.Recordset

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

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

rstPubs.Open sShape, con

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

rstPubs.Close con.Close

Set rstPubs = Nothing Set con = Nothing

When the above code is run, we see the following output:

A K PETERS A Physical Approach to Color... Colour Principles for Computer... A SYSTEM PUBNS C Plus Plus Reference Card C Reference Card AA BALKEMA Planning With Linear Programming AARP Thesaurus of Aging Terminology : ... ABACUS Access 2.0 Programming Bible Advanced Access Programming

Powerful stuff.

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

Page 108: VB1

Helpmate

VB & VBA Booklet

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

Implementing Custom EventsIn the early versions of VB, programmers were limited to working with the built-inevents. In VB5, however, three simple keywords--Event, RaiseEvent, and WithEvents--wereadded to the language to allow the programmer to define custom events or to trap eventsin external objects that would otherwise be inaccessible.

Custom events applications

Custom events can be used for any of the following:

• To report the progress of an asynchronous task back to the client application froman out-of-process ActiveX EXE component.

• To pass through events fired by the underlying control in an ActiveX customcontrol.

• As a central part of a real-time multiuser application in an n-tier client-serverapplication. (Incidentally, events can't be fired from within a MicrosoftTransaction Server Context.)

• To receive notification of events fired in automation servers.

• To query the user and receive further input.

Custom event rules

The following are some of the rules and "gotchas" for defining custom events:

• Events can be declared and fired only from within object modules (i.e., Form,User Control, and Class modules). You can't declare and fire events from astandard code module.

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

Page 109: VB1

Helpmate

• Events can be handled or intercepted only from within object modules. You can'thandle any type of event from within a code module. This isn't really a limitationbecause you can simply include a call to a function or sub within a code modulefrom within your event handler, to pass program control to a code module--justlike 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 anyevents.

• To allow the client application to handle the event being fired, the object variablemust be declared using the WithEvents keyword.

• VB custom events don't return a value; however, you can use a ByRef argument toreturn 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 particularinstance of the class raising the event.

Creating a custom event

To raise an event from within an object module, you first of all must declare the event inthe declarations section of the object module that will raise the event. You do this withthe Event statement using the following syntax:

[Public] Event eventname [(arglist)]For example:

Public Event DetailsChanged(sField As String)In the appropriate place in your code, you need to fire the event using the RaiseEventstatement. For example:

RaiseEvent DetailsChanged("Employee Name")That is all you need to do within the object module. Simply declare an event using Event,and fire it using RaiseEvent.The client code is just as simple. You declare an object variable using the WithEventskeyword to alert VB that you wish to be informed when an event is fired in the object.For example:

Private WithEvents oEmployee As EmployeeThis declaration should be placed in the Declarations section of the module. VBautomatically places an entry for the object variable name in the Object drop-down list atthe top left of your code window. When you select this, note that the events declared inthe object are available to you in the Procedure drop-down list at the top right of yourcode window. You can then select the relevant event and its event handler. For example:

Private Sub oEmployee_DetailsChanged(sField As String) MsgBox sField & " has been changed"End Sub

In the earlier section "The Property Let procedure," we mentioned using a custom eventto fire a warning to the client as part of a data-validation procedure. Unfortunately,though, events don't return a value. However, if you define one of the parameters of your

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

Page 110: VB1

Helpmate

event to be ByRef, you can examine the value of the variable once the event has beenhandled to determine the outcome of the event handling within the client application.Here's a simple example:Server code:

Public Event Warning(sMsg As String, ByRef Cancel As Boolean) Public Property Let ClaimValue(dVal As Double)

Dim blnCancel As Boolean

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

mdClaimValue = dVal End Property

Client code:Private WithEvents oServer As clsServer Private Sub oServer_Warning(sMsg As String, _ Cancel As Boolean) Dim iResponse As Integer iResponse = MsgBox(sMsg & " is this OK?", _ vbQuestion + vbYesNo, _ "Warning") If iResponse = vbNo Then Cancel = True Else Cancel = False End If

End SubAs you can see, this is a powerful technology. However, it also demonstrates anotheraspect of custom events that may not be desirable in certain circumstances: RaiseEvent isnot asynchronous. In other words, when you call the RaiseEvent statement in your classcode, your class code won't continue executing until the event has been either handled bythe client or ignored. (If the client has not created an object reference using the WithEventskeyword, then it isn't handling the events raised by the class, and any events raised willbe ignored by that client.) This can have undesirable side effects, and you should bear itmind 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 ExamplesSo let's bring together all you've seen in this chapter with a few sample implementationsof OLE automation servers.

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

Page 111: VB1

Helpmate

Using Word as a Repor t Wr iter from VBThis first application demonstrates how you can seamlessly use Microsoft Word to printoutput from your VB program without the user knowing that you have actually usedMicrosoft Word:

Private Sub cmdWordDoc_Click() 'create an error handler On Error GoTo cmdWordDoc_Err

'create the local Early Bound object variables Dim oWord As Word.Application Dim oWordActiveDoc As Word.Document Dim oWordSel As Word.Selection

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

'Do some work with the Selection object oWordSel.TypeText "This is some text from the VB app." oWordSel.WholeStory oWordSel.Font.Name = "Arial" oWordSel.Font.Size = 12 oWordSel.Font.Bold = wdToggle

'Now print out the doc oWordActiveDoc.PrintOut

'always tidy up before you leave Set oWordSel = Nothing Set oWordActiveDoc = Nothing

Set oWord = Nothing

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

End SubBecause this example uses early binding, you'll have to use the References dialog to adda project reference to the Word 8 Object Model.

TIP: Note that this application appears seamless because the application'sVisible property is False by default. If you wanted to show the Wordapplication window in operation (which may be required whiledebugging), simply set the property to True.

Using Email Within VB

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

Page 112: VB1

Helpmate

This application demonstrates how you can work with a late bound object. The OLEserver in this instance is Windows MAPI. Using MAPI in this way uses Outlook sort ofthrough the back door; you don't actually create an instance of Outlook, but this sampledemonstrates how closely tied MAPI and Outlook are. In fact, the mail side of Outlookisn't much more than a nice GUI to the Windows MAPI. If you are connected to anExchange 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 alsohave 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 theindividual addresses of the recipients. This example is in fact part of a workingapplication used several times a day to send test messages to new email accounts:

Private Function SendReturnEMail() As Boolean ' create an error handlerOn Error GoTo SendReturnEMail_Err 'set the default return value SendReturnEMail = False

'we're using late binding for this app Dim objSession As Object Dim objMessage As Object Dim objRecipient As Object

'declare some other utility variables Dim i As Integer Dim sSubject As String Dim sText As String Dim sName As String

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

'start with the top of the mapi hierarchy -- 'the session object Set objSession = CreateObject("mapi.session") 'use the local Outlook default profile objSession.LogOn profilename:="Microsoft Outlook" 'this application will send a number of test messages 'to the members of a particular domain For i = 0 To lstEmails.ListCount - 1 'build the addresses from the names in the list 'and the given domain name sName = Trim(lstEmails.List(i)) & "@" & _ Trim(txtDomain.Text) 'now create a new message object Set objMessage = objSession.outbox.messages.Add

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

Page 113: VB1

Helpmate

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

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

End Function

Output from VB to ExcelTo finish with, here's an easy little application that places values from a VB applicationinto an Excel spreadsheet. There are project-level (early bound) references created toboth Excel and the ADODB 2.0 Reference Library. An ADO recordset has already beencreated and is passed as a parameter to the OutputToExcel function. The function createsan instance of a new Excel workbook and worksheet, then copies the values from theADO recordset into the worksheet. Excel's functionality is used to perform a simplecalculation on the data, the worksheet is saved, Excel is closed down, and all referencesare tidied up.This example illustrates the power of a glue language such as Visual Basic. Here VB isacting as the glue between ADO, which is an ActiveX server, and Excel--controlling bothto produce a simple yet patently powerful and seamless application:

Private Function OutputToExcel(oADORec As ADODB.Recordset) _ As Boolean On Error GoTo cmdExcel_Err

'set up the default return value OutputToExcel = False

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

Page 114: VB1

Helpmate

' Declare the Excel object variables Dim oXLApp As Excel.Application Dim oXLWBook As Excel.Workbook Dim oXLWSheet As Excel.Worksheet 'start at the top of the model Set oXLApp = New Excel.Application 'and work your way down Set oXLWBook = oXLApp.Workbooks.Add 'until you get to the worksheet Set oXLWSheet = oXLWBook.Worksheets.Add

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

' do some stuff in Excel with the values oXLWSheet.Cells(3, 1).Formula = "=R1C1 + R2C1"

' save your work oXLWSheet.SaveAs "vb2XL.xls"

'quit Excel oXLApp.Quit

' always remember to tidy up before you leave Set oXLWSheet = Nothing Set oXLWBook = Nothing Set oXLApp = Nothing

OutputToExcel = True Exit Function cmdExcel_Err: MsgBox Err.Description & vbCrLf & Err.Number & _ vbCrLf & Err.Source

End Function

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

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

Page 115: VB1

Helpmate

properties, which you would expect to set before beginning logging, are read-only andcan only be set by calling the StartLogging method.)

WARNING: Note that the log mode constants were missing from Version5 of VB, so you either have to enter their literal values, or you have todefine your own constants.

In Windows NT, if you call the StartLogging method but don't specify a log file, or inWindows 95, if you don't call the StartLogging method at all, VB creates a file calledvbevents.log, which is placed in the Windows directory. To use event logging, you don'tnecessarily need to use the StartLogging method.The LogEvent method itself takes two parameters. The first is a string containing all thedetail 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 displaysthe 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 sawhow to force MsgBox prompts to be automatically written to an event logby selecting the Unattended Application option. But which event log? TheMsgBox function doesn't take a parameter to specify an optional event log,so VB will write the string contained within the Prompt parameter to thedefault vbevents.log in Windows 9x or to the application event log inWindows NT. However, you can place a call to the app object'sStartLogging method in the class's Initialize event, thereby specifying alog file for all Msgbox and LogEvent calls.

Once you have an event log for your application, you can look back through the historyof the application any time you choose. If you are networked to the user's machine, youcan open the user's event log from your machine and detect problems without evenleaving your desk.

#Const Directive

Named ArgumentsNo

Syntax#Const constantname = expressionconstantnameUse: RequiredData Type: Variant (String)Name of the constant.expressionUse: RequiredData Type: LiteralAny combination of literal values, other conditional compilation constants definedwith the #Const directive, and arithmetic or logical operators except Is.

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

Page 116: VB1

Helpmate

Descr iptionDefines a conditional compiler constant. By using compiler constants to create codeblocks that are included in the compiled application only when a particular condition ismet, 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; conditionalcompiler constants that aren't explicitly defined by the #Const directive but that arereferenced 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 valuereturned 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 blockis compiled as part of the application only when the conditional compiler constantevaluates to True.You may wonder why you should bother having code that is compiled only when acertain condition is met, when a simple If...Then statement could do the same job. Thereasons are:

• You may have code that contains early bound references to objects that arepresent only in a particular version of the application. You'd want that codecompiled only when you know it wouldn't create an error.

• You may wish to include code that executes only during the debugging phase ofthe application. It's often wise to leave this code in the application even after theapplication has been released, so that you can check back over a procedure if anissue arises. However, you don't want the code to be executed in the finalapplication. The answer is to wrap your debugging code in a conditionalstatement. You can then provide a conditional constant that acts as a switch toturn debugging code on or off, as the example below demonstrates.

• Although most operations performed with conditional compilation can bereplicated with normal If...Then code blocks, conditional compilation reduces thesize of the compiled application and thereby the amount of memory required forthe application, making for a more efficient application.

Rules at a Glance

• Conditional compiler constants are evaluated by the conditional compiler #If...Thenstatement 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 theexpression.

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

Page 117: VB1

Helpmate

• According to the documentation, you can't use intrinsic functions in theexpression; e.g., #Const MY_CONST = Chr(13) is illegal. In most cases, VBAdisplays a "Compile error : Variable not found" message if you try this. But thereare numerous exceptions. For example, the use of the Int function in the followingcode fragment doesn't produce a compiler error, and in fact, successfully defines aconstant ccDefInt whose value is 3:

• #Const ccDefFloat = 3.1417• #Const ccDefInt = Int(ccDefFloat)

• When using #Const, you can't use variables to assign the conditional constant avalue.

• Constants defined with #Const can be used only in conditional code blocks.

• Constants defined with #Const have scope only within the module in which theyare 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 oncewithin a module. Attempting to do so produces a "Compile Error: DuplicateDefinition" error message.

• Interestingly, you can define the same constant both through the VB or VBAinterface (see the second item in the "Programming Tips & Gotchas" section) andusing the #Const directive. In this case, the constant defined through the interfaceis visible throughout the application, except in the routine in which the #Constdirective is used, where the private constant is visible.

• The #Const directive must be the first statement on a line of code. It can befollowed only by a comment. Note that the colon, which combines two completesets of statements onto a single line, can't be used on lines that contain #Const.

Example#Const ccDebug = 1 'evaluates to true Function testValue(sValue as String)

sValue = UCase(sValue)testValue = sValue #If ccDebug Then 'this code only executes if ccDebug evaluates to true Debug.Print sValue#End If End Function

Programming Tips & Gotchas

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

Page 118: VB1

Helpmate

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

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

Constants Defined Through the VB/VBA Inter faceThe rules for defining constants in the Conditional Compilation Arguments text box aresomewhat different than for constants defined in code using the #Const statement. Thevalue assigned through the VB/VBA interface must be an integer literal; it can't be anexpression formed by using multiple literals or conditional constants, along with one ormore operators, nor can it be a Boolean value (i.e., True or False). If multiple conditionalconstants are assigned through the user interface, they are separated from one another by acolon. For instance, the following fragment defines three constants, ccFlag1, ccFlag2, andccFlag3:

ccFlag1 = 1 : ccFlag2 = 0 : ccFlag3 = 1

• In many cases, failing to properly define a constant doesn't produce an errormessage. When this happens (as it does, for instance, when you attempt to assigna variable's value to a constant), the default value of the constant is False. As aresult, attempting to assign the value resulting from an invalid expression to aconstant can lead to the inclusion of the wrong block of code in the compiledapplication.

• Although it may be obvious, it's important to remember that the constant definedby #Const is evaluated at compile time, and therefore doesn't return informationabout the system on which the application is running. For example, the intent ofthe following code fragment is to test for a sound card and, if one is present, toinclude code taking advantage of the system's enhanced sound capabilities:

If waveOutGetNumDevs > 0 Then #Const ccSoundEnabled = TrueEndif...#If ccSoundEnabled Then ' Include code for sound-enabled systems

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

Page 119: VB1

Helpmate

#Else ' Include code for systems without a sound card#End If

However, the code doesn't work as expected, since it includes or excludes the codesupporting a sound card based on the state of the development machine, rather than themachine on which the application is running.

See Also#If...Then...#Else Directive

AddressOf Operator

Named ArgumentsNo

SyntaxAddressOf procedurenameprocedurenameUse: RequiredThe name of an API procedure.

Descr iptionPasses the address of a procedure to an API function. There are some API functions thatrequire the address of a callback function as a parameter. (A callback function is a routinein your code that is invoked by the routine that your program is calling: it calls back intoyour code.) These callback functions are passed to the API function as pointers to amemory address. In the past, calling functions that required callbacks posed a uniqueproblem to VB, since, unlike C or C++, it lacks a concept of pointers. However, theAddressOf operator allows you to pass such a pointer in the form of a long integer to theAPI 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 aclass or a form module generates a compile-time error, "Invalid use of AddressOfoperator."

• 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 Declarestatement must be As Any or As Long.

• The AddressOf operator can't call one VB procedure from another.

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

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

Page 120: VB1

Helpmate

as its first parameter. A custom function, EnumCallBackProc, is the callback functionthat populates the lstWindowTitles list box.When the cmdListWindows command button is clicked, the list box is cleared, and a callto the EnumWindows API function is made, passing the AddressOf the EnumCallBackProcfunction and a reference to the list box control. EnumWindows then calls back toEnumCallBackProc, passing it the window handle of an open window and the referenceto the list box. EnumCallBackProc then uses the GetWindowText API function to returnthe text in the titlebar of the window, passing it the window handle, a string buffer, andthe length of that buffer. EnumCallBackProc is called by the API function as many timesas is required, depending upon the number of open windows. The first portion of theexample code must be stored in a code module, while the cmdListWindows_Click eventhandler can be stored in the form module containing the cmdListWindows button.

Option Explicit Public Declare Function EnumWindows Lib "User32" _ (ByVal lpEnumFunc As Any, _ ByVal lParam As Any) As Long Public Declare Function GetWindowText Lib "User32" _ Alias "GetWindowTextA" _ (ByVal hWnd As Long, _ ByVal lpString As String, _ ByVal cch As Long) As Long Function EnumCallBackProc(ByVal hWnd As Long, _ ByVal lParam As ListBox) As Long On Error Resume Next Dim sWindowTitle As String Dim lReturn As Long sWindowTitle = String(512, 0) lReturn = GetWindowText(hWnd, sWindowTitle, 512) If lReturn > 0 Then lParam.AddItem sWindowTitle End If EnumCallBackProc = 1 End Function Private Sub cmdListWindows_Click() Dim lReturn As Long

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

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

Page 121: VB1

Helpmate

Programming Tips & Gotchas

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

• It's possible to pass an AddressOf pointer from one VB procedure to another bycreating a wrapper for the callback function. To do this, however, you mustdeclare the pointer as either Long or Any. The following snippet shows how youcould add such a wrapper function to the example used above:

Private Sub cmdListWindows_Click() Dim lReturn As Long lReturn = DoWindowTitles(AddressOf EnumCallBackProc, _ lstWindowTitles)End Sub Private Function DoWindowTitles(CallBackAddr As Long, _ lstBox As ListBox) As Long 'other stuff here lstBox.Clear DoWindowTitles = EnumWindows(CallBackAddr, lstBox)

End Function

• Because you can't pass an error back to the calling Windows API function fromwithin your VB callback function, you should use the On Error Resume Nextstatement at the start of your VB callback function.

See AlsoDeclare Statement

CallByName Function (VB6)

Named ArgumentsNo

SyntaxCallByName(object, procedurename, calltype, _[argument1,..., argumentn])objectUse: RequiredData Type: ObjectA reference to the object containing the procedure being called.procedurenameUse: RequiredData Type: StringThe name of the procedure to call.calltype

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

Page 122: VB1

Helpmate

Use: RequiredData Type: vbCallType constantA constant that indicates the type of procedure being called. vbCallType constantsare listed in the next table.argumentsUse: OptionalData Type: VariantAny number of variant arguments, depending on the argument list of theprocedure to call.

Constant Value Description

vbGet 2 The called procedure is a Property Get

vbLet 4 The called procedure is a Property Let

vbMethod 1The called procedure is a method; this can be a Sub or aFunction within object

vbSet 8 The called procedure is a Property Set

Return ValueDepends on the return value (if any) of the called procedure.

Descr iptionProvides a flexible method for calling a public procedure in a VB object module. Sinceprocedurename is a string expression, rather than the hard-coded name of a routine, it'spossible to call routines dynamically at runtime with a minimum of coding.

Rules at a Glance

• The return type of CallByName is the return type of the called procedure.

• procedurename isn't case sensitive.

Programming Tips & Gotchas

• At last, VB allows you to create a call to a procedure using a string. This meansthat the call can be flexible at runtime.

• The only drawback to the current implementation of CallByName is that theparameters to pass to the called function must be entered individually. This meansthat, when coding the CallByName function, you need to know in advance howmany parameters are needed. You could work around this by coding yourfunctions to accept only Variant arrays so that you only need to pass a singleparameter.

• Late binding is necessarily used to instantiate objects whose procedures areinvoked by the CallByName function. Consequently, the performance ofCallByName is inferior to that of method invocations in early bound objects. This

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

Page 123: VB1

Helpmate

degradation of performance is especially acute if CallByName is invokedrepeatedly inside a looping structure.

ExampleThe following example takes CallByName and the amendments to CreateObject to theirlogical conclusion: a variable procedure call to a variable ActiveX server in a variablelocation. 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 differentrecordsets: one from the Authors table, the other from the Titles table. However, nowherein the program are the names of the ActiveX DLLs, the procedures, or the remote serversmentioned.The middle tier of this application uses the registry to store these names, allowing fastalteration of the application without touching a single line of code or creatingincompatibilities 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 therequired datasets and fields specified. The Form_Load event calls a standard function topopulate combo box controls with the required data:

Private Sub Form_Load()

PopulateCombo cboAuthors, "Authors", "au_lname" PopulateCombo cboTitles, "Titles", "title"

End SubThe PopulateCombo function calls a GetRecordset function in the first middle tier of themodel, passing in the recordset name required (either Authors or Titles in this case) and asearch criteria string that is concatenated into the embedded SQL script to refine therecordset. GetRecordset returns an ADO recordset that populates the desired combo box:

Private Function PopulateCombo(oCombo As ComboBox, _ sRecords As String, _ sField As String) As Boolean Dim adorRecords As ADODB.Recordset Dim sSearch As String

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

Set adorRecords = oAdmin.GetRecordset(sRecords, sSearch)

Do While Not adorRecords.EOF oCombo.AddItem adorRecords(sField) adorRecords.MoveNext Loop

adorRecords.Close Set adorRecords = Nothing

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

Page 124: VB1

Helpmate

End FunctionThe GetRecordset method that sits on a central machine interrogates the registry (usingthe GetSetting function) to determine the names of the ActiveX server, the machine, andthe procedure to call. I've also coded an alternative method of obtaining these namesusing a Select Case statement (which is commented out in the code sample). Finally, theCreateObject function obtains a reference to the appropriate ActiveX server on theappropriate machine and a call is made to the function in that server to obtain the correctrecordset:

Public Function GetRecordset(sRecords As String, _ sCriteria As String _ ) As ADODB.Recordset

Dim sServer As String Dim sLocation As String Dim sMethod As String

Dim oServer As Object

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

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

Set oServer = CreateObject(sServer, sLocation)

Set GetRecordset = CallByName(oServer, _ sMethod, _ VbMethod, _ sCriteria) End Function

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

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

Page 125: VB1

Helpmate

server. Then with two quick edits of the registry, all the clients in the organization wouldinstantly access the new procedure with a minimum of fuss, no loss of componentcompatibility, zero downtime, and an almost seamless transition.

See AlsoCall Statement

Declare Statement

Named ArgumentsNo

SyntaxSyntax 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]PublicUse: OptionalKeyword used to declare a procedure that has scope in all procedures in allmodules in the application.PrivateUse: OptionalKeyword used to declare a procedure that has scope only within the module inwhich it's declared.SubUse: OptionalKeyword indicating that the procedure doesn't return a value. Mutually exclusivewith Function.FunctionUse: OptionalIndicates that the procedure returns a value. Mutually exclusive with Sub.nameUse: RequiredData Type: StringAny valid procedure name within the DLL or code library. If the aliasnameargument is used, name represents the name the function or procedure is called inyour code, while aliasname represents the name of the routine as found in theexternal library.LibUse: RequiredKeyword indicating that the procedure is contained within a DLL or other codelibrary.libnameUse: RequiredData Type: String

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

Page 126: VB1

Helpmate

The name of the DLL or other code library that contains the declared procedure.AliasUse: OptionalKeyword whose presence indicates that name is different from the procedure'sreal name within the DLL or other code library.aliasnameUse: OptionalData Type: StringThe real name of the procedure within the DLL or code library.arglistUse: OptionalData Type: AnyA list of variables representing the arguments that are passed to the procedurewhen it's called. (For details of the arglist syntax and elements, see the entries forthe Sub statement or Function statement.)typeUse: OptionalData type of the value returned by a function. (For further details see the Functionstatement entry.)

Descr iptionUsed at module level to declare references to external procedures in a dynamic-linklibrary (DLL).

Rules at a Glance

• You can place a Declare statement within a code module, in which case it can bepublic or private, or within the declarations section of a form or class module, inwhich case it must be private.

• Leaving the parentheses empty and not supplying an arglist indicates that the Subor Function procedure has no arguments.

• The number and type of arguments included in arglist are checked each time theprocedure is called.

• The data type you use in the As clause following arglist must match that returnedby the function.

ExampleOption Explicit Declare Function GetVersion Lib "kernel32"() As Long Public Function WhereAmI() As Boolean Dim lWinVersion As Long Dim lWinMajVer As Long Dim lWinMinVer As Long Dim sSys As String

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

Page 127: VB1

Helpmate

lWinVersion = GetVersion()

lWinMajVer = lWinVersion And 255 lWinMinVer = (lWinVersion And 65280) / 256 If lWinVersion And &H80000000 Then sSys = "Windows 95" Else sSys = "Windows NT" End If Msgbox "Platform: " & sSys & vbCrLf & _ "Version: " & lWinMajVer & "." & lWinMinVer

Programming Tips & Gotchas

• If you don't specify a Public or Private keyword, the visibility of the externalprocedure is public by default. However, if the routine is declared in thedeclarations section of a form or a class module, a compiler error ("Constants,fixed length strings, arrays, and Declare statements not allowed as Publicmembers of object modules") results.

• Using an alias is useful when the name of an external procedure would conflictwith a Visual Basic keyword or with the name of a procedure within your project,or when the name of the procedure in the code library isn't allowed by the VisualBasic DLL naming convention. In addition, aliasname is frequently used in thecase of functions in the Win32 API that have string parameters, where the"official" documented name of the function is used in code to call either of two"real" functions, one an ANSI and the other a Unicode version. For example:

• Declare Function ExpandEnvironmentStrings _• Lib "kernel32" Alias "ExpandEnvironmentStringsA" _• (ByVal lpSrc As String, ByVal lpDst As String, _

ByVal nSize As Long) As Longdefines the documented Win32 function ExpandEnvironmentStrings to a VBapplication. However, although calls to the function take the form:

lngBytes = ExpandEnvironmentStrings(strOriginal, _ strCopy, len(strCopy)

the actual name of the function as it exists in Kernel32.dll isExpandEnvironmentStringsA. (Windows API functions ending in A are the ANSIstring versions, and those ending in W (for W ide) are the Unicode stringversions.)

• You can use the # symbol at the beginning of aliasname to denote that aliasnameis in fact the ordinal number of a procedure within the DLL or code library. In thiscase, all characters following the # sign that compose the aliasname argumentmust be numeric. For example:

• Declare Function GetForegroundWindow Lib "user32" _ Alias "#237" () As Long

• Remember that DLL entry points are case sensitive. In other words, either nameor, if it's present and doesn't represent a routine's ordinal position, aliasname must

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

Page 128: VB1

Helpmate

correspond in case exactly to the routine as it's defined in the external DLL.Otherwise, VB displays runtime error 453, "Specified DLL function not found." Ifyou aren't sure how the routine name appears in the DLL, use QuickView tobrowse the DLL and scan for its export table.

• libname can include an optional path that identifies precisely where the externallibrary is located. If the path isn't included along with the library name, VB bydefault searches the current directory, the Windows directory, the Windowssystem directory, and the directories in the path, in that order.

• If the external library is one of the major Windows system DLLs (likeKernel32.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 severaldata types as arguments. This is particularly common when a function accepts apointer to a string buffer if an argument is to be supplied and a null pointer if itdoesn't; the former is expressed in Visual Basic by a string argument and the latterby a 0 passed to the function by value. It's also the case whenever an API functiondesignates a parameter's data type as LPVOID, which indicates a pointer to anydata type. To handle this, you can define separate versions of the DECLAREstatement, one for each data type to be passed to the function. (In this case, namedesignates the name by which a particular API function is referenced in yourprogram, while the ALIAS clause designates the name of the routine as it exists inthe DLL.) A second alternative, rather than having to "strongly type" a parameterin arglist, is to designate its data type as As Any, indicating that the routine acceptsan argument of any data type. While this provides you with a flexible way ofpartly overcoming the mismatch between VB and C data types, you should use itwith caution, since it suspends Visual Basic's normal type checking for thatargument.

• 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 stringsexclusively. 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 requirethem to be passed as Unicode strings. Unfortunately, although Visual Basic usesUnicode strings internally, it converts strings passed to these DLLs back intoANSI. The remedy is to use a dynamic array of type Byte. Passing and receivingarrays of bytes circumvents Visual Basic's Unicode-ANSI conversion.

To pass a string to a Unicode API function, declare a dynamic byte array, assignyour 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 thefunction, as the following simple snippet shows:

Dim bArray() As BytebArray() = "My String" & vbNullCharsomeApiCall(bArray(0))

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

Page 129: VB1

Helpmate

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

See AlsoSub Statement, Function Statement, StrConv Function

DoEvents Function

Named ArgumentsNo

SyntaxDoEvents()

Return ValueIn VBA, DoEvents returns 0; in the retail version of VB, it returns the number of openforms.

Descr iptionAllows the operating system to process events and messages waiting in the messagequeue. 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 eventwouldn't be processed until after the operation had completed; with DoEvents, the Cancelbutton's Click event can be fired and its event handler executed even though theprocessor-intensive operation is still executing.

Rules at a GlanceControl is returned automatically to your program or the procedure that called DoEventsonce the operating system has processed the message queue.

ExampleThe following example uses a UserForm with two command buttons to illustrate howDoEvents interrupts a running process:

Option ExplicitPrivate lngCtr As LongPrivate blnFlag As Boolean Private Sub CommandButton1_Click() blnFlag = True Do While blnFlag lngCtr = lngCtr + 1 DoEvents Loop MsgBox "Loop interrupted after " & lngCtr & _ " iterations."

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

Page 130: VB1

Helpmate

End Sub Private Sub CommandButton2_Click() blnFlag = False End Sub

Programming Tips & Gotchas

• You may consider using the retail version of VB to create standalone ActiveXEXEs that handle very intensive or long processes. These can then be called fromyour VBA code. This allows you to pass the responsibility of time slicing andmultitasking to the operating system.

• Make sure that during the time you have passed control to the operating systemwith DoEvents, the procedure calling DoEvents isn't called from another part ofthe application or from another application, since the return from DoEvents maybe compromised. For the same reason, you must not use the DoEvents functionwithin VB in-process ActiveX DLLs.

• While DoEvents can be essential for increasing the responsiveness of yourprogram, it should at the same time be used judiciously, since it entails anenormous performance penalty. For example, the following table compares thenumber of seconds required for a simple For...Next loop to iterate one million timeswhen DoEvents isn't called, on the one hand, and when it's called on each iterationof the loop, on the other:

without DoEvents 0.3 seconds

with DoEvents 49.8 seconds

• If most of a procedure's processing occurs inside a loop, one way of avoiding far-too-frequent calls to DoEvents is to call it conditionally every hundred orthousand 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

• Descr iption• A read-only property containing a long data type representing a system error

produced within a DLL called from within a VB program.

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

Page 131: VB1

Helpmate

• Rules at a Glance

o Only direct calls to a Windows system DLL from VB code assign a valueto LastDLLError.

o The value of the LastDLLError property depends upon the particular DLLbeing called. Your code must be able to handle the various codes that canbe returned by the DLL you are calling.

o Don't forget that a failed DLL call doesn't itself raise an error within yourVB program. As a result, the Err object's Number, Description, and Sourceproperties aren't filled.

Programming Tips & Gotchas

o The LastDLLError property is used only by system DLLs, such askernel32.dll, and therefore errors that occur within DLLs you may havecreated in VB won't be assigned.

o Obtaining accurate documentation about the return values of system DLLscan be a challenging experience. Most useful information can be found bystudying the API documentation for Visual C++. However, you can usethe Windows API FormatMessage to return the actual Windows errormessage string from within Kernel32.DLL, which incidentally is also inthe correct language. The following is a brief example you can use in yourapplications to display the actual Windows error description:

Option ExplicitDeclare Function FormatMessage Lib "kernel32" _ Alias "FormatMessageA" _ (ByVal dwFlags As Long, lpSource As Any, _ ByVal dwMessageId As Long, _ ByVal dwLanguageId As Long, _ ByVal lpBuffer As String, ByVal nSize As Long, _ Arguments As Long) As LongPublic Const FORMAT_MESSAGE_FROM_SYSTEM = &H1000Public Const FORMAT_MESSAGE_IGNORE_INSERTS = &H200 Function apiErrDesc(lErrCode As Long) As String Dim sErrDesc As String Dim lReturnLen As Long Dim lpNotUsed As Long

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

If lReturnLen > 0 Then apiErrDesc = Left$(sErrDesc, lReturnLen) End If

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

Page 132: VB1

Helpmate

End FunctionHere's a snippet demonstrating how you can use this utilityfunction:lReturn = SomeAPICall(someparams)If lReturn <> 0 then Err.Raise Err.LastDLLError & vbObjectError, _ "MyApp:Kernel32.DLL", _ apiErrDesc(Err.LastDLLError)End IfNote that some API calls return 0 to denote a successful functioncall; others return 0 to denote an unsuccessful call. You should alsonote that some API functions don't appear to set the LastDLLErrorproperty. In most cases, these are functions that return an errorcode. You could therefore modify the snippet above to handlethese 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 IfEnd If

See AlsoErr Object, Chapter 6

Filter Function (VB6)

Named ArgumentsNo

SyntaxFilter(SourceArray, FilterString[, Switch[, Compare]])

SourceArrayUse: RequiredData Type: String or VariantAn array containing values to be filtered.FilterStringUse: RequiredData Type: StringThe string of characters to find in the source array.SwitchUse: OptionalData Type: Boolean

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

Page 133: VB1

Helpmate

A Boolean (True or False) value. If True, the default value, Filter includes allmatching values in result; if False, Filter excludes all matching values (or, to put itanother way, includes all nonmatching values).CompareUse: OptionalType: Constant of vbCompareMethod EnumerationAn optional constant (possible values are vbBinaryCompare, vbTextCompare,vbDatabaseCompare) that indicates the type of string comparison to use. The defaultvalue is vbBinaryCompare.

Return ValueA String array of the elements filtered from SourceArray.

Descr iptionProduces an array of matching values from an array of source values that eithermatch or don't match a given filter string. In other words, individual elements arecopied from a source array to a target array if they either match or don't match afilter 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 andcase. In contrast, vbTextCompare is case insensitive, matching only characterregardless of case.

o The returned array is always base 0, regardless of any Option Base setting.

Programming Tips & Gotchas

o The Filter function ignores zero-length strings ("") if SourceArray is astring array and ignores empty elements if SourceArray is a variant array.

o The array you declare to assign the return value of Filter must be adynamic, single-dimension String array or a variant.

o Although the Filter function is primarily a string function, you can alsofilter numeric values. To do this, specify a SourceArray of type Variantand populate this array with numeric values. Although FilterStringappears to be declared internally as a string parameter, a String, Variant,Long, or Integer can be passed to the function. Note, though, that thereturned string contains string representations of the filtered numbers. Forexample:

o Dim varSource As Variant, varResult As Varianto Dim strMatch As Stringo o strMatch = CStr(2)

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

Page 134: VB1

Helpmate

o varSource = Array(10, 20, 30, 21, 22, 32)o varResult = Filter(varSource, strMatch, True, _

vbBinaryCompare)In this case, the resulting array contains four elements: 20, 21, 22, and 32.

o The Filter function is an ideal companion to the Dictionary object. TheDictionary object is a collection-like array of values, each of which isstored with a unique string key. The Keys method of the Dictionary objectallows you to produce an array of these Key values, which you can thenpass into the Filter function as a rapid method of filtering the members ofyour Dictionary, as the following example demonstrates.

ExampleDim sKeys() As StringDim sFiltered() As StringDim sMatch As StringDim blnSwitch As BooleanDim oDict As Dictionary Set oDict = New Dictionary oDict.Add "One Microsoft Way", "Microsoft"oDict.Add "31 Harbour Drive", "AnyMicro Inc"oDict.Add "The Plaza", "Landbor Data"oDict.Add "999 Pleasant View", "Micron Co." sKeys = oDict.KeyssMatch = "micro"blnSwitch = True'find all keys that contain the string "micro" - any casesFiltered() = Filter(sKeys, sMatch, blnSwitch, _ vbTextCompare)'now iterate through the resulting arrayFor i = 0 To UBound(sFiltered) Set oSupplier = oDict.Item(sFiltered(i)) With oSupplier Debug.Print oSupplier.Address1 End With Set oSupplier = NothingNext i

GetObject Function

Named ArgumentsYes

SyntaxGetObject([pathname] [, class])pathnameUse: OptionalData Type: Variant (String)The full path and name of the file containing the ActiveX object.

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

Page 135: VB1

Helpmate

classUse: OptionalData Type: Variant (String)The class of the object (see next list).The class argument has these parts:AppnameUse: RequiredData Type: Variant (String)The name of the application.ObjecttypeUse: RequiredData Type: Variant (String)The class of object to create, delimited from Appname by using a point (.). Forexample, Appname.Objecttype.

Return ValueReturns a reference to an ActiveX object.

Descr iptionAccesses an ActiveX server held within a specified file.

Rules at a Glance

o Although both pathname and class are optional, at least one parametermust be supplied.

o In situations in which you can't create a project-level reference to anActiveX object, you can use the GetObject function to assign an objectreference from an external ActiveX object to an object variable.

o GetObject is used when there is a current instance of the ActiveX object;to create the instance, use the CreateObject function.

o If you specify pathname as a zero-length string, GetObject returns a newinstance of the object--unless the object is registered as single instance, inwhich case the current instance is returned.

o If you omit the pathname, the current instance of the object is returned.

o An error is generated if pathname isn't specified, and no current instanceof the object can be found.

o The object variable you use within your program to hold a reference to theActiveX object is dimensioned as type Object. This causes the object to belate bound; that is, your program knows nothing of the type of object norits interface until the object has been instantiated within your program. Toassign the reference returned by GetObject to your object variable, youmust use the Set statement:

o Dim myObject As Object

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

Page 136: VB1

Helpmate

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

o The details of how you create different objects and classes are determinedby how the server has been written; you need to read the documentationfor the server to determine what you need to do to reference a particularpart of the object. There are three ways you can access an ActiveX object:

The overall object library. This is the highest level, and it givesyou access to all public sections of the library and all its publicclasses:

GetObject("C:\OtherApp\Library.lib")

A section of the object library. To access a particular section of thelibrary, use an exclamation mark (!) after the filename, followed bythe name of the section:

GetObject("C:\OtherApp\Library.lib!Section")

A class within the object library. To access a class within thelibrary, use the optional Class parameter:

GetObject("C:\OtherApp\Library.lib", "App.Class")

Programming Tips & Gotchas

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

o You can't use GetObject to obtain a reference to a class created withVisual Basic.

o When possible, you should use early binding in your code. For moredetails on early and late binding, see Chapter 4. You can use GetObject inearly binding, as in:

o Dim objExcel As Excel.ApplicationSet objExcel = GetObject(, "Excel.Application")The following table shows when to use GetObject and CreateObject :

Task Use

Create a new instance of an OLE server CreateObject

Create a subsequent instance of an already instantiatedserver (if the server isn't registered as single instance)

CreateObject

Obtain a further reference to an already instantiated server GetObject

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

Page 137: VB1

Helpmate

without launching a subsequent instance

Launch an OLE server application and load an instance ofa subobject

GetObject

Instantiate a class created with VB CreateObject

Instantiate a class registered on a remote machine CreateObject

See AlsoCreateObject Function, Set Statement

WithEvents Keyword

Named ArgumentsNo

SyntaxDim|Private|Public WithEvents objVarname As objectTypeobjVarNameUse: RequiredData Type: StringThe name of any object variable that refers to an object that exposes events.objectTypeUse: RequiredData Type: Any object type other than the generic ObjectThe ProgID of a referenced object.

Descr iptionThe WithEvents keyword informs VB that the object being referenced exposesevents. When you declare an object variable using WithEvents, an entry for theobject variable is placed in the code window's drop-down Object List, and a list ofthe 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 objectvariable in the same way that you write other more common event handlers suchas Form_Load.

Rules at a Glance

o An object variable declaration using the WithEvents keyword can be usedonly in an object module such as a Form or Class module.

o An object variable declaration using the WithEvents keyword should beplaced only in the Declarations section of the object module.

o Any ActiveX object or class module that exposes events can be used withthe WithEvents keyword. WithEvents is valid only when used to declare anobject variable.

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

Page 138: VB1

Helpmate

o You can't use WithEvents when declaring the generic Object type.

o Unlike other variable declarations, the As keyword is mandatory.

o There is no limit to the number of object variables that can refer to thesame object using the WithEvents keyword; they all respond to that object'sevents.

o You can't create an array variable that uses the WithEvents keyword.

ExampleThe following example demonstrates how to trap and respond to the events withinan ADO recordset. An object variable is declared using the WithEvents keyword inthe 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 thatshows progress in populating the recordset.)

Private WithEvents oADo As ADODB.Recordset Private Sub oADo_FetchProgress(ByVal Progress As Long, _ ByVal MaxProgress As Long, _ adStatus As ADODB.EventStatusEnum, _ ByVal pRecordset As ADODB.Recordset)

ProgressBar1.Max = MaxProgress ProgressBar1.Value = Progress

End Sub

Programming Tips & Gotchas

o Placing the object variable declaration that uses the WithEvents keyword ina procedure doesn't add the object variable name to the module's ObjectList. In other words, the events fired from the object would have scopeonly in the procedure and therefore can't be handled.

o Even if you declare the object variable using the Public keyword, the eventsfired by the object have scope only in the module in which the objectvariable has been declared.

o Because you can't use WithEvents to declare a generic Object type,WithEvents can be used only with early bound object references. In otherwords, objects must have been added to the project using the Referencesdialog. Without this prior knowledge of the object's interface, VB has nochance of knowing how to handle events from the object.

o If the object you are referencing doesn't expose any public events, you willgenerate 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'treally a limitation, because to pass program control to a code module, you

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

Page 139: VB1

Helpmate

can simply call one of its functions or procedures from your event handler,just as you would from a form or control's event handler.

o For information about generating your own custom events in form andclass modules, see the "Implementing Custom Events" section in Chapter4.

These are the questions I most want answered from Developer.com, my emails, and othersources. Some I don't have time to dig into deeply enough to give a good answer. Some Ihave 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, zipup an example program and send that as a binary attachment (no uuencodings please).

Click the items in the list to see peoples' solutions.

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

2. How can I create a floating toolbar?

3. How can I access the History list of an embedded browser?

4. How can I load fonts in VB? (By this I mean install a new font on the system, notjust use different fonts)

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

6. How can I pass a Form object from an In-Process DLL back to the mainApplication?

7. How can I open a password protected database?

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

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

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

11. How can I change the color of a title bar?

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

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

14. How can I empty the wastebasket?

15. How can I delete temporary internet files?

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

If you know how to accomplish these tasks, please email me! Better still, if you can, zipup an example program and send that as a binary attachment (no uuencodings please).ANSWERS

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

Page 140: VB1

Helpmate

1. How can I retr ieve a metafile from a resource fi le?David OteroYou have to add the metafile as a custom resource, then when you need the file, u have todo something like this: Dim bData() As Byte

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

2. How can I create a floating toolbar?Robert TerblancheMy solution is quite simple. In fact, it's so simple I'm wondering if I haven't missedsomething or maybe misunderstood your question.All I did was to add a form to my application, and set the BorderStyle to 4 - FixedToolWindow. On this form I created the toolbar that I would normally have created onmy main application form. When I show the form, I set the owner to the main form bysaying: frmToolbar.Show, MeThat's it. Nothing to it. The Toolbar will now always be on top of my application, but noton top of other applications. The toolbar I used is just a simple example, using an alignedpicturebox with command buttons on it, but you can of course place any toolbar in thetoolbar form that you wish. You can of also totally remove the toolbar form's title bar,and add all sorts of fancy rearrangement of buttons when the toolbar form resizes.

An elegant solution. Note that for MDI applications, the toolbox must be a child of theMDI 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 youcould put inside Robert's toolbox form.

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

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

My company is producing software for CAD-users, so we do most of our programmingin C++ but we intend to create the UI in VB. I therefore face the challenge of linking VBand C++ programs. As long as this only consists in putting a control that is written in the

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

Page 141: VB1

Helpmate

"foreign" language this is (often) painless. But as soon as you have to do something morecomplicated or there are problems it seems to be rather difficult to get answers toquestions 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++ resourceeditor. There is one caveat though, the containers VB offers (Forms, picturecontrols and the like) offer a richer "environment" in which the control lives andwith which it can communicate. In VB parlance this environment is calledExtender and Ambient properties. They do not exist for non VB containers oroffer 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 glueas soon as you create a member variable for the control the first time. If youchange 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 hasbecome out of date. I have yet to find a way how to remedy this situation withoutscanning the MFC created files and deleting all references to the control.

I created a mini MFC application "RRMfcControll" that hosts a simple VBcreated 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 controlsby the way) MFC offers the #import compiler directive. Using it you get allneeded interface glue created and wrapped into a set of smart pointers which arevery 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 andthoroughly as you like. The glue will always be up to date since it is recreatedwhen ever MFC detects an change in the file dates. However, there is a down sidetoo. You do not get a interface map created, so there is no "automatic"communication with your server. You have to do it yourself. As mentionedbefore, this is a trivial task as long as the communication is VC++ -> VB. Viceversa matters are a bit more complicated. Raising an event in VB that is servicedby 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 acontrol on a resource by creating an message map and an event sink map. Boththese maps are actually wrappers around functions that subclass the windowsinvolved. Since nobody creates them for us we have to do it for ourselves. Luckilysubclassing comes naturally to C++ and is easy to implement.

I have included two samples demonstrating the communication of C++ with anActiveX server. RRConBarBone is a minimal console application calling an

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

Page 142: VB1

Helpmate

ActiveX server RRMfcTwoWays demonstrates the use of a "hand made" messagemap to build up a two way communication.

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

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

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

Dim Dbpath as String Dim DB as database DBPassword as String

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

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

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

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

Page 143: VB1

Helpmate

If I use a clientside cursor, with batchoptimistic locking and batchupdate I can getreliable 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 valuesmay 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 PersistLayeras an activex server running on the same machine as the MS Access .mdb. So I don'treally see a reason to have to use clientside cursors. Also I am only dealing with onerecord in this example so the Batch part does not make since either. But hey at this pointif it works I'll use it.What I am concluding is that with ADO using Jolt 3.51 you cannot get reliable (sameerrors, same place) with serverside cursors. A friend has also demonstrated this techniqueworks with SQL Server 7.0. He has not gone to the trouble of seeing if the situations thatdon't seem to work (that I think should using serverside cursors) will work with SQLServer 7.0.

10. How can I list the programs in the task bar?Click here to download an example program. It seems mostly correct, though in myinitial tests it missed a couple tasks.

12. How can I make a gradient color title bar?Subclass and use an owner draw window. Click here to download an example program.

13. How can I install multiple programs in one setup program? When you are using the auto setup maker in VB, near the end of the process before itstarts compiling (the last place where you can check things on and off), you can click onan "add" button, this will compress any other program you add to it. The only drawbackis that when it is installed the programs you added manually will be in the same directoryas the main program.

The source code for the setup program should be in your VB directory. On my computerit 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 installseven and a half applications, run dos programs, draw fractals and beep incessantlythrough the PC speaker. ...If that is what you want.So you have full control over the setup process.Steve SquiresI made a multiple application setup file by writing down each project's references afterrunning the setup wizard. After finishing the list, I made a setup for one of the programsand 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 itworks for me.

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

Page 144: VB1

Helpmate

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

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

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

On Error Resume Next SHEmptyRecycleBin hwndParent, RootPath, nOptions End Sub

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

When you make use of the Printer object, VB will control said printer through theinstalled printer driver. This leads to the attempted translation of data that does not needtranslating, 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 yourpreformatted file to the printer "file". VB thinks it is a file and does not do anytranslation. If the printer is connected to your LPT1 port, you can open the port itself as afile, e.g. Open MyFile For Input As #1 Open "LPT1" For Output As #2

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

Page 145: VB1

Helpmate

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

Print #2, TempString;

CloseSome 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 aremote 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 #2Here the printer named Digital is being served by the computer Beauty. Presumably thiswill 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 touse 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. Forexample: Open "LPT2:" For Output As #1 Print #1, "Hello, world!" Close #1Use caution when working with LPT1 if using Windows NT since LPT1 is a physicalport; that is "actual hardware," which NT protects with absolute security permissions. Insome, if not all, versions of NT, a failed attempt at directly accessing LPT1 results in ahardware lockout which renders LPT1 unavailable from that point on, unless and untilyou reboot the OS.

This used to be a similar problem when I was trying to print preformatted files to aprinter over a Novell network. The solution was to copy the file to the relevant capturedport using the DOS binary switch 'COPY /b'.In VB, the 'Filecopy sourcefile, destfile' method does the same. The reason RobertTerblanche is having problems is that the Input# and Print# methods try to interpret thedata 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 SubWhere "c:\tims.prn" is the preformatted file and "\\Server\HP" is our networked HPprinter.NOTE: The files that are preformatted in this way are usually produced by a softwarepackage that has a default printer selected, then the file is printed with the user selecting'Print to File' option. The .PRN file produced will then contain all the formatting codes

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

Page 146: VB1

Helpmate

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

I had this problem on a network. Admin had disabled MS-DOS commands and VB exe's.We found that notepad can print with the /p flag on the command line. These executenotepad/write(wordpad) and print the file you provide. notepad.exe /p filepath write.exe /p filepathThis 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. Ihave not tried FilePrint (/mFilePrint). I had to do this when using oracle forms 4.5 (areally 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, whichsimply spools characters to the printer.For various plain-text files, PSET decides the best fit to the paper you use, and optimizesthe printing as much as possible to fit.Note: These are DOS programs called from a SHELL command. Of course, you canimport the code into VB and make them VB programs.I've used PSET extensively on every combination of network and PC, from handheld andserial port to NT and network. As long as the drivers etc. are in place, it works perfectly.Performance TuningThis 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. Moreimportantly, it explains when to optimize and when not to optimize. The condensedversion 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 produceonly a small benefit. Even if a program uses huge collections, converting them into arrayswill probably only save you a few percent in run time. On the other hand, rewriting theprogram'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 atthe 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 themdata" can in some cases give funny results, so I prefer to use the

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

Page 147: VB1

Helpmate

LockWindowUpdate API Call. You call it once passing the hwnd of the control,and all repaint messages will be blocked for that control (giving the same effect ashiding it), you then call it again with 0 as the parameter to restart the repaintmessages. 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 todisplay 1,000 strings in a ListBox. Set the control's Visible property to False, setthe strings, and then set Visible back to True. The reason this makes it faster isbecause the ListBox doesn't have to "repaint" itself every time a string of data issent. The ListBox only has to paint itself once, not every single time an item isadded.

• Constantijn Enders:Split IF functions. Visual Basic doesn't has an option like complete booleanevaluation. If the first expression is false it will still evaluate the second one evenif 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 SubAnd if possible put the fastest function at the top line If both function have thesame 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 needfor UCase$ functions. You can still use StrComp(s1, s2, vbBinaryCompare).

• Constantijn Enders:Parse results directly to controls. If CheckPassword is a function which result aboolean:

• If CheckPassword = True Then• cmdLogon.Enabled = True• Else• cmdLogon.Enabled = False

End IfIs slower than: cmdLogon.Enabled = (CheckPassword = True)Or even better: cmdLogon.Enabled = CheckPassword

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

Page 148: VB1

Helpmate

• Constantijn Enders:Cache the results of a function just as you would a property.

• For i = 1 To 10• Command(i).Enabled = CheckPassword

NextIs slower than: bEnabled = CheckPassword For i = 1 to 10 Command(i).Enabled = bEnabled NextBecause in the first routine the CheckPassword function is executed 10 times.

• Carman Thornton:In addition to using integer operations whenever possible. Use \ for divisioninstead of / (it's faster). Use * .5 instead of / 2. Example: 11 * .5 = 5.5 is fasterthan 11 / 2 = 5.5. Assembler instruction "MUL" is faster than "FDIV".

[Click here to download a test program comparing different calculations. Youmay 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 canmake an enormous difference, depending on your application.

If you have to do anything with repetative calculations really fast (ie: Circles oranything dealing with Trig functions), it may help out a lot to create a table ofvalues for whatever resolution you need for the data.

For example, precalculating the x, y coordinates of a circle about a point everytwo degrees (or use radians, which are actually better for this) is often goodenough 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 theultimate solution when you REALLY need more performance, so I am listing itanyway.)

• Upgrade to Visual Basic 5 or 6. Compiled native Visual Basic executables are alot 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 isanother expensive option, but easier than learning a new language.)

• Profile your application. Use a performance monitoring tool to see exactly wherethe 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 timeoptimizing code that is already fast enough.

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

Page 149: VB1

Helpmate

• Decompress graphics. Set the Picture property for a Form or PictureBox to a .bmpfile, not a compressed JPEG or GIF file. Those files are stored in the program incompressed form so the program needs extra time to display them.

• Preload forms. Load all the forms you will use when the program starts. Then theycan display faster when you need them.

• Use arrays instead of collections. Arrays are much faster. Use collections only ifyou 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. Thistakes longer than leaving the array alone (the previous tip), but is faster thansetting the entries one at a time.

• To set entries to zero in a fixed-sized array (allocated using Dim), use the Erasestatement. This destroys dynamically allocated arrays, but resets fixed-sizedarrays. (Thanks to BwetS).

• Use the MemCopy or RtlMoveMemory API functions to copy arrays instead ofcopying 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. Beas specific as possible. For example, Object is bad, Control is better, TextBox isbest.

• Do not empty a collection by removing its elements. Destroy it by setting it toNothing.

• Declare and allocate objects in separate lines. The statement "Dim obj As NewMyClass" is actually slower than "Dim obj As MyClass" and "Set obj = NewMyClass" on two separate lines (try it).

• Use integer operations whenever possible. Use \ for division instead of / (it'sfaster).

• 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 executesfaster than if you repeat the series of objects in each statement.

• Use as few string operations as possible, they are slow.

• Order Select Case statements so the most commonly used value comes first.

• Call sub and function with by ref parameters when possible. Adriano Ghezzi

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

Page 150: VB1

Helpmate

[Note that this makes the routine more prone to accidental side effects so becareful--Rod]

• Set form to nothing when you never need. Adriano Ghezzi [This saves memoryand may save lots of time if you have so many forms that you must page. If youonly have a few forms, it will be faster to keep them always loaded and just hidethem--Rod]

• Perceived performance is as important as actual performance. Imagine clicking ona button, and nothing happens for 10 seconds. That will be a very long 10seconds. 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 oneform and load them when needed from that form. Jan Cromwijk [This makes allthe images load when that form is loaded so they are ready to go when you needthem--Rod]

• If you need to do a lot of string/file processing, use mid$ (and trim$ etc.) ratherthan mid as the latter treats the data type as a variant as opposed to a string, whichcan be up to 3 times slower (I think you can use the $ sign with mid, trim, left andright). 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 performinglong startup calculations.

• Put as little code as possible in Form_Load event handlers so the forms loadquickly.

• If the initial form taks a long time to load, display a splash screen immediatelyand remove it only when the first form is loaded (Advanced Visual BasicTechniques shows how to make different kinds of interesting splash screens).

• Group subroutines in modules. When one routine calls another, the other routine'smodule is loaded. If one routine calls many others from different modules, all themodules must be loaded. If all the routines are in the same module, they will all beloaded at once.

• Do not waste memory. Sometimes you can make a program faster using morememory, but sometimes more memory can slow things down. In particular, if youuse so much memory that the program cannot fit in real memory all at once, thesystem will page. That can slow the program enormously.

• Set AutoRedraw to False to reduce memory usage. Set AutoRedraw to True tomake redrawing faster for complicated drawings.

• Set ClipControls to False (read the help for more information).

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

Page 151: VB1

Helpmate

• Use Move to position controls instead of setting the Left and Top properties.

• Hide a control if you need to change a bunch of its appearance properties. Make itvisible again when you are done modifying it.

• Use a temporary variable to refer to a complex expression multiple times. Forexample, suppose you need to set several values in theSelectedEmployee.NextOfKin.HomeInformation.Address object. Instead ofreferring 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 totxtLastName.Left several times, save that value in a variable and refer to thevariable 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 lessmemory.

• Use Frame controls to hold other controls instead of PictureBoxes. Frame controlstake less memory.

• Use control arrays for controls that are unimportant. For example, many formscontain a lot of uninteresting labels. Put them all in a control array. A controlarray 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 variablenames, and blank lines do not add to the compiled program's size so there is noharm 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 upmemory.

• Use DoEvents to allow the user to perform other actions while your long processis running. This can reduce the user's frustration even if it doesn't make theprogram move faster. (John Dye)

• Use the FindFirstFile, FindNextFile, and FindClose API functions to quicklysearch directories. Thanks to Nikolaos D. Dimopoulos.

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

Page 152: VB1

Helpmate

[Note that using API functions is often but not always faster. It is always morecomplicated and sometimes riskier than using VB--Rod]

• UCase$ and LCase$ let you perform case insensitive comparisons. The followingAPI 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. Forexample, suppose you need to set several values in theSelectedEmployee.NextOfKin.HomeInformation.Address object. Instead ofreferring 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 WithThanks to Mark Focas.

• Use ByRef to pass values instead of ByVal. When you use ByRef, the programpasses the (small) address of the value. When you use ByVal, it must make a newcopy of the value and pass that. Generally it is faster to pass the address instead ofa 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 theother process. If you use Byref, it must then unpackage the returned result and thattakes extra time. Thanks to Kevin B. Castleberry.

• Use * instead of ^ to take simple integer powers. For example, use A = B * Binstead of A = B ^ 2. The first is faster. Thanks to Michalis Vlastos.

• If you need to build a long string, build it in pieces and then join the pieces whenthey are all finished. For example, suppose subroutines AddText1, AddText2, etc.append text to a string. Then the following code:

• Dim txt As String• • txt = AddText1(txt)

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

Page 153: VB1

Helpmate

• txt = AddText2(txt)• txt = AddText3(txt)

takes longer than this code: Dim txt As String Dim txt1 As String Dim txt2 As String Dim txt3 As String

AddText1(txt1) AddText2(txt2) AddText3(txt3) txt = txt1 & txt2 & txt3In the first code, the AddText subroutines must manipulate long strings. In thesecond 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 tonative code for speed. For large interactive applications which are not processorbound, I have found the size of the executable for the compiled version causes itto load much slower and execute slower due to the large executable size, andprobably larger working set. I had a 10MB exe go down to 4MB by switchingback to P-Code. The compile time is vastly shorter as well, resulting in quickertest-cycles. We no longer compile to native code at all, even on smallerapplications.

[Database bound applications may show the same effect. Any program that spendsa 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 smallerexecutables 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 thelooping logic.

i = 1 Do While i <= SlowFunction() total = total + i LoopRuns slower than

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

Page 154: VB1

Helpmate

i_max = SlowFunction() i = 1 Do While i <= i_max total = total + i Loop[Note: For loops do not evaluate their bounds every time through the loop. Whenthe For statement starts, the system calculates the upper bound and saves it. Itdoes not recalculate it every time through the loop. Therefore this code isreasonably fast: For i = 1 To SlowFunction() total = total + i Next iRod]

Fifty Ways to Improve Your Visual BasicPrograms

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

I don't expect you to agree with everything presented here. Programmers can bepassionate about their beliefs as to what is good or bad. But hopefully, there is a tidbit ortwo 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 toO'Reilly. But shortly after I sent in my proposal, .NET appeared on the scene; thus, doinga 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 thebeginning 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 dothat!" 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 softwarearchitect and developer. With that said, let's get to the tips, which are broken down intothe 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 simplyaccessing a basic property, define an error handler using On Error GoTo. Thisshould look similar to the following:

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

Page 155: VB1

Helpmate

2. Private Sub Encryptolizer()3. 4. On Error GoTo Errs5. 6. 'Code goes here7. 8. Exit Sub9. Errs:10. 'Call error handler here11. End Sub

Even if your function, property, or sub only contains a single line of code, put thaterror handler in there. The drawback to this is a substantially larger executable.The benefit is code that will eventually have little to no errors. The error handleritself can be a standard function located in a module or in a class that encapsulatesall of the required functionality of the handler.However you decide to implement it, make the error handler as robust as possible.And try this: Next time you start a project, write the error-handling code first, sothat it is in place. If you treat your exception-handling code as the most importantpart of an application (it is actually) instead of as a footnote to be tacked on at theend of the development cycle, you will be amazed at how strong your applicationwill be by the time it goes to testing.

12. Never use On Er ror Resume Next. Using the On Error Resume Next statement not onlyviolates tip 1, it also makes code difficult to debug. This statement should beavoided at all costs. It breeds the most hard-to-find and unpredictable bugs youcould ever imagine. And for those of you who know assembly language, trydisassembling code with an On Error Resume Next statement embedded in it. Whatyou will find is a massive jumble of spaghetti code that is injected into thefunction by the compiler in order to implement this functionality. Imagine a 9-year-old programmer who likes GoSub and GoTo and you get the idea.

13. Indent, use white space, and comment your code. This is a simple suggestionmeant to make your code more readable. First, all code should be indented once(four spaces) within the block where it resides. Second, all code should have atleast one line of white space separating it from the block where it resides. Third,declare all variables on their own line so that they can have comments associatedwith them. It can also be beneficial for large blocks of code to have a comment onthe End statement (End If, Loop, and so forth) of the block that points to the start ofthe block. Well-formatted code looks like this (squint your eyes and pretend thatthe function does something):

14. Public Function CrispAndClean() As Boolean15. 16. On Error Goto Errs17. 18. Dim x As Integer 'x coordinate19. Dim y As Integer 'y coordinate20. Dim z As Integer 'z coordinate21. 22. If (x > 10) Then23. 24. If (z < 20) Then

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

Page 156: VB1

Helpmate

25. 26. 'code starts here27. 28. End If29. 30. Else31. 32. x = x * 1033. y = y * 2034. z = z * 3035. 36. End If 'If (x > 10) Then37. 38. Exit Sub39. 40. Errs:41. 'Error handler here42. 43. End Function

44. Know the language. There are many great functions hidden away in VB that youmay not know about. Spend time learning the language. The VB Help file is agreat place to start. Go to the index, start at the top, and read about any keywordyou don't know. After you have done that, get a solid desktop reference. Handsdown, VB & VBA in a Nutshell: The Language is the best language reference onthe market. Believe it or not, that is my unbiased opinion. I owned a copy before Iever wrote a single word for O'Reilly.

This topic comes up for a reason. Once I was working on a project with a reallygreat 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, itcould do in one line of code what his was doing in over 20. Needless to say, hechucked 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. Avoidusing DefInt, GoSub, QBColor, REM, and Let. And unless you are writing anapplication with accessibility considerations, consider avoiding Beep as well.Users find it annoying. Also, avoid declaring data types with suffixes instead ofthe name of the data type:

46. Dim name$ 'This looks like my old Atari 800 code47. Dim num%48. Dim bignum&

I mention the declaration syntax for reasons of clarity. A good rule of thumb isthat you should always be clear, versus, well, not being clear. Don't be afraid totype. And don't put more than one statement on the same line:If x = 4 Then: x = 5: y = 6Nobody likes reading code like this.

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

Page 157: VB1

Helpmate

49. Don't sacrifice maintainability for speed. There are many things that can bedone in VB to make an application run faster. Global variables, undocumentedpointer functions, and loop unrolling are just a few. Unless you are maintainingyour own code for your own software company avoid these things and try to makeyour code as readable as possible. If you work in the corporate world this is veryimportant. The person who inherits your code when you've moved on to greenerpastures will praise your name instead of curse you. Believe me, the folks whopay you don't care how slick you can code. Ultimately, they want something thatwill not only work, but is also flexible and easy to maintain. If you are doingsomething that requires intimate knowledge of the black arts of coding, don't doit.

Var iables and Declarations

7. Always use Option Explicit. By default, VB is set for Sloppy Mode. In SloppyMode, variables don't have to be declared before they are used. Why is this bad?It leads to hard-to-find bugs, should you happen to misspell a variable name:

8. Public Function Vague()9. 10. position = (x * y) + 511. 12. 'more code here13. 14. If (position > 10) Then15. 16. 'This code will never be executed17. 'because "position" is misspelled18. 19. End If20. 21. End Function

To take VB out of Sloppy Mode, open the Tools/Options dialog from the menu.On the first tab, named Editor, check the option that says "Require VariableDeclaration." If you are doing this after you've already started a project, makesure any forms, classes, or modules you've already created have Option Explicit atthe top of the file.

22. Don't use Hungarian notation. Gasp! I am going to go against popular opinionon this and argue against using Hungarian notation in any way, shape, or form.This is my opinion, but I think well-named variables are better. I am actually arecent convert to the dark side. I used to use Hungarian notation religiously, but Ihave since been shown the error of my ways.

The first reason I will give you is that the design guidelines for the up-and-coming.NET Framework say not to use it. It would be an easy cop-out to say that this isthe 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 frontof everything can you?

But there are other reasons:

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

Page 158: VB1

Helpmate

o VB is not coded outside of the VB IDE. I am sure there are a fewrenegades out there who use vi for Windows, but for the other 99 percentof us, the IntelliSense built into the IDE works just great for telling youthe type for all your variables.

o Another argument for using Hungarian notation is: "When I print out mycode to read it, I want to be able to see what I am dealing with." This is avalid argument. On the other hand, some people prefer looking at well-named variables instead of a bunch of gobbledygook.

o What if the data type changes? You have explicitly tied the variable nameto its type. In addition to having to redeclare your variables to the newtype, you have to change every single place where the variable was used touse the new Hungarian prefix. Think about the repercussions of this. Someof you out there will say that global Find and Replace was created forthings like this. But I say that this is a clue that Hungarian notation islacking.

23. I f you are going to use Hungarian notion anyway, maintain style. Use thesame style that is being used in the project. If everyone else is prefixing integerswith an n, follow suit. If you are the only one on the project, at least make sureyour style is consistent. Read this Microsoft document for more information.

24. Don't use hard-coded values. Always avoid using literal values. Even if youknow a value will never change, you should at least use a constant.

25. Don't use global variables. Global variables lead to global problems, especiallywhen you are working on a large project with several developers. However, insome circumstance a global variable will be necessary. There are just those timeswhen you need to get something done instead of designing the applicationproperly (yes, this is a barb). In these cases, create accessor functions to get to theglobal variable instead of using it directly:

26. 'Global27. Public GlobalNumber As Integer28. 29. 'Accessor functions30. Public Function Get_GlobalNumber() As Integer31. Get_GlobalNumber = GlobalNumber32. End Function33. 34. Public Function Set_GlobalNumber(ByVal number As Integer)35. GlobalNumber = number36. End Function

It is especially important to do this if your global variable needs to be validated inany way. But unfortunately, there is no way to force other developers to use theaccessor methods. When faced with a situation like this, ask yourself whyGlobalNumber isn't a private member of a class as it should be.

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

Page 159: VB1

Helpmate

37. Think small. What does this mean exactly? It means several things. One, itmeans that you should strive to limit the scope of all your variables as much aspossible. In the ideal situation every variable is private.

Two, make your class interfaces as small as possible. Accomplish as much as youcan with the fewest number of public functions that have the smallest number ofparameters (tip 15 is the exception).

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

Function Design

14. Procedures should be designed to complete a single task. Avoid writingprocedures that try to accomplish too much. Failing to follow this rule will makeyour code less modular, which ultimately could lead to code that is harder tomanage or extend.

15. Pass as much information as possible when using remote objects. If you areusing remote objects such as ActiveX DLL's in a COM+ setting or an ActiveXEXE, 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. Avoidusing Exit Function and Exit Sub. Use a GoTo to jump to your cleanup block (see nexttip), or see if you can restructure your code differently. As a general rule, yourcode should flow from top to bottom. Exit Loop and Exit For are fine because theydo not disrupt program flow. I think you will find that your code will be easier todebug 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 isvalid to use GoTo in order to jump to the program exit. (I didn't make this up, Istarted doing this years ago, after reading Writing Solid Code.)

Using GoTo allows you to implement generic error handling and allows thefunction to place any cleanup code in a single location, rather than repeating itthroughout the routine. Here's some pseudo-code:

Public Function GetData() As Boolean On Error Goto Errs

'Allocate resources that need cleaning up Dim conn As ADODB.Connection conn.Open(.)

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

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

Page 160: VB1

Helpmate

If (something bad happens) Then Goto Cleanup End If

If (another bad thing happens) Then Goto Cleanup End If

'More code here

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

Exit Sub

Errs: 'Call error handler here

End FunctionUsing GoTo is bad, but only if you don't know what you're doing. This is one ofthe few valid reasons to ever use them.

18. Use ByVal for function parameters. I am not going into much detail on this topicbecause it is actually bigger than it looks. There are reasons for calling ByVal thatdeal with performance, but the reason this tip exists is simple: If you do not passByVal, you are modifying the variable that was passed in. This variable willremain modified once the function exits, which can cause problems for thebeginning programmer.

19. Default functions to failed condition upon entry. After you declare your localvariables, the first line of code in a function should either set the return value to adefault value or a False condition. There are usually a lot more things that can gowrong, but only a few things that will satisfy a True result.

Yes, VB will automatically set a Boolean value to False, but do it yourselfanyway. It states your intent clearly. And seriously, this topic would not exist if Ididn't see the following all the time (I literally saw this last week):

Public Function Foo() As Boolean

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

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

Page 161: VB1

Helpmate

Else Foo = False End If

End FunctionThe first clue something is wrong is that Foo is set to False three times. If youassume a False condition from the onset, you can rewrite the function to look likethis: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 FunctionHere, each condition is set only once.

Classes, Objects, and Object Models

20. I f 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 UDTas a parameter. In this case, what you actually want is a class so that you canencapsulate your data along with the functions that manipulate that data. UDTsshould only be used to group small pieces of related data. This data should onlyconsist of primitive data types (Integer, Long, Double) and not object references.

21. Understand when to use properties versus methods. Properties describe anobject's state: like a color, a name, or a quantity. A function describes a behavior:Open, Close, Release. Properties are nouns. Methods are verbs.

22. Model " Has A" relationships through containment. Containment oraggregation is when one object contains another (usually as a private member ofthe class). For instance, a Car class could contain an Engine class. A Boat classcould contain an Engine class, too.

23. Model " Is A" relationships with interfaces. Define common object behaviorswith an interface. For instance, if you were writing objects for an online retaileryou might have a Visa class, a Discover class, and a MasterCard class. Whileeach 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 timeyet keeps them backwards compatible. Keep your interfaces as small as possible,and build your objects through the use of interface composition.

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

Page 162: VB1

Helpmate

25. Implement several inter faces to define object behavior. Interfaces shouldrepresent a service that your object provides. These services are not necessarilyintrinsic to the object itself. For instance, an IPlay interface could be implementedby VCR, DVD, or CDPlayer. Each of these classes handles a different media in adifferent way, but the interface (play, pause, stop, etc.) is the same.

26. Never break an interface--no matter what! This is pretty self-explanatory.Once an interface has been used, don't change it. Apply this rule to publishedclasses as well, and you will do even better.

27. Use Implements in a way that supports polymorphism. Consider the credit cardclasses discussed in tip 23. Basing each class on an interface, would allow you towrite code that could manipulate these objects polymorphously:

28. Dim v As Visa29. Set v = New Visa30. 31. .32. .33. .34. Call BillOrder(Visa)35. .36. .37. .38. 39. Public Sub BillOrder (ByVal pCreditCard As ICreditCard)40. 41. If pCreditCard.Bill Then42. 'Ship the order43. End If44. 45. End Function

Why is this great? Well, what if you add more credit card types, or even a giftcertificate? You would not have to rewrite the BillOrder method every time youadded a new type of payment.

46. Free object references. Be aware of the objects you are using and set them equalto Nothing as soon as they are no longer needed. Don't necessarily wait for thecleanup 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 amethod 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 outand learn UML and start drawing object diagrams. Write out the methods youthink the class should have. Draw pictures and use a pencil. Sure, this isunorthodox, but the idea here is to contemplate instead of diving head first intothe 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 totip 8 somewhat. Go take a look at ADO in the Object Browser and you will noticethat none of the parameters for any of the Public methods use Hungarian notation.

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

Page 163: VB1

Helpmate

I once worked with a guy who used to name his functions using Hungariannotation, too. Since every one of his functions returned a Boolean, IntelliSensewas pretty much rendered useless because everything began with a "b". Keep inmind how the object browser and IntelliSense will present your code to otherdevelopers.

The User Inter face

32. Read " Official Guidelines for User Inter face Developers and Designers."This is another Microsoft document worth reading, which deals with userinterfaces.

33. Separate the business logic from the user inter face. Forms should only containdata validation and persistence code. Business logic and database access codeshould be in another tier.

34. Persist your forms. Persistence means that the user interface should save its statebetween invocations. It should default to whatever settings the user last selected.

35. Minimal is better. The simpler the UI the better. Don't distract the user byplacing all of the latest and greatest controls on a form, or non-standard UIelements like purple buttons and baby-blue list boxes.

36. The user interface should be able to convey the result of all input. Wheneverthe user does something, make sure the UI conveys it.

37. Don't report normal conditions to the user with dialogs. The less popupwindows a user has to look at the better. Try to use other visual queues to get youruser's attention. Dialogs interrupt the user and he or she will find it annoying afterawhile.

38. Don't require the user to enter information that can be obtainedautomatically.

Do as much as possible for the user. For instance, if 9 times out of 10 the currentmonth is used, make sure your date text box already has the current date in it. Dothe most, with the fewest number of clicks and the smallest amount of keyboardstrokes.

39. Forget that MDI windows exist. Enough said. If you have no idea what an MDIwindow is, you are off to a good start. Most people don't do it right anyway, so itis not worth the risk.

40. Work to eliminate tedium. Make sure the user can get related information asquickly as possible (but not at the expense of the user interface).

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

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

Page 164: VB1

Helpmate

42. Don't forget about the keyboard. Assume there is no mouse. Provide the meansto 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 anyconsiderable amount of time.

43. Use popular applications as a model. I like to use Word or SQL 2000 as amodel for user interface design. You might use a different application. It's up toyou. Look at professional applications and model yours after them. The sad truthis that most programmers are not very good at designing user interfaces. There isno shame in stealing someone else's ideas.

44. Don't display errors that should have been prevented. A user shouldn't have tolook at a dialog box stating that your application has performed a divide by zeroerror. For one thing, this error never should have made it this far.

45. Eliminate the possibility of errors by restr icting input. Prepopulate form fieldswhenever possible, and limit what a user can do in the UI. Try to prevent the userfrom entering as much free form data as possible. For instance, if you havenumerical data between 1 and 1000 make sure the MaxLength property of thetextbox is set to 4. If you have numerical data between 1 and 20, use a dropdownwith the numbers 1 to 20 in it.

Provide lists through dropdown combos instead of allowing users to enterinformation into a textbox. Where I am working at this moment, there is areporting application that asks the user to enter in the "account types" they wish tofilter on. Why this is not presented through a multiselect listbox, I don't know; theaccount types are stored in a database.

The point to this topic is that the more you can restrict the input of the user, theless validation code you will have to write.

46. User inter face should be consistent. All visual elements of your UI should bethe 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 areused to using them that way. Using Option Base makes looking at your codesomewhat of a guessing game.

48. Avoid ReDim. This is because ReDim can be an expensive operation because everytime you do this you are actually allocating a new block of memory. ReDimPreserve is even more expensive because an additional memory copy is used tomove all of the elements from the old block of memory to the new block. Use ofthese commands here and there is OK if you are aware of the penalties involved. Ibring this up because I have seen ReDim used in loops so many times (I can't countthat high) that is unbelievable. Think about how you could use a collection insituations where you are using ReDim in a loop.

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

Page 165: VB1

Helpmate

49. Avoid the operator precedence guessing game. Use parenthesis to guarantee theoutcome equations and logical branches. Yes, even professional programmers canget tripped up over operator precedence. I avoid having to think about it by usingparenthesis 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 thatsomeone 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 beendiscussed during several of the tips here. It really is the foundation of goodprogramming.

J. P. Hamilton is an independent software developer who lives and works in Houston,Texas, perhaps the largest underrated city in the known universe. He was born and raisedon the 6502 processor (long live Atari) and punk rock, but now devotes much of his timeto the .NET Framework. Currently, when he is not programming, he is reading aboutprogramming, writing about programming, or dreaming about programming. But hehopes to start building a Japanese garden at his home this fall when the weather coolsdown and the mosquitoes are dead.

My Top 15 VB Tips and Tr icks

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

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

Page 166: VB1

Helpmate

& "WHERE userid = " & lUser & vbCrLf _ & "AND color = '" & sColor "'"then you can quickly and easily read your completed SQL code to find the problem.The previous example illustrates another benefit of vbCrLf: if you don't use it, you mustremember to start (or end) each line with a space; otherwise, sSQL = "SELECT * FROM anytable" & _ & "WHERE userid = " & lUserbecomes "SELECT * FROM anytableWHERE userid = 1"and your code will generate a SQL syntax error.

2. Use #I f ccDebug to View Hidden Automation ServersOne of the problems with testing and debugging a remote automation server is that theserver application typically remains hidden, doing its work in the background. Hence,when something goes wrong, you frequently can't tell what has happened or where inyour code your application is being derailed.Consequently, when you're developing, testing, and debugging your application, it's agood idea to make sure that otherwise hidden automation server remain visible. You ando this by using code like the following to make sure that a new instance of anautomation server (in this case Microsoft Word) is visible: Dim objWord as Word.Application Set objWord = New Application

#If ccDebug Then objWord.Visible = True #End IfYou can define the project-level conditional compiler constant ccDebug in either of twoways:

• By including the line

• #Const ccDebug = 1in the declarations section of a form or code module.

• By using the Make tab of the Project Properties dialog (select the ProjectProperties option from the VB Project menu) and entering the following in theConditional Compilation Arguments text box:

• ccDebug = 1Then, whenever your code runs in the design-time environment, you automation serverwill be visible. When you are ready to create the final executable, you should either setthe ccDebug conditional compilation constant to 0 or, if you've used the ConditionalCompilation Arguments text box, remove its definition.

3. Implement an IsSaved (or IsDir ty) Proper tyI often find this technique very useful, but it takes a little extra work when you're creatingyour object classes. On balance, despite this up front investment of time, it saves a gooddeal of programming time by making it easy to determining when an object has changedand therefore needs to be saved.To implement an IsSaved or IsDirty property, each Property Let procedure of a particularobject must contain a line of code to determine if the value to be assigned to the property

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

Page 167: VB1

Helpmate

is different than its current value (which should be stored in a private member variable).If it is different, then the private member variable that represents the IsSaved property isset 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 IsDirtyproperty 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 easilyas follows: If Not myObject.IsSaved Then SaveTheObject End IfOn the server object, this is implemented as a simple Property Get procedure: Property Get IsSaved() As Boolean IsSaved = mbIsSaved End PropertyAnother neat addition to this is to define an object event called something likeObjectChanged. Then the event can be fired whenever some attribute of the objectchanges: Property Let CustStreetAddr(sVal As String) If msCustStreetAddr <> sVal Then msCustStreetAddr = sVal mbIsSaved = False RaiseEvent ObjectChanged() End If End PropertyOn the client form, you can then implement an event handler for the ObjectChangedevent that enables the Save button when the object needs to be saved: Sub MyObject_ObjectChanged() cmdSave.Enabled = Not myObject.IsSaved End SubThis code enables the Save button when the object is not saved and disables the buttonwhen the object has been changed.I should add a major qualification to this tip: don't update your object property based onthe Change event handler of a text box. The Change event is fired for each keystroke thatthe text box receives. Therefore typing a word like "Stupid" into the text box will fire off6 Change events - and the final result is that the text box could contain the same wordthat it originally started with, so that in fact its contents haven't changed at all despite thefiring of six unnecessary events.

4. Implement a For Each...Next Statement against a Collection ClassMost of the time, we take for granted the For Each...Next loop, which iterates the membersof an array or a collection. It's the fastest, most efficient method of visiting all themembers of the collection or array, and we could care less that, as it enumerates thecollection, the unseen code is actually generating new references to members of thecollection with each iteration of the loop. However, as the provider of a collection class,

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

Page 168: VB1

Helpmate

it is up to you to provide an interface that the For Each...Next statement can work with.This may sound a little daunting, but you'll be pleasantly surprised how easy it is toimplement a property that enumerates members of the collection within your class. Firstof 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 Propertywhere 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 theProcedure Attributes option from the Tools menu, then click the Advanced button on theProcedure Attributes dialog. Enter a Procedure ID of -4. You should also check the "Hidethis member" option to prevent the property from appearing in the IntelliSense dropdown.

5. Implement an Exists Method within a Collection ClassOne of my long standing gripes about the Collection object is the complete absence lackof an easy method to determine whether the member you're looking for exists within thecollection. Therefore, when I 'm writing a wrapper class for a collection, I always includemy own.However, I add a little more to the method than simply determining if the member existsin the collection. If the member is not found within the collection, I attempt to add it tothe collection. This way, I can simplify the code at the client end by always calling theExists method prior to assigning the member to a local object variable. Therefore, I knowthat if the Exists method returns true, I can safely go on to assign the member to the localobject variable.The code for a typical Exists method is shown below: Public Function Exists(sDomainName As String) As Boolean

On Error GoTo Exists_Err

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

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

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

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

Page 169: VB1

Helpmate

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

6. Implement a Fast Reset Method within a FormThere are many occasions where you need to reset the controls on a form, either inreadiness for a new record, or perhaps when a record has been displayed and the userclicks the button to enter a new record.You could, of course, reference each control on the form individually to reset its display.However, I tend to whip through every control on the form and perform the samefunction on each. Of course, not all controls have the same properties, so I preface myprocedure with an On Error Resume Next statement that tells my code to ignore any errorsand continue with the next line of code: On Error Resume Next For Each oControl In Controls oControl.Text = "" 'mainly for text box controls oControl.ListIndex = -1 'reset combo and list boxes. Next

7. Beware O'Reilly, O'Malley, and I t'sYou've spent the last 17 years writing your new application and it works like a dream.You've got error handling coming out of your ears, belts, and braces. You've tested theapplication till you were blue in the face. Then, first day out in the field, a user contactsyou and says, "It won't let me save this record."Huh? What could be wrong? It can't be your code. No way!You hunt through the code, and eventually find out from the user that the error messagewas a SQL syntax error that only reared its ugly head when the customers name wasO'SomethingOrOther. Ahha!

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

Page 170: VB1

Helpmate

Look at this embedded SQL Script: sSQL = "UPDATE Customers Set CustName = '" & sCustName & _ "' WHERE CustID = " & custID"Given a customer name of "Lomax" and an ID of 100, then the string sSQL will contain aperfectly valid "UPDATE Customers Set CustName = 'Lomax' WHERE CustID = 100"However, change the customer name to "O'Reilly" and you generate a SQL syntax error: "UPDATE Customers Set CustName = 'O'Reilly' WHERE CustID = 100"The "O" is the string variable for CustName, but "Reilly" is a string that has no beginning(or is it the "O" that has no end? Hmmm..). So what's the answer? Well, there are a fewways of sorting out this little problem. One of the easiest is to replace all single quotationmarks ("'") within strings with an apostrophe ("`" ). There are several ways to do this, buthere's my favorite (for pre-VB6 code): Mid(CustName, Instr(CustName, 1, "'"), 1) = "`"This code fragment makes unusual use of Mid on the left side of the statement. It replacesthe single character "'" with the "`" character. However, this single line of code can onlyreplace the first instance of a single quotation mark ("'") in a string. To replace allinstances, you'd have to implement a Do While loop like the following: Do While Instr(CustName,1,"'") > 1 Mid(CustName, Instr(CustName, 1, "'"), 1) = "`" LoopIf you're using VB6, you can use the new Replace function to substitute a single line ofcode for the code block: CustName = Replace(CustName, "'", "`")You'll also find that providing users with large, multi-line text boxes for entering freeformat text provides opportunities for them to enter words like "It's", "They're", and otherapostrophe-containing, SQL script-breaking words. So be sure to remove the apostropheshere, too.

8. Populate Classes in One GoI've called this a mass assignation function. What's a mass assignation function? Let's sayyou have a collection class that contains 20 properties, which you populate by readingdata from a database. You have a procedure that opens the database, creates a recordset,and then assigns the values from the recordset to each of the relevant properties in theclass, something like this: oClass.EmployeeNo = rsRecordset!EmpNo oClass.FirstName = rsRecordset!FirstName Etc...Using this method, you are calling the Property Let procedure of each property. If there isvalidation code within the Property Let procedure, this must execute too, most likely ondata that has been validated before being saved in the database. A more efficient methodof population is to create a function within the collection class, like this: Friend Function Initialize(sEmpNo as String, _ sFirstName as String ...etc) As Boolean msEmpNo = sEmpNo msFirstName = sFirstName ...Etc...This single function assigns all the values for the object in one go by assigning the valuesdirectly to the local variables, thus bypassing the Property Let procedures and the redundant

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

Page 171: VB1

Helpmate

validation code. You can therefore pass all the values to populate the object in one go: If oClass.Initialise( rsRecordset!EmpNo, _ rsRecordset!FirstName, _ etc...) ThenOf 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 beenvalidated. You will find that a mass assignation function will dramatically improve theperformance of your collection classes.

9. Using API Calls to Create a Software Timer ClassYou 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 clutteringup the server with forms. This example also shows how callback functions and theAddressOf operator are used.The following code forms two separate projects. The first is the automation server; thisconsists of a class module (clsRemTimer) and a code module. The code module isnecessary to provide a callback procedure for the API functions used to initiate anddestroy the Windows Timer.TimerServer.vbp - clsRemTimer.cls Option Explicit

Public Event Timer() Private blnEnabled As Boolean Private lTimerID As Long Private lInterval As Long

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

Private Function StartTimer() As Boolean

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

End Function

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

Page 172: VB1

Helpmate

Private Function StopTimer() As Boolean

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

End Function

Friend Function RaiseTimerEvent()

RaiseEvent Timer

End Function

Private Sub Class_Terminate() Call TimerStop End SubTimerServer.vbp - modTimer.bas Option Explicit

Private oTimer As clsRemTimer Private lTimerID As Long

Declare Function SetTimer Lib "user32" _ (ByVal hwnd As Long, _ ByVal nIDEvent As Long, _ ByVal uElapse As Long, _ ByVal lpTimerFunc As Long) As Long

Declare Function KillTimer Lib "user32" _ (ByVal hwnd As Long, _ ByVal nIDEvent As Long) As Long

Public Sub TimerCallBack(ByVal hwnd As Long, _ ByVal uMsg As Long, _ ByVal idEvent As Long, _ ByVal dwTime As Long)

oTimer.RaiseTimerEvent

End Sub

Public Function TimerStart(ByRef oTmr As clsRemTimer, _ lInterval As Long) As Long Set oTimer = oTmr lTimerID = SetTimer(0, 0, lInterval, AddressOf TimerCallBack) TimerStart = lTimerID

End Function

Public Function TimerStop() As Long

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

Page 173: VB1

Helpmate

TimerStop = KillTimer(0, lTimerID) Set oTimer = Nothing End FunctionTimerClient.vbp - frmTimerClient.frmFinally, here's a sample client, which is simply a form that uses the RemTimer class. Notethat for this to function correctly, the client must declare the instance of the RemTimerclass WithEvents. This sample client form contains one text box (named Text1) with itsLocked property set to True and one command button (named Command1). Option Explicit

Private WithEvents oTimer As TimerServer.clsRemTimer

Private Sub Form_Load()

Set oTimer = New TimerServer.clsRemTimer oTimer.Interval = 1000 Text1.Text = 0 Command1.Caption = "Start"

End Sub

Private Sub Command1_Click()

On Error GoTo Command1_Err

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

Command1_Err: MsgBox Err.Description

End Sub

Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer) Set oTimer = Nothing End Sub

Private Sub oTimer_Timer()

Text1.Text = Text1.Text + 1

End Sub

10. Use a Count Proper ty to Populate a ClassOne problem you always have when architecting a class hierarchy is implementing amethod that allows the user of the class to populate the class. For example, you couldimplement a Load method that reads all relevant records into the class. However, I preferto automatically populate classes using the Count property; this method produces quite

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

Page 174: VB1

Helpmate

elegant code within the client.Here's an example of a Count property that automatically populates the class: Public Property Count() As Long If colMyCollection.Count = 0 Then LoadRecordsIntoObject End If Count = colMyCollection.Count End PropertyBeware, though, you may need to add a filter property to reduce the number of recordsautomatically loaded into the object. Also, bear in mind that many times these days, theperson writing the objects will not be the person who writes the client-side code.Therefore, you should make your class objects as easy to use as possible.Here's an example of some client side code that utilizes the Count property shown above: Set oDomains = New Domains oDomains.Type = 1 If oDomains.Count > 0 Then sNames = oDomains.Names End If Set oDomains = Nothing

11. Never Create Custom SubsThere is no operational difference between a sub and a function. A sub is the VBequivalent of a void function in C and C++. However, if you use a function, you have thechoice of using or ignoring the function's return value.If you were about to create a Sub because you don't necessarily need a particular returnvalue, then create a function with a Boolean return value instead. This way, you canreturn True if all went well, or False if some error occurred. Your code will be muchmore robust as a result.For example, the following code calls two sub procedures, one after the other: Call DoFirstRoutine(sVal) Call SecondRoutine(sOther)However, the call to the second may actually presuppose on the successful completion ofthe first. In this case, it is safer to implement the two routines as functions that return aBoolean value to denote the success or failure of the procedure, as follows: If DoFirstRoutine(sVal) Then SecondRouting sOther End IfThe only time you should need to write code within a sub procedure is as an eventhandler; otherwise, use a function and return a value (at least a Boolean). The callingstatement can then choose whether to use or ignore the return value from your function.

12. Never Assume the Unusual Won't HappenI love this story. It's true (and very recent). One weekend (like most in the summer), wewere off racing and had to stay over in a hotel, part of a major UK chain. The kids aregetting a bit older now, so we decided that they could have their own room. When I'dfilled out the form for the first room and signed in, the lady behind the check-in desk said,"I'm sorry, but can we put your wife down as the occupier of the second room? It's justthat our system won't let us use the same surname and initial for two records on the sameday. We need a different initial."Whose bright idea was it that the primary key on the day table should be last name and

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

Page 175: VB1

Helpmate

initial?OK, perhaps it's unusual, but what about father and adult son, both paying for their roomsindividually 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 notmean that it's impossible. Unusual, etc., means that it can happen. And as sure as eggs areeggs, it's going to happen, and it'll happen to your system! Sound programming is notabout gambling on probabilities, its about banking on certainties!

13. Always Use the Event Log to Log ErrorsOne of the neat things about NT is its event log. Wherever possible (and I can't think of asituation where it isn't), you should add a line of code (or two) into your error handlingroutine to write a line out to the event log.Even if your application is unfortunate enough to have to run on Windows 9x, you canstill 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 thenetwork and (if you have administrator rights) keep your eye on the event logs of yourusers for potential problems. No more having to rely on your users giving you the exacterror message that was shown on the screen.You will soon find that, with the use of the event log, once difficult to track errors areeasy to trace, and even errors that may be caused by other sometimes "unseen" causes arepinpointed quickly and accurately.

14. Navigate Effectively with SHIFT+F2 and CTRL+SHIFT+F2One of the confusing aspects of developing a large project with Visual Basic is that codesnippets tend to be in any of a variety of places, and keeping track of what's where - notto 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'llbe transported as if by magic to the highlighted property or procedure. (If it's outside theproject, you'll be taken to the Object Browser, where you can obtain further informationabout it.)You can navigate back to where you came from just as easily. This time hitCTRL+SHIFT+F2 and you're back at your original place. This can save hours and hoursof scrolling and jumping from one module to another in a large project.

15. Fast Combo Population from Objects with VB6One of the most common tasks we have to undertake when creating a user form is toprovide a dropdown list of values based on property values within one of our objects. Acombo control, for example, might contain the names of all the employees in a certaindivision of the company.One of the most common methods of doing this (until VB6) was to populate thecollection object, then enumerate each object in the collection and assign the particularvalue from each object into the combo control. The code looks something like this: Set oEmployees = New Employees For i = 1 to oEmployees.Count

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

Page 176: VB1

Helpmate

If oEmployees.Exists(i) Then Set oEmployee = oEmployees.Employee(i) cboEmployees.AddItem oEmployee.Name Set oEmployee = Nothing End If Next i Set oEmployees = NothingThe amount of processing and (if an object is remote) the massive amount of networktraffic that this simple procedure can create is staggering! So when I found that functionscan return arrays in VB6, I was delighted. This means that you can now execute a simpleSQL query that returns to the client only an array containing the values of that field thatyou want to display in the list or combo box. Here's the server side code: Public Property Get Names() As String()

ReDim sTemp(0) As String Dim oADORec As ADODB.Recordset Dim lRecCount As Long Dim sSQL As String Dim iCounter As Integer

Set oUtil = New PowerUtils.DBUtils sSQL = "SELECT SName from Employees" Set oADORec = oUtil.GetRecordset(sSQL) If oADORec.RowCount > 0 Then ReDim sTemp(oADORec.RowCount) Do While Not oADORec.EOF sTemp(iCounter) = oADORec!SName iCounter = iCounter + 1 oADORec.MoveNext Loop End If Set oADORec = Nothing Set oUtil = Nothing Names = sTemp

End PropertyIf you can't get the RowCount property to work with your system, it is faster to execute aSQL Query that returns the row count and then do a single ReDim of the array, rather thanto use ReDim Preserve.Here's how the Names property shown above is used at the client end: Dim sNames() As String Dim vName As Variant Dim oEmps As Employees

Set oEmps = New Employees sNames = oEmps.Names For Each vName In sNames cboEmployees.AddItem vName Next Set oEmps = NothingNote that only an array of string values is passed from the server object to the client.Therefore, the amount of traffic between the two objects is negligible. Also note that toassign an array from a function in VB6, your local variable must be dimensioned as a

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

Page 177: VB1

Helpmate

dynamic array.

Ten Tips for Taking the Visual Basic Exams

Obtaining Microsoft certification is a rewarding achievement. However, it can also be acostly and time-consuming process. (To get the MCSD certification you must pass fourexams. The two Visual Basic exams can be used as core exams or as electives.) In orderto pass the Visual Basic exams and become certified, you need to be proficient with allaspects of the Visual Basic language. But because Visual Basic is such a rich andcomplex language, even the most die-hard Visual Basic developer doesn't use all theavailable features.

Although Microsoft doesn't release exam statistics, it's believed that nearly half thepeople who take an exam for the first time fail it. Even competent developers fail examsat times, not due to lack of knowledge or experience, but due to lack of preparation. It'simpossible to teach you everything you need to know to pass the exams in a top ten list(that's why I wrote MCSD in a Nutshell), but this list will help prepare you for taking theVisual Basic exams.

1. Know the requirements of each of the exams. It seems trivial, but manydevelopers don't consider all of the objectives listed for an exam, and thereforethey 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 morequestions. 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, andprobably more. If you don't spend a few minutes learning the basics of theTreeView control prior to taking an exam, you're going into the battle withoutyour guns fully loaded. You can obtain the latest exam objectives at Microsoft'sCertification site.

2. Be familiar with all of M icrosoft's technologies. Back in the days of VisualBasic 3, the exam (there was only one at the time) focused on your programmingabilities: Could you create a For.Next loop? Could you define a Select Caseconstruct? However, times have changed. The exams now make you demonstratea working knowledge of Microsoft technologies. You can actually write prettypoor code and still be certified, as long as you understand Microsoft technologies.It's doubtful that you use all of Microsoft's various technologies, such as forcreating ActiveX controls or ActiveX documents, but you must understand all ofthe technologies in order to pass the exams. For example, ActiveX documentshave long been considered a solution looking for a problem, and very few peopleuse them. However, there are a surprising number of questions related to creatingActiveX documents on both exams. If you don't understand the differencebetween in-process .DLLs and out-of-process .EXEs, you're in trouble. Beforetaking the exams, make sure you understand COM (including early and latebinding and reference counting), and creating and testing ActiveX codecomponents, controls, and user documents. Also make sure you have some

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

Page 178: VB1

Helpmate

understanding of ADO (though more is required for the Distributed exam). Themore you understand about the implementation and coordination of the Microsofttechnologies, the better chance you have of passing the exams.

3. Learn the Internet programming controls. Not everyone programs for theInternet, and of those that do, not everyone uses Visual Basic's intrinsic tools suchas the Internet Transfer control, the Winsock control, or DHTML. However, youhave to understand Visual Basic's Internet programming functionality in order topass 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 sureyou 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 toearn a right on the top ten list, (after all it's handled by a simple wizard), but letme tell you: become an expert at packing and deploying solutions and you willdramatically improve your performance on both exams. When I first took theexams in beta, I was unpleasantly surprised at how many questions regardingpackage and deployment appeared on both exams. I took the exams once againafter they were released to see how things changed so that I could keep mymaterial accurate. What I found is that both exams were still laden with packageand deployment questions. I personally think that much of the material is moresuited to the Distributed exam, but the reality is that both exams have includemany questions on package and deployment; if you don't fully understandpackage and deployment (.CAB files, Internet deployment, and installation scriptfiles, 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 highschool, would you have studied more? Regardless of how proficient you are withVisual Basic, your chances of passing an exam are greatly increased if you simplyspend 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 onconcepts that you may not use all the time. If you've been programmingapplications with Visual Basic for some time, you can probably create truly robustapplications quite easily. However, how much of your coding is 'raw' codingversus cutting and pasting from a vast library of routines? You may have a greatclass that encapsulates all of the functionality for working with the ListViewcontrol, 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 andpassing a Microsoft exam is an investment in your future, treat it as such asdevote some time to studying before taking an exam.

6. Take a Practice Exam. If you've never taken a Microsoft exam, you shoulddownload a practice exam from Microsoft's Web site. The practice exam won'tteach you want you need to know, but it will help you get familiar withMicrosoft's testing software and approach, which is a big plus in my opinion.

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

Page 179: VB1

Helpmate

When you take an exam, you want to focus your efforts on answering questions,not on getting comfortable with the testing software. I highly recommendpurchasing a Transcender practice test for the exam you want to take, in additionto whatever other study materials you may use (<shameless_plug>MCSD in aNutshell<shameless_plug>). The Transcender exams won't teach you all you needto know, but they do an excellent job in preparing you for the types of questionsyou will be asked, and in the manner in which the questions appear. When youfirst sit down to take an exam, you are given the opportunity to take a practiceexam. The time used to take the practice exam does not count against yourallotted time for the real exam, so you should go ahead and take the practice examto get acquainted (or reacquainted) with the testing software.

7. Read questions completely, and read them more than once. Know this: Thequestions on the exams are worded to deliberately bait you into making mistakes.Questions are often cleverly worded such that the question is actually asking theopposite of what you think it is. In these cases, it's not uncommon for the firstanswer to appear to be the correct answer, when in reality the correct answerappears later in the list. If you read the question wrong the first time (which isvery easy to do), you'll formulate the answer in your head, see the answer listed asthe first choice, answer the question, and move on. There are so many questionsworded 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 readthe 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 somethingaltogether different than what I thought the first time I read it?" If you do this forall questions (it takes only a second or so to do per question), you will greatlyreduce the amount of mistakes you make on an exam.

8. Use the Marking feature of the exam to your advantage. Microsoft's testingsoftware has a Marking feature that lets you flag questions for easy review at alater time. If you're stuck on a question, select your best-guess answer, use theMarking 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 allof the test question numbers, with those you've marked highlighted. To review aquestion, double-click the question number. During this review phase, chooseyour best answer and unmark the question. If at all possible, you want to finishthe exam with no questions marked so that you know you revisited all of thequestions that gave you trouble. The reason that you make a best guess at the timeyou mark the question is so that, in the event you do run out of time, you have atleast given an answer; by not answering a question, you're assured of getting itwrong.

9. Use all of the time you are given. You are given a finite amount of time tocomplete an exam. If you're prepared, the time will be more than adequate, andyou'll be left with some time to spare. Use all of the time allotted. If you'veanswered all of the questions, review any questions you may have marked usingthe exam software's Marking feature. If you've already reviewed the marked

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

Page 180: VB1

Helpmate

questions, review all of the questions from the beginning. During this stage, thereare really two key things to keep in mind. The first is that it's possible you mayhave encountered a question later in the exam that gave you enough informationto more accurately answer the question you're reviewing. The second thing tokeep in mind is the idea of rereading a question from the perspective of "did Iunderstand the question correctly when I answered it?" Sometimes coming backto a question allows your mind to process the information and put you in a betterstate 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 paperMCSE or paper MCSD is someone who has obtained certification, but doesn'thave the skills to back it up. This phenomenon has proliferated in a large part dueto 'brain dumps,' Web sites where users post questions and answers that theyremember after taking the exams. This information, coupled with good studymaterials, such as study guides and practice tests, has allowed some people topass tests on subjects in which they really aren't proficient. Unfortunately, this hasnegative consequences in many ways. First and foremost, it dilutes the value ofcertification for those who have actually earned it. Microsoft recognizes this, andas a result they are changing the way they administer exams. If you've taken theSolutions 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-worldexperience. Passing an exam without having real experience in the subject is very,very difficult to do--but not impossible. However, certification has little value ifyou can't walk the walk. In addition, the exams are evolving, and I believe thatsoon 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 isto build applications using Visual Basic.

Executing Common SQL Coding Tasks Using FunctionCalls

One of the great things about RDMS (Relational Database Management Systems) is itsunmatched flexibility in addressing a number of different work requirements. This articleshows examples of several common work requirements that are answered using functioncalls. 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 thataverages a list of database values. But many functions do not use any type of inputparameter, 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 aNutshell. There are a great many function calls that are universally supported by theANSI (American National Standards Institute) standard and all the database vendors. Forexample, most vendors support the commonly used aggregate functions of SUM, AVG,

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

Page 181: VB1

Helpmate

MIN, and MAX. These functions extract summary value, average value, and minimum ormaximum value from a column or an expression, respectively. There are also a wholevariety of functions that are not universally supported, such as RPAD and LPAD orSUBSTRING versus SUBSTR.

Although this article discusses database implementations by Microsoft SQL Server,MySQL, Oracle, and PostgreSQL, this information represents just the tip of the icebergon how business operations accomplish common, everyday SQL coding tasks usingfunctions calls. As you will see, these functions can vary widely.

Date OperationsThis first set of examples show how to query the database for common date-processingoperations using functions. To get the current date and time:Microsoft SQL Server SELECT GETDATE() GOMySQL [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 differentlyusing its own proprietary function calls. Microsoft SQL Server uses a SELECT statementcalling the GETDATE() function. MySQL has two different function calls: CURDATE() andNOW(). 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_DATEfunction 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()) GOMySQL 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 syntaxDATEPART(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 acolumn containing a date or an actual date value), as the second part. MySQL offers theDAYNAME(date_expression) as its function of choice for finding the day of the week for agiven date value. Oracle requires that the date be converted into a character value usingTO_CHAR, but allows the application of a format mask that returns the data of the week

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

Page 182: VB1

Helpmate

value. Conversions of this type in Oracle follow the syntax TO_CHAR(conversion_expression,'datetype'). In this case, TO_CHAR can be used to convert any other datatype to characterdatatype, including INT and DATE datatypes. PostgreSQL accomplishes date conversionusing the DATE_PART function to extract the day of the week from the date expression.The syntax is DATE_PART('text', timestamp), where text defines how the date is returned (inour example, as a day of the week), and timestamp defines the date expression.Sometimes an application needs to know how far two dates are from one another. Todetermine how far away a date is from the current date (or any other date for that matter),either in the future or in the past, use these examples:Microsoft SQL Server SELECT DATEDIFF(dd, '1/1/01', GETDATE()) GOMySQL SELECT FROM_DAYS(TO_DAYS(CURDATE()) - TO_DAYS('2001-11-25'));Oracle SELECT TO_DATE('25-Nov-2000','dd-mon-yyyy') - TO_DATE('25-Aug-1969','dd-mon-yyyy') FROM dual;PostgreSQL SELECT AGE(CURRENT_DATE, '25-Aug-1969');Measuring the time span between two dates is a procedure best left to procedure calls.But again, the syntax varies widely between the vendors. Microsoft uses the DATEDIFFfunction to measure the time span between two dates (in the example, between January 1,2001 and today's date). The syntax is DATEDIFF(datetype, start_date, end_date), wheredatetype is a code representing how the time span should be represented (days, weeks,months, and so on), the start_date is the date to measure from, and the end_date is the date tomeasure to. MySQL must use the somewhat more cumbersome FROM_DAYS andTO_DAYS functions in a nested format to tell the time span between two dates. Oraclevery neatly allows date addition and subtraction. The only reason the TO_DATE functionis even needed is that the operation is being performed on character strings. If theoperation were performed against two columns of DATE datatype, then no TO_DATEconversion would be necessary and the subtraction operation would act directly on thedate expression. PostgreSQL has a cool function called AGE(start_date, end_date) that tellsthe time span between two passed dates as parameters.It is common procedure to retrieve a date in a different format mask (Mon, DD, YYYY;mm/dd/yy; dd/mm/yy; etc.). Here are some examples:Microsoft SQL Server SELECT CONVERT(VARCHAR(11), GETDATE(), 102) GOMySQL SELECT DATE_FORMAT( "2001-11-25", "%M %e, %Y");Oracle SELECT TO_CHAR(SYSDATE,'dd-Mon-yyyy hh:mi:ss PM') FROM dual;PostgreSQL SELECT TO_CHAR (timestamp(CURRENT_DATE), 'dd-Mon-yyyy hh:mi:ss PM');

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

Page 183: VB1

Helpmate

In these examples, the vendors use specialized function calls to retrieve date expressionsin a specific format. Microsoft SQL Server uses the CONVERT function (though CASTcould 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, thesource_expression is the source that will be converted, and the date_format is a set of codesthat Microsoft has set aside for specific date format masks. MySQL uses theDATE_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 thesource_expression must be enclosed within the time-stamp subfunction, as shown in theexample above.

Str ing OperationsOften, an application may need to find one string within another string. This is one wayof performing this operation across the different vendors:Microsoft SQL Server SELECT CHARINDEX('eat', 'great') GOMySQL 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 otherstrings. In this example, it will return the starting position of one string, 'eat,' withinanother, 'great.' The syntax is CHARINDEX(search_string, searched_string, [starting_position]).MySQL and PostgreSQL both accomplish a similar operation using the POSITIONfunction, 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 SQLoperation:Microsoft SQL Server SELECT LTRIM(' sql_in_a_nutshell'), SELECT RTRIM('sql_in_a_nutshell '), SELECT LTRIM(RTRIM(' sql_in_a_nutshell ') GOMySQL SELECT LTRIM(' sql_in_a_nutshell'), SELECT RTRIM('sql_in_a_nutshell '), SELECT TRIM(' sql_in_a_nutshell '), SELECT TRIM(BOTH FROM ' sql_in_a_nutshell ');Oracle SELECT LTRIM(' sql_in_a_nutshell'), SELECT RTRIM('sql_in_a_nutshell '), TRIM(' sql_in_a_nutshell ') FROM dual;PostgreSQL SELECT TRIM(LEADING FROM ' sql_in_a_nutshell'),

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

Page 184: VB1

Helpmate

TRIM(TRAILING FROM 'sql_in_a_nutshell '), TRIM(BOTH FROM ' sql_in_a_nutshell ');Microsoft SQL Server uses the LTRIM and RTRIM functions to remove spaces from theleft or right side of an expression, respectively. When trimming spaces on both sides ofan expression in Microsoft SQL Server, the LTRIM function must encapsulate the RTRIMfunction (or vice versa). MySQL and Oracle both use LTRIM and RTRIM, but differ fromSQL Server in that spaces can be trimmed from both sides of an expression with the TRIMfunction. MySQL also allows TRIM with the BOTH operator to indicate that both left andright sides of the expression should be trimmed. PostgreSQL uses only the TRIM functionand controls whether the left, right, or both sides should be trimmed using the LEADING,TRAILING, and BOTH operators, as shown in the example above.The opposite of trimming spaces is to pad them into an expression. To pad in x number oftrailing or leading spaces with the various vendors:Microsoft SQL Server Not supportedMySQL SELECT LPAD('sql_in_a_nutshell', 20, ' '),

RPAD('sql_in_a_nutshell', 20, ' ');Oracle SELECT LPAD(('sql_in_a_nutshell', 20, ' '),

RPAD(('sql_in_a_nutshell', 20, ' ') FROM dual;PostgreSQL SELECT LPAD('sql_in_a_nutshell', 20, ' '),

RPAD('sql_in_a_nutshell', 20, ' ');In this example, the supporting vendors all use LPAD to insert spaces (or a characterexpression) on the left side of a string expression and RPAD to put them on the right. Thesyntax for MySQL, Oracle, and PostgreSQL is xPAD('string_expression1', length,'string_expression2'), where string_expression1 is the string to have characters padded, length isthe total length of the string, and string_expression2 is the characters to pad out.An operation similar to pad is to substitute characters within a string with othercharacters:Microsoft SQL Server [returns 'wabbit_hunting_season'] SELECT STUFF('wabbit_season', 7, 1, '_hunting_') GOMySQL [returns 'wabbit_hunting_season'] SELECT REPLACE('wabbit_season','it_','it_hunting_');Oracle [returns 'wabbit_hunting_season'] SELECT REPLACE('wabbit_season','it_','it_hunting_') FROM dual;PostgreSQL SELECT TRANSLATE('wabbit_season','it_','it_hunting_');Microsoft SQL Server uses the STUFF function to overwrite existing characters. Usingthis syntax, STUFF(string_expression, start, length, replacement_characters), string_expression is thestring that will have characters substituted, start is the starting position, length is thenumber of characters in the string that are substituted, and replacement_characters are thenew characters interjected into the string. MySQL and Oracle both use the function call

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

Page 185: VB1

Helpmate

REPLACE, using the syntax REPLACE(string_expression, search_string, replacement_string), whereevery incidence of search_string found in the string_expression will be replaced withreplacement_string. PostgreSQL uses the TRANSLATE function as a synonym of REPLACE.Many times, a SQL statement must retrieve only a portion of a string. The followingexamples show how to extract 'duck_season' from the string 'wabbit_duck_season' foreach vendor:Microsoft SQL Server SELECT SUBSTRING('wabbit_duck_season', 7, 11) GOMySQL SELECT SUBSTRING('wabbit_duck_season', 7, 11);Oracle SELECT SUBSTR('wabbit_duck_season', 7, 11) FROM dual;PostgreSQL SELECT SUBSTR('wabbit_duck_season', 7, 11);In each example, the syntax for SUBSTRING (or SUBSTR) is essentially the same:SUBSTRING(string_expression, start, length), where string_expression is the expression or columnto be searched, start is an integer telling the starting position, and length is an integertelling the database how many characters to extract.Some vendors allow function calls that can structure an IF, THEN, ELSE result set withinthe query:Microsoft SQL Server SELECT CASE WHEN foo = 'hi' THEN 'there' WHEN foo = 'good' THEN 'bye' ELSE 'default' END FROM t2 GOMySQL N/AOracle SELECT DECODE (payments_info,'CR','Credit','DB','Debit', null) FROM dual;PostgreSQL SELECT CASE WHEN foo = 'hi' THEN 'there' WHEN foo = 'good' THEN 'bye' ELSE 'default' END FROM t2;Microsoft SQL Server and PostgreSQL support the powerful ANSI SQL command CASE.CASE has two usages: simple and searched. Simple CASE expression compares one value,the input_value, with a list of other values and returns a result associated with the firstmatching value. Searched CASE expressions allow the analysis of several logicalconditions and returns a result associated with the first one that is true.Simple comparison operation

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

Page 186: VB1

Helpmate

CASE input_value WHEN when_condition THEN resulting_value [...n] [ELSE else_result_value] ENDBoolean searched operation CASE WHEN Boolean_condition THEN resulting_value [...n] [ELSE else_result_expression] ENDIn the simple CASE function, the input_value is evaluated against each WHEN clause. Theresulting_value is returned for the first TRUE instance of input_value = when_condition. If nowhen_condition evaluates as TRUE, the else_result_value is returned. If no else_result_value isspecified, then NULL is returned.In the more elaborate Boolean searched operation, the structure is essentially the same asthe simple comparison operation except that each WHEN clause has its own Booleancomparison operation. In either usage, multiple WHEN clauses are used, although onlyone 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 issuccessful, the corresponding result is returned. In our example, when returning a resultset from the payments_info column, any incident of 'CR' will be replaced with 'Credit,' anyinstance of 'DB' will be replace with 'Debit,' and any other values will be replaced with adefault value of Null.

Nulls OperationsNulls are sometimes tricky business. Sometimes a company process, such as a query orother data manipulation statement, must explicitly handle NULLs. These examples showhow to return a value specified when a field or result is null:Microsoft SQL Server SELECT ISNULL(foo, 'Value is Null') GOMySQL N/AOracle SELECT NVL(foo,'Value is Null') FROM dual;PostgreSQL [allows you to write a user-defined function to handle this feature] N/AMicrosoft SQL Server uses the ISNULL function following the syntaxISNULL(string_expression, replacement_value), where string_expression is a string or columnbeing searched and replacement value is the value returned if string_expression is NULL. Oracleuses 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 aspecific value:Microsoft SQL Server [returns NULL when foo equates to 'Wabbits!']

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

Page 187: VB1

Helpmate

SELECT NULLIF(foo, 'Wabbits!') GOMySQL N/AOracle SELECT DECODE(foo,'Wabbits!',NULL) FROM dual;PostgreSQL SELECT NULLIF(foo, 'Wabbits!');Aside from using CASE or DECODE to solve this problem, Microsoft and PostgreSQLallow use of the NULLIF function. The syntax is NULLIF(expression1, expression2), which tellsthe database that if expression1 equals expression2, then returns a NULL value.

SummaryThere are a great many function calls that are universally supported by the ANSI standardand all the database vendors. This article has shown a variety of useful function callsavailable in database implementations by Microsoft SQL Server, MySQL, Oracle, andPostgreSQL. For more details on ANSI standard functions, check out our book, SQL in aNutshell.

Kevin K line serves as the lead information architect for shared information services atDeloitte & Touche LLP. In addition to coauthoring SQL in a Nutshell, Kevin is thecoauthor of Transact-SQL Programming, also for O'Reilly & Associates. He coauthoredProfessional SQL Server 6.5 Administration (WROX Press), and is also the author ofOracle CDE: Reference and User's Guide (Butterworth-Heinemann). He can be reachedat [email protected].(Editor's Note: We tried to link directly to Kevin's book published by BH, however the BHsite does not produce urls that work when copied.)Daniel K line 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 highereducation.

Win32 API Programming with Visual Basic

Chapter 6Str ingsThe subject of strings can be quite confusing, but this confusion tends todisappear with some careful attention to detail (as is usually the case). Themain problem is that the term string is used in at least two different waysin Visual Basic!

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

Page 188: VB1

Helpmate

Just what is a string in Visual Basic? According to the VB documentation,it is:

A data type consisting of a sequence of contiguouscharacters that represent the characters themselves ratherthan their numeric values.

Huh?It seems to me that Microsoft is trying to say that the underlying set for theString data type is the set of finite-length sequences of characters. ForVisual Basic, all characters are represented by 2-byte Unicode integers.Put another way, VB uses Unicode to represent the characters in a string.For instance, the ASCII representation for the character h is &H68, so theUnicode representation is &H0068, appearing in memory as 68 00.Thus, the string "help" is represented as:

00 68 00 65 00 6C 00 70Note, however, that because words are written with their bytes reversed inmemory, the string "help" appears in memory as:

68 00 65 00 6C 00 70 00This is fine, but it is definitely not how we should think of strings in VBprogramming. To avoid any possibility of ambiguity, we will refer to thistype of object as a Unicode character array which is, after all, preciselywhat it is! This also helps distinguish it from an ANSI character array,that is, an array of characters represented using single-byte ANSIcharacter codes.Here is the key to understanding strings: when we write the code:

Dim str As Stringstr = "help"

we are not defining a Unicode character array per se. We are defining amember of a data type called BSTR, which is short for Basic String. ABSTR is, in fact, a pointer to a null-terminated Unicode character arraythat is preceeded by a 4-byte length field. We had better elaborate on this.

The BSTRActually, the VB string data type defined by:

Dim str As Stringunderwent a radical change between versions 3 and 4 of Visual Basic, duein part to an effort to make the type more compatible with the Win32operating system.Just for comparison (and to show that we are more fortunate now), Figure6-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 stringdescriptor, which contains the 2-byte length of the string along withanother pointer to the character array, which is in ANSI format (one byteper character).

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

Page 189: VB1

Helpmate

With respect to the Win32 API, this string format is a nightmare.Beginning with Visual Basic 4, the VB string data type changed. The newdata type, called a BSTR, is shown in Figure 6-2.This data type is actually defined in the OLE 2.0 specifications; that is, itis part of Microsoft's ActiveX specification.There are several important things to note about the BSTR data type.

• The BSTR is the actual pointer variable. It has size 32 bits, like allpointers, and points to a Unicode character array. Thus, a Unicodecharacter array and a BSTR are not the same thing. It is correct torefer to a BSTR as a string (or VB string) but, unfortunately, theUnicode character array is also often called a string! Hence, wewill not refer to a BSTR simply as a string--we will refer to it byits unequivocal name--BSTR.

• The Unicode character array that is pointed to by a BSTR must bepreceded by a 4-byte length field and terminated by a single null 2-byte character (ANSI = 0).

• There may be additional null characters anywhere within theUnicode character array, so we cannot rely on a null character tosignal the end of the character array. This is why the length field isvital.

• Again, the pointer points to the beginning of the character array,not to the 4-byte length field that precedes the array. As we willsee, this is critical to interpreting a BSTR as a VC++-style string.

• The length field contains the number of bytes (not the number ofcharacters) in the character array, excluding the terminating nullbytes. Since the array is Unicode, the character count is one-halfthe byte count.

We should emphasize that an embedded null Unicode character is a 16-bit0, not an 8-bit 0. Watch out for this when testing for null characters inUnicode arrays.Note that it is common practice to speak of "the BSTR `help'" or to saythat a BSTR may contain embedded null characters when what is reallybeing referred to is the character array pointed to by the BSTR.Because a BSTR may contain embedded null characters, the terminatingnull is not of much use, at least as far as VB is concerned. However, itspresence is extremely important for Win32. The reason is that the Unicodeversion of a Win32 string (denoted by LPWSTR) is defined as a pointer toa null-terminated Unicode character array (which, by the way, is notallowed to contain embedded null characters).This makes it clear why BSTR's are null terminated. A BSTR with noembedded nulls is also an LPWSTR. We will discuss C++ strings in amoment.Let us emphasize that code such as:

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

Page 190: VB1

Helpmate

Dim str As Stringstr = "help"

means that str is the name of a BSTR, not a Unicode character array. Inother words, str is the name of the variable that holds the address xxxx, asshown in Figure 6-2.Here is a brief experiment we can do to test the fact that a VB string is apointer to a character array and not a character array. Consider thefollowing code, which defines a structure whose members are strings:

Private Type utTest astring As String bstring As StringEnd Type Dim uTest As utTestDim s as String s = "testing"uTest.astring = "testing"uTest.bstring = "testing" Debug.Print Len(s)Debug.Print Len(uTest)

The output from this code is:78

In the case of the string variable s, the Len function reports the length ofthe character array; in this case there are 7 characters in the character array`testing'. However, in the case of the structure variable uTest, the Lenfunction actually reports the length of the structure (in bytes). The returnvalue of 8 clearly indicates that each of the two BSTRs has length 4. Thisis because a BSTR is a pointer!

C-Style LPSTR and LPWSTR Str ingsVC++ and Win32 use the string data types LPSTR and LPWSTR.An LPSTR string is defined as a pointer to a null-terminated ANSIcharacter array. However, because the only way that we can tell when anLPSTR string ends is by the location of the terminating null, LPSTRs arenot allowed to contain embedded null characters. Similarly, an LPWSTRis a pointer to a null-terminated Unicode character set with no embeddednulls. (The W in LPWSTR stands for Wide, which is Microsoft's way ofsaying Unicode.) These string data types are pictured in Figure 6-3.We will also encounter the data types LPCSTR and LPCWSTR. Theembedded C stands for constant and simply means that an instance of thisdata type cannot (and will not) be changed by any API function that usesthis type. Otherwise, an LPCSTR is identical to an LPSTR, and, similarly,an LPCWSTR is identical to an LPWSTR.Finally, the generic LPTSTR data type is used in conditional compilation,just like the TCHAR data type, to cover both ANSI and Unicode in asingle source code. Here are the declarations:

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

Page 191: VB1

Helpmate

#ifdef UNICODE typedef LPWSTR LPTSTR; // LPTSTR is synonym for LPWSTRunder Unicodetypedef LPCWSTR LPCTSTR; // LPCTSTR is synonym forLPCWSTR under Unicode #else typedef LPSTR LPTSTR; // LPTSTR is synonym for LPSTR underANSItypedef LPCSTR LPCTSTR; // LPTCSTR is synonym for LPCSTRunder ANSI #endif

Figure 6-4 summarizes the possibilities.Thus, for instance, LPCTSTR is read long pointer to a constant genericstring.

Str ing TerminologyTo avoid any possible confusion, we will use the terms BSTR, Unicodecharacter array, and ANSI character array. When we do use the termstring, we will modify it by writing VB string (meaning BSTR) or VC++string (meaning LP??STR). We will avoid using the term string withoutsome modification.However, in translating VB documentation, you will see the unqualifiedterm string used quite often. It falls to you to determine whether thereference is to a BSTR or a character array.

Tools for Explor ing Str ingsIf we are going to do some exploring, then we will need some tools. Wehave already discussed the CopyMemory API function. Let us take a lookat some additional tools for dealing with strings.

The Visual Basic StrConv FunctionThe StrConv function is used to convert character arrays from one formatto another. Its syntax is:

StrConv(string, conversion, LCID)where string is a BSTR, conversion is a constant (described later), andLCID is an optional locale identifier (which we will ignore).Among the possible constants, and the only ones that interest us, are:

• vbUnicode (which should have been vbToUnicode)

• vbFromUnicode

These constants convert the character array of the BSTR between Unicodeand ANSI.

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

Page 192: VB1

Helpmate

But now we have a problem (which really should have been addresed bythe official documentation). There is no such thing as an ANSI BSTR. Bydefinition, the character array pointed to by a BSTR is a Unicode array.However, we can image what an ANSI BSTR would be--just replace theUnicode character array in Figure 6-2 with an ANSI array. We will use theterm ABSTR to stand for ANSI BSTR, but you should keep in mind thatthis 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 ABSTRStrConv(anABSTR, vbUnicode) ' returns a BSTR

The irony is that, in the first case, VB doesn't understand the return valueof its own function! To see this, consider the following code:

s = "help"Debug.Print sDebug.Print StrConv(s, vbFromUnicode)

The result is:help??

because VB tries to interpret the ABSTR as a BSTR. Look at thefollowing code:

s = "h" & vbNullChar & "e" & vbNullChar & "l" & vbNullChar & "p"& vbNullCharDebug.Print sDebug.Print StrConv(s, vbFromUnicode)

The output is:h e l phelp

Here we have tricked VB by padding the original Unicode character arrayso that when StrConv does its conversion, the result is an ABSTR thathappens to have a legitimate interpretation as a BSTR!This shows that the StrConv function doesn't really understand or careabout BSTRs and ABSTRs. It assumes that whatever you feed it is apointer 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, theycan take a BSTR or an ABSTR--to them it is just a pointer to some null-terminated array of bytes.

The Len and LenB FunctionsVisual Basic has two string-length functions: Len and LenB. Each takes aBSTR 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 thenumber of bytes in the BSTR.

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

Page 193: VB1

Helpmate

The Chr , ChrB, and ChrW FunctionsThese three functions have different input ranges and produce differentoutputs. These differences can seem confusing at first--you may have toread the definitions a few times:

• Chr takes a long value x in the range 0 to 255 and returns a BSTRof length 1. This one character pointed to by the BSTR hasUnicode code equal to x. (In this case, the Unicode and ANSIvalues are actually equal.) Note that, according to the latestdocumentation, there is no difference between Chr and Chr$.

• ChrB takes a long value x in the range 0 to 255 and returns anABSTR of length 1 (byte). This one byte pointed to by the ABSTRhas ANSI code equal to x.

• ChrW takes a long value x in the range 0 to 65535 and returns aBSTR of length 1. This one character pointed to by the BSTR hasUnicode code equal to x.

The Asc, AscB, and AscW FunctionsThese functions are the inverses of the Chr functions. For instance, AscBtakes a single character (byte) ABSTR and returns a byte equal to thecharacter's ANSI code. To see that the return type is a byte, try running thecode:

Debug.Print VarType(AscB("h")) = vbByte(The output is True.) It may appear that AscB will accept a BSTR as input,but in reality, it just takes the first byte in the BSTR.The Asc function takes a BSTR (but not an ABSTR) and returns an integerequal to the character's Unicode code.

Null Str ings and Null CharactersTo its credit, VB does allow null BSTRs. The code:

Dim s As Strings = vbNullStringDebug.Print VarPtr(s)Debug.Print StrPtr(s)

produces the following output (your address may vary, of course): 1243948 0

This shows that a null BSTR is simply a pointer whose contents are 0.(We will discuss the meaning of StrPtr in a moment.) In Win32 andVC++, this is called a null pointer. You can probably see the differencebetween vbNullString and vbNullChar at this point. vbNullChar is not a pointer--it is a Unicode character whose value is 0. Thus, at the bit level, thevalues vbNullString and vbNullChar are identical. However, they areinterpreted differently, so they are in fact different.It is also important not to confuse a null BSTR with an empty BSTR,usually denoted by a pair of adjacent quotation marks:

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

Page 194: VB1

Helpmate

Dim s As StringDim t As Strings = vbNullStringt = ""

Unlike a null string, the empty BSTR t is a pointer that points to somenonzero memory address. At that address resides the terminating nullcharacter for the empty BSTR, and the preceeding length field alsocontains a 0.

VarPtr and StrPtrWe have discussed the function VarPtr already, but not in connection withstrings. The functions VarPtr and StrPtr are not documented by Microsoft,but they can be very useful, so we will use them often, particularly theVarPtr 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 theUnicode character array pointed to by the BSTR.Let us elaborate. Figure 6-5 shows a BSTR.Figure 6-5. A BSTR

The code for this figure is simply:Dim str As Stringstr = "help"

Note that the variable str is located at address aaaa and the character arraybegins at address xxxx, which is the contents of the pointer variable str.To see that:

VarPtr = aaaaStrPtr = xxxx

just run the following code:Dim lng As LongDim i As IntegerDim s As StringDim b(1 To 10) As ByteDim sp As Long, vp As Long s = "help" sp = StrPtr(s)Debug.Print "StrPtr:" & sp vp = VarPtr(s)Debug.Print "VarPtr:" & vp ' Verify that sp = xxxx and vp = aaaa' by moving the long pointed to by vp (which is xxxx)

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

Page 195: VB1

Helpmate

' to the variable lng and then comparing it to spCopyMemory lng, ByVal vp, 4Debug.Print lng = sp ' To see that sp contains address of char array,' copy from that address to a byte array and print' the byte array. We should get "help".CopyMemory b(1), ByVal sp, 10For i = 1 To 10 Debug.Print b(i);Next

The output is:StrPtr:1836612VarPtr:1243988True 104 0 101 0 108 0 112 0 0 0

This shows again that the character array in a BSTR is indeed in Unicodeformat. Also, by adding the following lines:

Dim ct As LongCopyMemory ct, ByVal sp - 4, 4Debug.Print "Length field: " & ct

just after the lines:sp = StrPtr(s)Debug.Print "StrPtr:" & sp

we get the output:Length field: 8

which shows that the length field does indeed hold the byte count and notthe character count.As mentioned earlier, if you do not like to use undocumented functions(and who can blame you for that?), you can use the function rpiVarPtr inthe rpiAPI.dll library on the accompanying CD. You can also simulateStrPtr as follows:

' Simulate StrPtrDim lng As LongCopyMemory lng, ByVal VarPtr(s), 4' lng = StrPtr(s)

As we have seen, this code copies the contents of the BSTR pointer, whichis the value of StrPtr, to a long variable lng.

Str ing Conversion by VBNow we come to the strange story on how VB handles passing BSTRs toexternal DLL functions. It doesn't.As we have seen, VB uses Unicode internally; that is, BSTRs use theUnicode format. Window NT also uses Unicode as its native charactercode. However, Windows 9x does not support Unicode (with someexceptions). Let's examine the path that is taken by a BSTR argument toan external DLL function (Win32 API or otherwise).In an effort to be compatible with Windows 95, VB always (even whenrunning under Windows NT) creates an ABSTR, converts the BSTR'sUnicode character array to ANSI, and places the converted characters in

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

Page 196: VB1

Helpmate

the ABSTR's character array. VB then passes the ABSTR to the externalfunction. As we will see, this is true even when calling the Unicode entrypoints under Windows NT.

Prepar ing the BSTRBefore sending a BSTR to an external DLL function, VB creates a newABSTR string at a location different from the original BSTR. It thenpasses that ABSTR to the DLL function. This duplication/translationprocess is pictured in Figure 6-6.Figure 6-6. Translating a BSTR to an ABSTR

When we first introduced the CopyMemory function, we used it todemonstrate this Unicode-to-ANSI translation process. But let's do thatagain in a different way. The rpiAPI.dll library includes a function calledrpiBSTRtoByteArray, whose purpose is to return the values of VarPtr andStrPtr on the string that is actually passed to a DLL function. The VBdeclaration is as follows.

Public Declare Function rpiBSTRtoByteArray Lib "???\rpiAPI.dll" ( _ ByRef pBSTR As String, _ ByRef bArray As Byte, _ pVarPtr As Long, _ pStrPtr As Long) As Long

For its first parameter, this function takes as input a BSTR, which ispassed by reference. Hence, the address of the BSTR is passed, not theaddress of the character array. (Thus, we are passing a pointer to a pointerto the character array.)The second parameter should be set to the first byte of a byte array that thecaller must allocate with enough space to accommodate all of the bytes ofthe BSTR. Failing to do so will definitely crash the application.The last two parameters are OUT parameters, meaning that the caller justdeclares a pair of long variables, which the function will fill in. ThepVarPtr variable will be filled by the address of the BSTR, and the pStrPtrwill be filled by the contents of the BSTR (which, as we know, is theaddress of the character array) as the DLL function sees it. Thus, we willbe able to get a glimpse of what the DLL is actually passed by VB!The function returns the length (in bytes) of the original string. Finally, inorder to convince ourselves that everything is working as it should, thefunction changes the first character of the original string to an X.Here is a test run (the function VBGetTarget was discussed in Chapter 3,API Declarations, under the section "Implementing Indirection in VisualBasic"):

Sub BSTRTest() Dim i As IntegerDim sString As StringDim bBuf(1 To 10) As Byte

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

Page 197: VB1

Helpmate

Dim pVarPtr As LongDim pStrPtr As LongDim bTarget As ByteDim lTarget As Long sString = "help" ' Print the BSTR's initial address and contentsDebug.Print "VarPtr:" & VarPtr(sString)Debug.Print "StrPtr:" & StrPtr(sString) ' Call the external functionDebug.Print "Function called. Return value:" & _ rpiBSTRToByteArray(sString, bBuf(1), pVarPtr, pStrPtr) ' Print what the DLL sees, which is the temp ABSTR' Its address and contents are:Debug.Print "Address of temp ABSTR as DLL sees it: " & pVarPtrDebug.Print "Contents of temp ABSTR as DLL sees it: " & pStrPtr ' Print the buffer pointed to by temp ABSTRDebug.Print "Temp character array: ";For i = 1 To 10 Debug.Print bBuf(i);NextDebug.Print ' Now that we have returned from the DLL function call' check status of the passed string buffer -- it has been deallocatedVBGetTarget lTarget, pVarPtr, 4Debug.Print "Contents of temp ABSTR after DLL returns: " & lTarget ' Check the string for altered characterDebug.Print "BSTR is now: " & sString End Sub

Here is the output:VarPtr:1242736StrPtr:2307556Function called. Return value:4Address of temp ABSTR as DLL sees it: 1242688Contents of temp ABSTR as DLL sees it: 1850860Temp character array: 104 101 108 112 0 0 0 0 0 0Contents of temp ABSTR after DLL returns: 0BSTR is now: Xelp

This code first prints the address (VarPtr ) and the contents (StrPtr ) of theoriginal BSTR as VB sees it. It then calls the function, which fills in thebyte buffer and the OUT parameters. Next, the buffer and OUTparameters are printed. The important point to note is that the address andcontents of the "string," as returned by the DLL function, are differentthan the original values, which indicates that VB has passed a differentobject to the DLL. In fact, the buffer is in ANSI format; that is, the objectis an ABSTR.

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

Page 198: VB1

Helpmate

Next, we print the contents of the passed ABSTR, when the DLL hasreturned. This is 0, indicating that the temporary ABSTR has beendeallocated. (It is tempting but not correct to say that the ABSTR is nowthe null string--in fact the ABSTR no longer exists!)Finally, note that I am running this code under Windows NT--thetranslation still takes place even though Windows NT supports Unicode.

The Returned BSTRIt is not uncommon for a BSTR that is passed to a DLL function to bealtered and returned to the caller. In fact, this may be the whole purpose ofthe function.Figure 6-7 shows the situation. After the ABSTR is altered by the DLLfunction, the translation process is reversed. Thus, the original BSTR strwill now point to a Unicode character array with the output of the APIfunction. Note, however, that the character array may not be returned toits original location. For instance, as we will see, the API functionGetWindowText seems to move the array. The point is that we cannot relyon the contents of the BSTR to remain unchanged, only its address. Thiswill prove to be an important issue in our discussions later in the chapter.Figure 6-7. The return translation

What to CallSince Windows 9x does not implement Unicode API entry points, forcompatibility reasons you will probably want to call only ANSI API entrypoints in your applications. For instance, you should call SendMessageA,not SendMessageW. (Nonetheless, we will do a Unicode entry pointexample a little later.)

The Whole Str ing Tr ipLet's take a look at the entire round trip that a BSTR takes when passed toan external DLL.Assume that we call a DLL function that takes a string parameter andmodifies that string for return. The CharUpper API function is a goodexample. This function does an in-place conversion of each character inthe string to uppercase. The VB declaration for the ANSI version is asfollows.

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 backand forth, not the actual character arrays:

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

Page 199: VB1

Helpmate

• The BSTR lpsz is duplicated as an ABSTR by VB, and theduplicate is passed to the function CharUpperA, which treats it asan LPSTR.

• This function processes the LPSTR and passes the result to VB.

• VB translates the LPSTR back to a BSTR.

Note that since most API functions (in this case CharUpper) treat BSTRsas LPSTRs, that is, they ignore the length field, we cannot be certain thatthis field will always be accurate. For CharUpper, the length is notchanged, so it should remain correct, but other API functions couldconceivably change the length of the character array. Unless writtenspecifically for the BSTR format, the function will just null-terminate thenew character array, without updating the length field. Thus, we cannotrely on the length field to be valid.

Under Windows NT

Under Windows NT, our string argument will go through the followingmachinations:

1. The string is translated from a BSTR to an ABSTR by VB andpassed to the function CharUpperA, which treats it as an LPSTR.

2. This function translates the LPSTR to an LPWSTR and passes theLPWSTR to the Unicode entry point CharUpperW.

3. The Unicode function CharUpperW processes the LPWSTR andproduces an LPWSTR for output, returning it to CharUpperA.

4. The function CharUpperA translates the LPWSTR back to anLPSTR and passes it to VB, which thinks of it as an ABSTR.

5. VB translates the ABSTR back to a BSTR!

A Unicode Entr y Point ExampleUnder Windows NT, we can call the Unicode entry points and expect toget something meaningful in return. However, VB still makes the BSTR-to-ABSTR translations, and we must counteract this translation. Here isthe ANSI version of a call to CharUpperA:

s = "d:\temp"Debug.Print sCharUpperA sDebug.Print s

Under both Windows 9x and Windows NT, the outcome is as expected:d:\tempD:\TEMP

Under Windows NT, we might first attempt the Unicode version thusly:s = "d:\temp"Debug.Print sCharUpperW s

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

Page 200: VB1

Helpmate

Debug.Print sbut the result is:

d:\tempd:\temp

Clearly, something is wrong. Incidentally, here is what the documentationsays 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; dono [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 ss = StrConv(s, vbUnicode)Debug.Print sCharUpperW sDebug.Print ss = StrConv(s, vbFromUnicode)Debug.Print s

The output is:d:\tempd : \ t e m pD : \ T E M PD:\TEMP

What we are doing here is compensating for the shrinking of our BSTR toan ABSTR by expanding it first. Indeed, the first call to the StrConvfunction simply takes each byte in its operand and expands it to Unicodeformat. 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 is0064 (in hex), which appears in memory as 64 00. Each byte is translated byStrConv to Unicode, which results in 0064 0000 (appearing in memory as 6400 00 00). The effect is to put a null character between each Unicodecharacter in the original Unicode string.Now, in preparation for passing the string to CharUpperW, VB takes thisexpanded string and converts it from Unicode to ANSI, thus returning it toits original Unicode state. At this point, CharUpperW can make sense of itand do the conversion to uppercase. Once the converting string returnsfrom CharUpperW, VB "translates" the result to Unicode, thus expandingit with embedded null characters. We must convert the result to ANSI toremove the supererogatory padding.

Passing Str ings to the Win32 APIWe can now discuss some of the practical aspects of string passing.

ByVal Versus ByRefSome authors like to say that the ByVal keyword is overloaded for strings,meaning that it takes on a different meaning when applied to strings thanwhen applied to other variables. Frankly, I don't see it. Writing:

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

Page 201: VB1

Helpmate

ByVal str As Stringtells VB to pass the contents of the BSTR (actually the ABSTR), which isthe pointer to the character array. Thus, ByVal is acting normally--it justhappens that the content of the BSTR is a pointer to another object, so thissimulates a pass by reference. Similarly:

ByRef str As Stringpasses the address of the BSTR, as expected.

IN and OUT Str ing ParametersThere are many API functions that require and/or return strings. Almost allof these functions deal with C-style strings, that is, LPSTRs or LPWSTRs.Some OLE-related functions do require BSTRs. By way of example, thefollowing function is part of the Microsoft Web Publishing API. Note thatit uses BSTRs. (Note also that the declaration is kind enough to tell uswhich parameters are IN parameters and which are OUT parameters. Thisis all too rare.)

HRESULT WpPostFile( [in] LONG hWnd [in] BSTR bstrLocalPath [in, out] LONG * plSiteNameBufLen [in, out] BSTR bstrSiteName [in, out] LONG * plDestURLBufLen [in, out] BSTR bstrDestURL [in] LONG lFlags [out, retval] LONG * plRetCode );

In general, API functions that use strings can do so in three ways:

• They can require a string as input in an IN parameter

• They can return a string as output in an OUT parameter

• They can do both, either in the same parameter or in separateparameters

To illustrate, Example 6-1 shows three API declarations.

Example 6-1: Three Example Declarations// IN parameter exampleHWND FindWindow( LPCTSTR lpClassName, // pointer to class name LPCTSTR lpWindowName // pointer to window name); // OUT parameter exampleint GetWindowText( HWND hWnd, // handle to window or control with text LPTSTR lpString, // address of buffer for text int nMaxCount // maximum number of characters tocopy); // IN/OUT parameter example

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

Page 202: VB1

Helpmate

LPTSTR CharUpper( LPTSTR lpsz // single character or pointer to string);

The FindWindow function returns a handle to a top-level window whoseclass 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 anOUT parameter lpString. It also returns the number of characters in thetitle as its return value.The CharUpper function converts either a string or a single character touppercase. When the argument is a string, the function converts thecharacters 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 Stringdeclaration, which, as we know, is a BSTR data type. However, there aresome caveats. First, let us talk about the difference between passing aBSTR by value as opposed to by reference.

Dealing with IN ParametersThe 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 thecontents of the parameters (note the C in LPCTSTR), the BSTRs will betreated by Win32 as LPSTRs, which they are. In general, when dealingwith a constant LPSTR, we can use a BSTR.We should also note that FindWindow allows one (but not both) of thesestring parameters to be set, with the remaining parameter set to a null. InWin32, this parameter that the programmer chooses not to supply isrepresented 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 typeof pointer and is treated in this way by Win32.Fortunately, VB has the vbNullString keyword, which is a null BSTR (andso 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 atfirst. Before the introduction of the vbNullString into Visual Basic (I thinkwith VB 4), we would need to do something like:

FindWindow(0&,. . .)to simulate a null string for the first parameter. The problem is that VBwould issue a type mismatch error, because a long 0 is not a string. Thesolution was to declare three separate aliases just to handle the two extra

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

Page 203: VB1

Helpmate

cases of null parameters. With the introduction of vbNullString, thisannoyance went away.To illustrate, in order to get the handle of the window with title "MicrosoftWord - API.doc," we can write:

Dim sTitle As StringDim hnd As LongsTitle = "Microsoft Word - API.doc"hnd = FindWindow(vbNullString, sTitle)

or more simply:Dim hnd As Longhnd = FindWindow(vbNullString, "Microsoft Word - API.doc")

Dealing with OUT ParametersNow consider the second declaration in Example 6-1:

int GetWindowText( HWND hWnd, // handle to window or control with text LPTSTR lpString, // address of buffer for text int nMaxCount // maximum number of characters to copy);

This might be translated to VB as follows:Declare Function GetWindowText Lib "user32" Alias"GetWindowTextA" ( _ ByVal hwnd As Long, _ ByVal lpString As String, _ ByVal cch As Long _) As Long

An HWND is a long value, as is a C-style int (integer). In this case, thestring parameter is an OUT parameter, meaning that the function is goingto fill this string with something useful--in this case, the title of thewindow whose handle is in the hwnd parameter.Here is an example of a call to this function:

Sub GetWindowTitle() Dim sText As StringDim hnd As LongDim cTitle As IntegerDim lngS As Long, lngV As Long ' Allocate string buffersText = String$(256, vbNullChar) ' Save the BSTR and Unicode character array locationslngV = VarPtr(sText)lngS = StrPtr(sText) ' Search for window with a given classhnd = FindWindow("ThunderRT5Form", vbNullString) ' If window found, get titleIf hnd > 0 Then cTitle = GetWindowText(hnd, sText, 255) sText = Left$(sText, cTitle) Debug.Print sText

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

Page 204: VB1

Helpmate

' Compare the BSTR and character array locations' to look for changes

Debug.Print VarPtr(sText), lngV Debug.Print StrPtr(sText), lngSElse Debug.Print "No window with this class name.", vbInformationEnd If End Sub

The output of one run is:RunHelp - Unregistered Copy - Monday, December 7, 199810: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 thisimportant point further in a moment. Then we use FindWindow to searchfor a window with class name ThunderRT5Form--a VB5 runtime form. Ifsuch a window is found, its handle is returned in the hnd parameter. Wecan then call GetWindow-Text, passing it hnd as well as our text buffersText and its size. Since the GetWindowText function returns the numberof characters placed in the buffer, not including the terminating null, thatis, the number of characters in the window title, we can use the Leftfunction to extract just the title from the string buffer.Note also that we have saved both the BSTR address (in lngV) and thecharacter array address (in lngS ), so that we can compare these values tothe same values after calling GetWindowText. Lo and behold, the BSTRhas not moved, but its contents have changed, that is, the character arrayhas moved, as we discussed earlier.Incidentally, since the returned string is null terminated and contains noembedded nulls, the following function also extracts the portion of thebuffer 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 = sNameEnd Function

Getting back to the issue at hand, it is important to understand that, whenOUT string parameters are involved, it is almost always our responsibilityto set up a string buffer, that is, a BSTR that has enough space allocated tohold the data that will be placed in it by the API function. Most Win32API functions do not create strings--they merely fill strings created by thecaller. It is not enough simply to declare:

Dim sText As StringWe must allocate space, as in:

sText = String$(256, vbNullChar)Thus, it is important to remember:

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

Page 205: VB1

Helpmate

When dealing with OUT string parameters, be sure toallocate a string buffer of sufficient size.

Note that in some cases, such as GetWindowText, the function provides anIN parameter for specifying the size of the buffer. This is actually acourtesy to us, in the sense that the function agrees not to place morecharacters in the buffer than we specify as the size of the buffer. (I oftengive the buffer an extra character that the function doesn't know about.Usually, the function includes the terminating null in its reckoning, butwhy take chances?)Note that there are other cases in which no such courtesy is extended, sowe must be careful.Consider the case of SendMessage, for example. Here is part of what theWin32 documentation says about the LB_GETTEXT message, which can beused to retrieve the text of an item in a list box.

An application sends an LB_GETTEXT message toretrieve a string from a listbox.

wParam = (WPARAM) index; // item index [0-based]lParam = (LPARAM) (LPCTSTR) lpszBuffer; // address of buffer

[The parameter lpszBuffer is a] pointer to the buffer thatwill receive the string. The buffer must have sufficientspace for the string and a terminating null character. AnLB_GETTEXTLEN message can be sent before theLB_GETTEXT message to retrieve the length, incharacters, of the string.

Thus, in this case, there is no IN parameter to act as a safety net. If we failto allocate sufficient space in the buffer, the function will write over theend of our buffer, into unknown memory. If we are lucky, this will crashthe program. If we are not lucky, it will overwrite some other data,possibly resulting in logical errors in our program, or crashing a client'sprogram!However, in this case Windows is not completely devoid of compassion. Itdoes provide the LB_GETTEXTLEN message for us to use to first retrievethe length of the item in question. With this value, we can allocate asufficiently capacious buffer. Example 6-2 shows some sample code. Thiscode extracts the items from a listbox (which might belong to some otherapplication) and places them in our listbox lstMain. We will expand thisexample considerably in Chapter 16, Windows Messages. Note the use oftwo different forms of the SendMessage function.

Example 6-2: Using LB_GETTEXTPublic Sub ExtractFromListBox(hControl As Long) Dim cItems As IntegerDim i As IntegerDim sBuf As StringDim cBuf As Long

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

Page 206: VB1

Helpmate

Dim lResp As Long ' Get item count from controlcItems = SendMessageByLong(hControl, LB_GETCOUNT, 0&, 0&) If cItems <= 0 Then Exit Sub ' Put items into list boxFor i = 0 To cItems - 1

' Get length of item cBuf = SendMessageByString(hControl, LB_GETTEXTLEN,CLng(i), vbNullString)

' Allocate buffer to hold item sBuf = String$(cBuf + 1, " ")

' Send message to get item lResp = SendMessageByString(hControl, LB_GETTEXT, CLng(i),sBuf)

' Add item to local list box If lResp > 0 Then Form1.lstMain.AddItem Left$(sBuf, lResp) End If Next i Form1.lstMain.Refresh End Sub

An IN/OUT Parameter Example--Watching Out for As AnyConsider now the third and final function in Example 6-1:

PTSTR CharUpper( LPTSTR lpsz // single character or pointer to string);

One problem here is that, despite the declaration of lpsz as an LPTSTR,the function allows the parameter to be filled with a non-LPTSTR. To wit,the documentation states that the lpsz parameter is a:

Pointer to a null-terminated string or specifies a singlecharacter. If the high-order word of this parameter is zero,the low-order word must contain a single character to beconverted.

For use with string input, we can translate this into VB as:Declare Function CharUpperForString Lib "user32" Alias"CharUpperA" ( _

ByVal lpsz As String _) As Long

This will generally work, as in:' Convert stringstr = "help"Debug.Print StrPtr(str)

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

Page 207: VB1

Helpmate

Debug.Print CharUpperForString(str)Debug.Print str

whose output is: 1896580 1980916HELP

Let us pause for a moment to inspect this output. The CharUpperdocumentation also states:

If the operand is a character string, the function returns apointer to the converted string. Since the string is convertedin place, the return value is equal to lpsz.

On the other hand, the two addresses StrPtr(s) (which is the address of thecharacter array) and CharUpper(s) seem to be different. But remember theBSTR-to-ABSTR translation issue. Our string str undergoes a translationto a temporary ABSTR string at another location. This string is passed tothe CharUpper function, which then changes the string (uppercases it) andalso returns the location of the ABSTR string. Now, VB translates theABSTR back to our BSTR, but it knows nothing about the fact that thereturn value represents the location of the temporary ABSTR, so it returnsthe address of that string!We can confirm this further by calling the Unicode entry point, just as wedid in an earlier example. The following declaration and code:

Declare Function CharUpperWide Lib "user32" Alias "CharUpperW" (_

ByVal lpsz As Long _) As Long ' Construct an LPSTRs = "help"lng = StrPtr(s)Debug.Print lngDebug.Print CharUpperWide(lng)Debug.Print s

returns: 1980916 1980916HELP

Now the two addresses are the same, since no translation occurs!For dealing with characters, we can make the following declaration:

Declare Function CharUpperForChar Lib "user32" Alias"CharUpperA" ( _

ByVal lpsz As Long _) As Long

For instance, calling:Debug.Print Chr(CharUpperForChar(CLng(Asc("a"))))

returns an uppercase A.You might think we could combine the two declarations by using As Any.

Declare Function CharUpperAsAny Lib "user32" Alias "CharUpperA"( _

ByVal lpsz As Any _

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

Page 208: VB1

Helpmate

) As LongThe 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 codecrashes 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 isfatal. Who knows what is at address &H1100? In my case, it is protectedmemory.

What Happened to M y Pointer?There is another, much more insidious problem that can arise inconnection with passing strings to API functions. As we can see from theCharUpper case, the API occasionally uses a single parameter to holdmultiple data types (at different times, of course). Imagine the followinghypothetical 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 symbolicconstant defined somewhere), then lpsz will receive an LPTSTR string(pointer to a character array), but if nFlags has value WO_LENGTH, thenlpsz 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 toWO_LENGTH, then the following events take place under Windows 9x:

1. We create an initial BSTR string buffer for lpsz, say: Dim str AsString str = String$(256, vbNullChar)

2. VB creates a temporary ABSTR to pass to WatchOut, as shown inFigure 6-8.

Figure 6-8. Creating a temporary ABSTR

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

Page 209: VB1

Helpmate

3. As Figure 6-9 shows, because nFlags = WO_LENGTH, WatchOutchanges the pointer, not the character array!

Figure 6-9. Changing the pointer rather than the character array

4. As Figure 6-10 shows, VB tries to translate what it thinks is anANSI character array at address zzzz of length ????. This is adisaster.

Figure 6-10. The resulting broken pointer

Under Windows NT, the WatchOut function changes the original BSTRpointer (instead of an ANSI copy), but this will have the same disastrouseffects. Note that even if we somehow are unlucky enough to escape acrash when VB tries to translate the fraudulent ABSTR, the result will begarbage, the program may crash after we send it to our customers, andthere is still the matter of the dangling string, whose memory will not berecovered until the program terminates. This is called a memory leak.The problem can be summarized quite simply: occasionally an APIfunction will change a string pointer (not the string itself) to a numericvalue. But VB still thinks it has a pointer. This spells disaster. In addition,testing to see whether the contents of the BSTR pointer variable havechanged 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 animportant example, which we will play with at the end of the chapter.The GetMenuItemInfo function retrieves information about a Windowsmenu 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 MENUITEMINFOstructure that will be filled in by GetMenuItemInfo. This structure is:

typedef struct tagMENUITEMINFO { UINT cbSize; UINT fMask; UINT fType; UINT fState; UINT wID; HMENU hSubMenu; HBITMAP hbmpChecked; HBITMAP hbmpUnchecked; DWORD dwItemData; LPTSTR dwTypeData;

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

Page 210: VB1

Helpmate

UINT cch;}

Note that the penultimate member is an LPTSTR.Now, the rpiAPIData application on the accompanying CD willautomatically translate this to a VB user-defined type, replacing all C datatypes 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 '//UINTEnd 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 fMaskparameter to MIIM_TYPE, allocate a suitable string buffer in dwTypeData,and place its length in cch, then the GetMenuItemInfo function willretrieve 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 textof that menu item. However, and this is the problem, if the type isMFT_BITMAP, then the low-order word of dwTypeData gets the bitmap'shandle (and cch is ignored).Thus, GetMenuItemInfo may change dwDataType from an LPTSTR to abitmap handle! This is exactly the problem we described earlier. We willconsider an actual example of this later in the chapter. Keep in mind alsothat even if the type is MFT_TEXT, the dwDataType pointer may bechanged to point to a different character buffer.So if we shouldn't use a string variable for dwDataType, what should wedo?The answer is that we should create our own character array by declaring abyte array and pass a pointer to that array. In other words, we create ourown LPSTR. VB doesn't know anything about LPSTRs, so it will try tointerpret it as a VB string.This even solves the orphaned array problem, for if the API functionchanges our LPSTR to a numeric value (like a bitmap handle), we stillretain a reference to the byte array (we had to create it somehow), so wecan deallocate the memory ourselves (or it will be allocated when the bytearray 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 anumeric value. But VB will still think it has a string. This

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

Page 211: VB1

Helpmate

spells disaster. Moreover, testing to see whether thecontents of the BSTR pointer variable have changed doesn'thelp because VB sometimes changes the original BSTR topoint to a legitimate character array. Hence, if there is achance that this might happen, you should create your ownLPSTR using a byte array and use it in place of the BSTR.For safety, you may want to do this routinely when thestring is embedded within a structure.

The last point made in the caveat is worth elaborating. Oftentimes an APIfunction parameter refers to a structure, whose members may be otherstructures, whose members may, in turn, be other structures. This structurenesting can get quite involved. We will see an example when we createour DLL Export Table application. This makes it very difficult to keeptrack of what the API function might be doing to all of the structuremembers. The safest thing to do is to always use pointers to byte arrays(that is, LPSTRs) and avoid BSTRs completely when dealing with stringsembedded in structures.

Str ings and Byte ArraysOf course, a byte array is just an array whose members have type byte, forinstance:

Dim b(1 to 100) As ByteTo get a pointer to this byte array, we can use VarPtr:

Dim lpsz As Longlpsz = VarPtr(b(1)) ' or rpiVarPtr(b(1))

(Even though it doesn't seem so, the letters lpsz stand for long pointer tonull-terminated string.) Note that the address of the first member of thearray is the address of the array.Remembering that an LPSTR is a pointer to a null-terminated characterarray, we should initialize the array to nulls:

For i = 1 To 100 b(i) = 0Next

(It is true that VB does its own initialization, but it is not goodprogramming practice to rely on this.)

Translating Between Byte Arrays and BSTRsTo copy a BSTR:

Dim s As Stringto a byte array, we can proceed in a couple of different ways. For a strictlyVB solution, we have:

s = "help"Dim b(1 To 8) As ByteFor i = 1 To 8 b(i) = AscB(MidB(s, i))Next

Another approach is:s = "help"

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

Page 212: VB1

Helpmate

Dim b(1 To 8) As ByteCopyMemory b(1), ByVal StrPtr(s), LenB(s)

Note that (in both cases) we get:104 0 101 0 108 0 112 0

showing that the bytes are reversed in each Unicode integer.In the other direction, to copy a byte array into a BSTR, VB gives us somehelp. If b is a Unicode byte array, we can just write:

Dim t As Stringt = b

For an ANSI byte array b, we write:Dim t As Stringt = StrConv(b, vbUnicode)

Note, however, that the StrConv function does not recognize a nullterminator in the byte array--it will translate the entire array. Any nullsthat are encountered in the array become embedded nulls in the BSTR.

Translating Between BSTRs and LPTSTRsLet us consider how to translate back and forth between BSTRs andLPTSTRs.

From BSTR to LPWSTR

Getting a BSTR into a Unicode byte array is conceptually easy, becausethe character array of the BSTR is a Unicode byte array, so all we need todo is copy the bytes one by one. Here is a function to translate BSTRs toLPWSTRs:

Function BSTRtoLPWSTR(sBSTR As String, b() As Byte, lpwsz AsLong) As Long ' Input: a nonempty BSTR string' Input: * *undimensioned** byte array b()' Output: Fills byte array b() with Unicode char string from sBSTR' Output: Fills lpwsz with a pointer to b() array' Returns byte count, not including terminating 2-byte Unicode nullcharacter' Original BSTR is not affected Dim cBytes As Long cBytes = LenB(sBSTR) ' ReDim array, with space for terminating nullReDim b(1 To cBytes + 2) As Byte ' Point to BSTR char arraylpwsz = StrPtr(sBSTR) ' Copy the arrayCopyMemory b(1), ByVal lpwsz, cBytes + 2 ' Point lpsz to new arraylpwsz = VarPtr(b(1)) ' Return byte count

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

Page 213: VB1

Helpmate

BSTRtoLPWSTR = cBytes End Function

This function takes a BSTR, an undimensioned byte array, and a longvariable lng and converts the long to an LPWSTR. It returns the bytecount as the return value of the function. Here is an example:

Dim b() As ByteDim lpsz As Long, lng As Longlng = BSTRToLPWSTR("here", b, lpsz)

It might have occurred to you to simply copy the contents of the BSTR tothe 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 mightdeallocate the array.

From BSTR to LPSTR

The function to convert a BSTR to an LPSTR is similar, but requires atranslation from Unicode to ANSI first:

Function BSTRtoLPSTR(sBSTR As String, b() As Byte, lpsz As Long)As Long ' Input: a nonempty BSTR string' Input: * *undimensioned** byte array b()' Output: Fills byte array b() with ANSI char string' Output: Fills lpsz with a pointer to b() array' Returns byte count, not including terminating null' Original BSTR is not affected Dim cBytes As LongDim sABSTR As String cBytes = LenB(sBSTR) ' ReDim array, with space for terminating nullReDim b(1 To cBytes + 2) As Byte ' Convert to ANSIsABSTR = StrConv(sBSTR, vbFromUnicode) ' Point to BSTR char arraylpsz = StrPtr(sABSTR) ' Copy the arrayCopyMemory b(1), ByVal lpsz, cBytes + 2 ' Point lpsz to new arraylpsz = VarPtr(b(1)) ' Return byte countBSTRtoLPSTR = cBytes End Function

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

Page 214: VB1

Helpmate

From LPWSTR to BSTR

On return from an API call, you may have an LPWSTR, that is, a pointerto a null-terminated Unicode character array. Visual Basic makes it easy toget a BSTR from a byte array--just make an assignment using the equalsign. However, VB doesn't know how to handle a pointer to a byte array.Here is a little utility:

Function LPWSTRtoBSTR(ByVal lpwsz As Long) As String ' Input: a valid LPWSTR pointer lpwsz' Return: a sBSTR with the same character array Dim cChars As Long ' Get number of characters in lpwszcChars = lstrlenW(lpwsz) ' Initialize stringLPWSTRtoBSTR = String$(cChars, 0) ' Copy stringCopyMemory ByVal StrPtr(LPWSTRtoBSTR), ByVal lpwsz, cChars *2 End Function

From LPSTR to BSTR

We can modify the previous utility to return a BSTR from an LPSTR asfollows (recall that Trim0 just truncates a string at the first null character):

Function LPSTRtoBSTR(ByVal lpsz As Long) As String ' Input: a valid LPSTR pointer lpsz' Output: a sBSTR with the same character array Dim cChars As Long ' Get number of characters in lpszcChars = lstrlenA(lpsz) ' Initialize stringLPSTRtoBSTR = String$(cChars, 0) ' Copy stringCopyMemory ByVal StrPtr(LPSTRtoBSTR), ByVal lpsz, cChars ' Convert to UnicodeLPSTRtoBSTR = Trim0(StrConv(LPSTRtoBSTR, vbUnicode)) End Function

Example: Using Byte ArraysLet us demonstrate the use of byte arrays with a simple example usingCharUpper. We have seen that this function is declared as:

LPTSTR CharUpper(

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

Page 215: VB1

Helpmate

LPTSTR lpsz // single character or pointer to string);

This leads to two reasonable translations into VB:Declare Function CharUpperByBSTR Lib "user32" Alias"CharUpperA" ( _ ByVal s As String _) As Long

or:Declare Function CharUpperByLPSTR Lib "user32" Alias"CharUpperA" ( _ ByVal lpsz As Long _) As Long

We have seen the first form in action, so let us try the second form.The following code first converts a BSTR to an LPSTR. Note that weshould not convert to LPWSTR, since LPWSTRs are passed toCharUpperA without translation by VB; and if we passed an LPWSTR,then as soon as CharUpperA encountered the null byte that is part of thefirst Unicode character in the LPWSTR, it would think the string hadended. Thus, it would capitalize only the first character in the string.The LPSTR is then passed to CharUpperA, which converts it touppercase. Having saved the LPSTR pointer, we can check to see if it hasbeen changed. If not, we translate the LPSTR back to a BSTR and print it.If the pointer is changed, then we must deallocate the byte array ourselves(or just let the array variable pass out of scope).Of course, in this simple example, the pointer should not be changed byCharUpper. Nevertheless, this same procedure will deal with APIfunctions that may change the pointer:

Public Sub CharUpperText Dim lpsz As LongDim lpszOrg As LongDim sBSTR As StringDim b() As Byte sBSTR = "help" ' Convert BSTR to LPSTR.BSTRtoLPSTR sBSTR, b, lpsz ' Save LPSTR to check for modification by API functionlpszOrg = lpsz ' Convert to upper caseCharUpperAsLPWSTR lpsz ' If pointer not modified, then convert back to BSTR' and printIf lpszOrg = lpsz Then Debug.Print LPSTRtoBSTR(lpsz)Else Erase b ' Use new value of lpsz if desired...

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

Page 216: VB1

Helpmate

End If End Sub

Example: Windows MenusLet us turn to the example involving GetMenuItemInfo that we promisedearlier. Recall that the GetMenuItemInfo function retrieves informationabout a Windows menu item. Its VB declaration is:

Declare Function GetMenuItemInfo Lib "user32" Alias"GetMenuItemInfoA" ( _ ByVal hMenu As Long, _ ByVal uItem As Long, _ ByVal lByPos As Long, _ ByRef lpMenuItemInfo As MENUITEMINFO _) As Long

where, in particular, the parameter lpmii is a pointer to a MENUITEMINFOstructure that will be filled in by GetMenuItemInfo. This structure is:

Public Type MENUITEMINFO cbSize As Long fMask As Long fType As Long fState As Long wID As Long hSubMenu As Long hbmpChecked As Long hbmpUnchecked As Long dwItemData As Long dwTypeData As Long cch As LongEnd Type

According to the documentation, if we set the fMask parameter toMIIM_TYPE, allocate a suitable string buffer in dwTypeData, and place itslength in cch, then the GetMenuItemInfo function will retrieve the type ofthe menu item into fType. If this type is MFT_TEXT, then the string bufferwill be filled with the text of that menu item. However, and this is theproblem, if the type is MFT_BITMAP, then the low-order word ofdwTypeData gets the bitmap's handle. Thus, in this case,GetMenuItemInfo will change dwTypeData from an LPTSTR to a bitmaphandle!Figure 6-11 shows a menu with a bitmap. We will discuss how to createsuch a menu in VB using the Win32 API in Chapter 21, Bitmaps.Figure 6-11. A menu with a bitmap

Example 6-3 shows the code used to get the text for each of the items inthis menu.

Example 6-3: Getting Menu TextPublic Sub GetMenuInfoExample

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

Page 217: VB1

Helpmate

Const MIIM_TYPE = &H10 ' from WINUSER.HDim uMenuItemInfo As MENUITEMINFODim bBuf(1 To 50) As ByteDim sText As String ' Initialize structureuMenuItemInfo.cbSize = LenB(uMenuItemInfo)uMenuItemInfo.fMask = MIIM_TYPEuMenuItemInfo.dwTypeData = VarPtr(bBuf(1))uMenuItemInfo.cch = 49 ' Get menu textFor i = 0 To 2 ' Must reset count each time uMenuItemInfo.cch = 49 ' Get TypeData before Debug.Print "Before:" & uMenuItemInfo.dwTypeData ' Call API lng = GetMenuItemInfo(hSubMenu, CLng(i), -1, uMenuItemInfo) ' Get TypeData after Debug.Print "After:" & uMenuItemInfo.dwTypeData ' Print text -- CAREFUL HERE' sText = StrConv(bBuf, vbUnicode)' Debug.Print sText Next End Sub

Here is what happens as this code executes.The first loop (i = 0) presents no problems, and the output is:

Before:1479560After:1479560

Observe that the buffer pointer had not changed. Hence, the commentedcode that prints the menu text would run without error.The second loop also runs without error (as long as the statementsinvolving sText are commented out). The output, however, is:

Before:1479560After:3137668829

As the documentation suggests, GetMenuItemInfo returns the bitmap'shandle in uMenuItemInfo.dwTypeData. Thus, we have lost the pointer tothe buffer sBuf. On the third loop, the program will crash, because thethird call to GetMenuItem-Info will try to write the menu text for the thirditem to an imaginary buffer at address 3137668829 = &Hbb0506dd. If thismemory is protected (as it probably is), you will get a message similar tothe one I got in Figure 6-12.Figure 6-12. Whoops

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

Page 218: VB1

Helpmate

Note that if we uncomment the lines of code that print the menu text, thecode will probably crash when we come to these lines during the secondloop.To fix this code, we need to pay attention to when the pointer changes andcorrect the problem, as in Example 6-4.

Example 6-4: A Corrected Version of Example 6-3Public Sub GetMenuInfoExample Dim uMenuItemInfo As utMENUITEMINFODim bBuf(1 To 50) As ByteDim sText As StringDim lPointer As Long ' Initialize structureuMenuItemInfo.cbSize = LenB(uMenuItemInfo)uMenuItemInfo.fMask = MIIM_TYPEuMenuItemInfo.dwTypeData = VarPtr(bBuf(1))uMenuItemInfo.cch = 49 ' Get menu textFor i = 0 To 2 ' Must reset count each time uMenuItemInfo.cch = 49

' Save buffer pointer lPointer = uMenuItemInfo.dwTypeData Debug.Print "Before:" & uMenuItemInfo.dwTypeData ' Call API lng = GetMenuItemInfo(hSubMenu, CLng(i), -1, uMenuItemInfo) Debug.Print "After:" & uMenuItemInfo.dwTypeData

' Check for pointer change If lPointer <> uMenuItemInfo.dwTypeData Then Debug.Print "Bitmap!" ' Restore pointer uMenuItemInfo.dwTypeData = lPointer Else ' Print text sText = StrConv(bBuf, vbUnicode) Debug.Print sText End If

Next End Sub

The output is:Before:1760168After:1760168Test1Before:1760168

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

Page 219: VB1

Helpmate

After:1443168935Bitmap!Before:1760168After:1760168Test3

Note that if we had declared uMenuItemInfo.dwTypeData of type String,then as soon as GetMenuItemInfo changed the pointer to the bitmaphandle, VB would think it had a character array at that location. We can'teven watch out for this and reset the pointer, because the change mighthave been legitimate.The previous discussion and the previous example have shown that weneed to be very careful about BSTRs. In short, there are two issues thatmust be addressed:

• A BSTR undergoes a BSTR-to-ABSTR translation when passed toan external function.

• A BSTR may have its value changed to a non-BSTR value (such asa handle or length) by an external function.

Note that these issues must be addressed even when a BSTR is embeddedin a structure.In any case, the translation issue is generally not a problem, since VB doesthe reverse translation on the return value. However, the other issue can bea fatal problem. The only way to avoid it completely is to manuallyreplace any BSTRs by LPSTRs, using a byte array.

Getting the Address of a Var iableof User -Defined TypeAn API programmer often needs to get the address of a variable of user-defined type. Consider, for example, the structure:

Type utExample sString As String iInteger As IntegerEnd Type Dim uEx As utExample

Suppose we want to find the address of the variable uEx. First, note thatthe address of a structure variable is the same as the address of its firstmember.Now consider the following code:

Debug.Print VarPtr(uEx)Debug.Print VarPtr(uEx.sString)Debug.Print VarPtr(uEx.iInteger)Debug.PrintDebug.Print rpiVarPtr(uEx)Debug.Print rpiVarPtr(uEx.sString)Debug.Print rpiVarPtr(uEx.iInteger)

whose output is as follows. 1243836 1243836

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

Page 220: VB1

Helpmate

1243840 1243824 1243820 1243840

As you can see, VarPtr reports the address as you would expect: theaddress of uEx is the same as the address of uEx.aString, and the addressof uEx.iInteger is 4 bytes larger, to account for the 4-byte BSTR.On the other hand, the rpiVarPtr is susceptible to BSTR-to-ABSTRtranslation, which occurs on the member of the structure that is a BSTR.The relationship between the first and second address in the second groupmay look strange until we remember that each call to rpiVarPtr producesa 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 integermember. There is no translation in the call:

Debug.Print rpiVarPtr(uEx.iInteger)because there are no BSTR parameters. Thus, we can use an externalfunction such as rpiVarPtr to compute the address of a structure providedthe structure has at least one non-BSTR parameter. In this event, we getthe address of one such parameter and count backwards to thebeginning ofthe structure

Using Win32 API in VB

Table of Contents

o What's API - description of API

o API declarations - how to declare APIs

o API functions - types of function

o API messages

o Handles, Coordinates, Structs, ...

o API parameter types - VB equivalents

o Any

o Passing parameters - ByVal, ByRef, structs, strings, arrays...

o Callbacks

o WinProc

o Subclassing

o Getting Error result

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

Page 221: VB1

Helpmate

o Combining flags

o Handling parameters

o How to know the functions?

o An example

o Closing Words

What's API

API (Application Programers Interface) is a set of predefined Windowsfunctions used to control the appearance and behaviour of every Windowselement (from the outlook of the desktop window to the allocation ofmemory for a new process). Every user action causes the execution ofseveral or more API function telling Windows what's happened.

It is something like the native code of Windows. Other languages just actas a shell to provide an automated and easier way to access APIs. VB hasdone a lot in this direction. It has completely hidden the APIs andprovided a quite different approach for programming under Windows.

Here is the place to say that, Every line of code you write in VB is beeningtranslated by VB into API function and sent to Windows. Thus callingsomething like Form1.Print ... causes VB to call TextOut API functionwith the needed parameters (either given by you in the code, or taken bysome defaults).

Also, when the user click a button on your form, Windows sends amessage to your windows procedure (that is eventually hidden for you),VB gets the call, analyses it and raises a given event (Button_Click) foryou.

The API functions reside in DLLs (like User32.dll, GDI32.dll, Shell32.dll,...) in the Windows system directory.

API Declarations.

As said in "What's API", functions are declared in DLL's (Dynamic L inkL ibraries) located in the Windows System directory. You can type in thedeclaration of an API just as you do with any other function exported froma Dll, but VB has provided an easier way to do it. It is called API TextViewer.

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 tospeed it up), choose Declares, find the function, click Add and then Copy.

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

Page 222: VB1

Helpmate

Go to your project and paste it in. Do the same to have a predefinedconstant or type.

There are few problems you may face:

Suppose you want to declare a function in your form module. You pastethe declaration and run the program. VB says "Compile Error, ...andDeclare statemets not allowed as Public members of ... ." Sounds bad, butit isn't, all you need to do is add Private in front of the declaration (likePrivate Declare Function ...). Do not forget, though, that the function willbe visible only within this form module.

In some cases, you may get the message Ambiguous name detected byVB. It means you have two functions, constants or whatsoever sharing onename. As most of the functions (maybe all of them, I haven't checked that)are aliased, which means they are given different names, and not theiroriginal using Alias clause, you may simply change the name of thefunction and it will still work.

You may read the Declare statement help topic of VBs for description ofAlias.

Messages

OK, now you know what API function is, but you have definitely heard ofmessages (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 ofinput has occured and you must process it. A message to your form is sentwhen the user clicks on a button, moves the mouse over it or types text ina textbox.

All messages are sent along with four parameters - a window handle, amessage identifier and two 32-bit (Long) values. The window handlecontains the handle of the window the message is goint to. The identifier isactually the type of input occured (click, mousemove) and the two valuespecify an additional information for the message (like where is the mousecursor when the mouse is been moved).

But, when messages are sent to you, why don't you see them, looks likesomeone is stealing your mail. And before you get angry enough, let metell you.

The theft is actually VB. But he does not steal your mail, but instead readsit for you and give you just the most important in a better look (with someinformation hidden from time to time). This better look is the events youwrite code for.

So, when the user moves the mouse over your form, Windows sendsWM_MOUSEMOVE to your window, VB get the message and its

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

Page 223: VB1

Helpmate

parameters and executes the code you've entered for Button_MouseMoveevent. Along the way, VB has transformed the second 32-bit value of themessage (it contains the X and Y position in pixels, 16-bit each) into twoSingle type value of twips.

Now, say you need the coordinates of the mouse in pixels. VB convertedthem into twips, and now you will convert them into pixels again. Apartfrom losing time, it is somehow iritating to know Windows give you whatyou need and VB "favorably" alters it so that you must re-alter it. Hereyou will ask - Can't I receive the messages myself. OK, there is a waycalled Subclassing, but you should use it only if really necessary as it goesa bit against the concepts of safe programming of VB.

Something else that need to be said: You can send massages to your ownwindow or to another one yourself. You just call SendMessage orPostMessage (SendMessage will cause the window to process the messageimmediately and Post message will post it onto a queue, called messagequeue, after any other messages waiting to be processed (it will returnafter the message is processed, i.e. with some delay)). You must specifythe window handle to send the message to, the message identifier (allmessage identifiers are available as constants in VB API Text Viewer) andthe two 32-bit values.

Some Windows specifics.

This topic is intended to give you a clue about some Windows specificsthat are not the same under VB.

Windows identifies every form, control, menu, menu item or whateveryou can think of by its handle. When your application is run, every controlon it is assigned a handle which is used later to separate the button fromthe rest of the controls. If you want to perform any operation on the buttonthrough an API you must use this handle. Where to get it from? Well VBhas provided a hWnd property for all controls that have handles inWindows.

Windows works with pixels, not twips. So, it is a good idea to have thecontrols you'll use API functions over set their ScaleMode properties toPixel(3) so that you can use ScaleXXX properties to get their metrics. Buteven though, you have this opportunity, you may still need to converttwips to pixels and vice versa. You do it using TwipsPerPixelX andTwipsPerPixelY ot the global Screen object. Here it is:

pixXValue = twipXValue \Screen.TwipsPerPixelXpixYValue = twipYValue \Screen.TwipsPerPixelYtwipXValue = pixXValue *

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

Page 224: VB1

Helpmate

Screen.TwipsPerPixelXtwipYValue = pixYValue *Screen.TwipsPerPixelY

I haven't really seen the TwipsPerPixelX and TwipsPerPixelY value to bedifferent, but its always better to make difference, at least for the goodprograming 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 coordinatesystems for the functions. So, be careful.And lastly, don't forget that VB is safe till the moment you begin to useAPIs. A single syntax error in an API call may cause VB to crash (saveoften!). Also VB cannot debug APIs and if your program is crashing orbehaving awkwardly, firstly check the API calls - for missed ByVal, formistaken type or parameter, everything).

Where to get the functions descr iption from

This topics won't tell how to change the button text through API or how tofind a file quickly. It is not a API functions documentation.

To get the description of an API function, you need to have either SDKhelp 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 BorlandDelphi 3.0 package or MS Visual C++ 5.0 for example. Search the internetare 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 areobsolete, though most of them still exist for compatibility in Win95.

API parameter types

When you already have a SDK Help, you will surely notice that functionsreturn values and parameters have some strange types like VOID,LPCSTR or DWORD. If you are familiar with C, then you know whatthey mean. For the rest, here is a table taken from VB Books Online (topicis Converting C Declarations to Visual Basic):

C language data type In Visual Basic declare as Call with

ATOM ByVal variable As Integer An expression that evaluates to an Integer

BOOL ByVal variable As Long An expression that evaluates to a Long

BYTE ByVal variable As Byte An expression that evaluates to a Byte

CHAR ByVal variable As Byte An expression that evaluates to a Byte

COLORREF ByVal variable As Long An expression that evaluates to a Long

DWORD ByVal variable As Long An expression that evaluates to a Long

HWND, HDC, HMENU, etc. (Windows handles)ByVal variable As Long

An expression that evaluates to a Long

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

Page 225: VB1

Helpmate

INT, UINT ByVal variable As Long An expression that evaluates to a Long

LONG ByVal variable As Long An expression that evaluates to a Long

LPARAM ByVal variable As Long An expression that evaluates to a Long

LPDWORD variable As Long An expression that evaluates to a Long

LPINT, LPUINT variable As Long An expression that evaluates to a Long

LPRECT variable As type Any variable of that user-defined type

LPSTR, LPCSTR ByVal variable As String An expression that evaluates to a String

LPVOID variable As Any Any variable (use ByVal when passing a string)

LPWORD variable As Integer An expression that evaluates to an Integer

LRESULT ByVal variable As Long An expression that evaluates to a Long

NULL As Any or ByVal variable As Long ByVal Nothing or ByVal 0& or vbNullString

SHORT ByVal variable As Integer An expression that evaluates to an Integer

VOID Sub procedure Not applicable

WORD ByVal variable As Integer An expression that evaluates to an Integer

WPARAM ByVal variable As Long An expression that evaluates to a Long

Notes.

You should notice that the BOOL type (boolean) evaluates to Long andnot Boolean. So, 0 refers to False and any other value to True.

HWND, HDC, HMENU, etc. - etc. means there also other types like these.All of them begin with H and stand for handles for different type ofobjects. For example HBITMAP is a bitmap handle, HBRUSH is a brushhandle 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 aseparate topic dedicate to Any.

Some types begin with LP. It is an abbreviation of Long Pointer to. SoLPWORD is actually a memory location where the data is stored. No, youwon't have to call a function to get this address. When you pass yourargument ByRef (the defaul) you actually pass its address. The thing toremember here is that, if you parameter type begins with LP - you shouldpass it ByRef. By the way LPARAM is like Lparam and not LParam. It isnot 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'tdiscuss it here. Just choose a way you will pass it when needed. In most ofthe cases I see passing it as ByVal 0& or as vbNullString.

And lastly, VOID is used for functions return value to specify that there isno 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 itas Sub in your VB code.

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

Page 226: VB1

Helpmate

Any

Some messages contain parameters declared as "Any". It means thisparameter can be a variety of types (you may pass an integer, a string, or auser-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 rulesto 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 passa 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 althoughthey are numeric values. This is so, because in the function declarationthey are declared as ByVal. The fourth parameter, though, is declaredByRef (VB doesn't know what kind of values you are going to pass) andyou must explicitly specify ByVal in front of it.Sometimes it's much simpler to just declare several versions of onefunction and use a different one for different calls. You may declaresomething like:

Public Declare Function SendMessageLng Lib "User32"Alias "SendMessageA" _ (ByVal Hwnd as Long, ByValwMsg as Long, _ ByVal wParam as Long, ByVallParam as Long) as Long

or

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

Page 227: VB1

Helpmate

Public Declare Function SendMessageStr Lib "User32"Alias "SendMessageA" _ (ByVal Hwnd as Long, ByValwMsg as Long, _ ByVal wParam as Long,lParam as String) as Long

Notice that the parameter type does not change for API. The fourthparameter is always a 4-byte Long value. When you pass a Long or NullByVal, a the 4-byte value is passed directly to the function. If you pass aString or else, you pass it ByRef and so VB actually passes the address ofyou variable, and it is a 4-byte value again.

Passing Parameter

Of course you know how to pass parameters, just put it in the function calland its done! Well there are some details you should be aware of whenpassing parameters to API function.

ByVal or ByRef. Usually you don't have to bother about these keywordsas VB API Text Viewer declares the function parameters as API wantsthem and when you just enter your value, it is passed as is declared.Generally, when a value is passed ByVal, the actual value is passeddirectly to the function, and when passed ByRef, the address of the valueis passed. The only thing you may encounter is the Any type.

Passing strings to API function isn't difficult too. The API expects theaddress of the first character of the string and reads ahead of this addresstill it reaches a Null character. Sound bad, but this the way VB actuallyhandles strings. The only thing to remember is always to pass the StringByRef.

The situation is slightly different when you expect some information to bereturned by the function. Here is the declaration of GetComputerNameAPI function:

Declare Function GetComputerName Lib"kernel32" Alias "GetComputerNameA" _ (ByVallpBuffer As String, nSize As Long) As Long

The first parameter is a long pointer to string, and the second the length ofthe string. If you just declare a variable as String and pass it to thisfunction, an error occurs. So, you need to initialize the string first. Here ishow to get the computername:

Dim Buffer As StringBuffer = Space(255)

Ret& =GetComputerName(Buffer,

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

Page 228: VB1

Helpmate

Len(Buffer))if Ret& > 0 thenCompName$ = Left(Buffer,Ret&)

Here, the string is initialized as 255-spaces string. We pass it to thefunction and give it the length too. The function returns 0 for an error orthe actual length of the computer name otherwise. CompName$ willcontain the computer name.Some functions also expect arrays. Here an exmaple:

Declare Function SetSysColors Lib "user32"Alias "SetSysColors" _ (ByValnChanges As Long, lpSysColor As Long, _ lpColorValues As Long) As Long

The last two parameter are arrays of Long. To pass an array to a function,you pass just the first element. Here is a sample code:

Const COLOR_ACTIVECAPTION = 2Const COLOR_INACTIVECAPTION = 3Const COLOR_CAPTIONTEXT = 9ConstCOLOR_INACTIVECAPTIONTEXT = 19

Dim SysColor(3) As LongDim ColorValues(3) As Long

SysColor(0) = COLOR_ACTIVECAPTIONSysColor(1) =COLOR_INACTIVECAPTIONSysColor(2) = COLOR_CAPTIONTEXTSysColor(3) =COLOR_INACTIVECAPTIONTEXT

ColorValues(0) = RGB(58, 158, 58) 'darkgreenColorValues(1) = RGB(93, 193, 93) 'lightgreenColorValues(2) = 0 'blackColorValues(3) = RGB(126, 126, 126) 'gray

Ret& = SetSysColors(4&, SysColor(0),ColorValues(0))

This sample changes the system colors for the active window captionbackground and text and for the inactive window caption background andtext.

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

Page 229: VB1

Helpmate

Callbacks

A callback is a function you write and tell Windows to call for somereason. You create your own function with a specified number and type ofparameters, then tell Windows that this function should be called for somereason and its parameters filled with some info you need. Then Windowscalls you function, you handle the parameters and exit from the functionreturning some kind of value.

A typical use of callbacks is for receiving a continuous stream of datafrom Windows. Here is the declaration of a function that requires acallback:

Declare Function EnumWindows Lib"User32" _ ByVal lpEnumFunc As Long,ByVal lParam As Long) As Long

The first parameter is the address of your callback function, and thesecond is a whatever value you want. This value will be passed to yourfunction, so that you know what it is called for.VB 5.0 has provided a useful operator called AddressOf which returns theaddress of a function. It may be used only in front of a parameter whenyou call a function and uses like

FuncP = AddressOf MyFunction

are wrong and cause error. So, you must call EnumWindows like that:

Success& = EnumWindows(AddressOfcbFunc, 58&)

You must also write the callback function. There are different type ofcallbacks that have a different sets of parameters. Description of thisparameter can be found in a SDK Help file or MS SDK documentation.Here is the declaration for the callback:

Function cbFunc (ByVal Hwnd, ByVallParam) as Long

Here is a sample of callbacks:

Private Declare Function GetWindowTextLib "user32" Alias "GetWindowTextA" _ (ByVal hwnd As Long, ByVallpString As String, _ ByVal cch As Long) As Long

Success& = EnumWindows(AddressOfcbFunc, 58&)

Function cbFunc (ByVal Hwnd, ByVallParam) as Long

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

Page 230: VB1

Helpmate

If lParam = 58 then 'enum windows Str$ = Space(255) Ret& = GetWindowText(Str$,Len(Str$)) Debug.Print Left(Str$, Ret&) End If

End Function

This sample enumerates the captions of all windows (no childs).

The Window Procedure

Windows does not know anything about events. These are shipped in VBto hide the actual way Windows informs your window that something ishappening with him. VB gently serves as a interpreter and translates theWindows language into VBs.

But the reallity is different and you will soon face it. Imagine you want toknow when the user highlights you menu item (not press, just highlight).VB does not provide such event, but you've seen how other programsdisplay 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 calledwindow procedure. It is actually a callback function. This function is senta 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 windowprocedure instead of you. When Windows sends a message, this proceduredispatches it to a certain event and converts its parametrs into some easierto use parameters of the event. But, in some cases this procedures justignores some messages and can't receive the actual input. If you reallyneed to get this message, you must subclass your window, whichdiscussed in another topic.

Here is the declaration of a calback window procedure:

Function WindowProc(ByVal Hwnd AsLong, ByVal wMsg As Long, _ ByVal wParam AsLong, ByVal lParam As Long) As Long

The first parameter specifies the window handle, wMsg is a messageidentifier (like WM_COMMAND or WM_MOUSEMOVE), wParam andlParam are 32-bit values which meaning depends on the type of messagesent.

SubClassing

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

Page 231: VB1

Helpmate

When you have already used the maximum VB offers you and want to dosomething more, or just want to know something more about what's goingon with your window, now or then you will find the advantages ofsubclassing.

Subclassing refers to changing the active window procedure with a newone. Now this new procedure will receive all messages coming to yourwindow 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 procedureto process it.

Subclassing is done by calling SetWindowLong. This function changes aspecified 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 AsLong) As Long

The first parameter specifies the window to be subclassed, the secondshould be GWL_WNDPROC (-4) and the third should be the address ofthe new window procedure. See Callbacks and The Window Procedure.This function will be called literally every time your window has the focusand something is going on and in some other cases (like changing somesystem parameter by another process).SetWindowLong return 0 if an error occurs, or the address of the oldwindow procedure. This address is especially important and you shouldsave it in a variable or else. It is used to call the old function when you donot process a message (in fact you will process less than 1% of allmessage and will let the old procedure handle the rest).Calling the old window procedure is accomplished by CallWindowProcAPI function. Here is the declaration:

Declare Function CallWindowProc Lib"user32" Alias "CallWindowProcA" _ (ByVal lpPrevWndFuncAs 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 thesame as the four parameter you receive. Note that you may change someof the values to control the message process. For example, when youreceive WM_MOUSEMOVE, you get the coordinates of the mouse fromlParam and change them to some other coordinates. Then the old window

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

Page 232: VB1

Helpmate

procedure will think the mouse is not where it is actually and may forexample show a tooltip of some distant control or do some other funnythings.The ruturn value you specify is also meaningful. It depends on themessage sent.It is very important to return the original window procedure before endingyou 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 isa 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, AddressOfMyWndProc)End Sub

Private Sub Form_Unload() Ret& = SetWindowLong(Me.Hwnd,GWL_WNDPROC, oldWndProc)End Sub

Function MyWndProc(ByVal Hwnd AsLong, ByVal wMsg as Long, _ ByVal wParam AsLong, ByVal lParam As Long)

Debug.Print wMsg & " " & wParam &" " & lParam Ret& = CallWindowProc(oldWndProc,Hwnd, wMsg, wParam, lParam)

End Function

Handling Parameters

Sometimes the functions do not return the information you need the wayyou want it. Typical example is combining two integer(2-byte) valuesspecifying the mouse position into one 4-byte value. Another case istelling you that if bit 29 is on it means something. Also, you may receive aLong value that is the address of a structure.

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

Page 233: VB1

Helpmate

Combining or separating values does not need any description.APIMacro.bas you may find on our site(www.geocities.com/SiliconValley/Lab/1632/) contains all functions youneed to have.

To check if bit N of Value is on use the following:

If Value and (2^N) then ...

To set a bit on:

Value = Value Or 2^N

To set a bit off:

Value = Value And Not 2^N

If you set and get the state of a bit you know beforehand, its much faster toreplace 2^10 with 1024. This way VB won't have to calculate it itself (VB"hates" ^).If you receive a pointer to a type, then you must do a bit more. To get theinfo, you use CopyMem function. Here is the declaration:

Declare Sub CopyMem Lib "kernel32"Alias "RtlMoveMemory" _ (pDest As Any, pSourceAs Any, ByVal ByteLen As Long)

If you receive a pointer to RECT type in the Long variable Addr, use thefollowing:

Dim Info As RectCall CopyMem(Info, ByVal Addr, len(Info))

Note the ByVal keyword. Now, if you need to put the information back,use:

Call CopyMem(ByVal Addr, Info,Len(Info))

Closing Words

I hope this tutorial has helped you understand how to control the power ofAPI functions and how to use them properly. But BEWARE! It's like thefire, you let him out of control and you are lost. And, of course, neverforget that VB is designed for easy and safe programing and API foesstraight against. If you are looking for more control and power, bettermove to VC++.

To expand your knowledge of APIs and get some expeience, be sure totake a look at the sample on this site(www.geocities.com/SiliconValley/Lab/1632/). Almost all of them arededicated to API and its advantages.

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

Page 234: VB1

Helpmate

Also, if you have any questions, something has left misunderstood, orwhatever it is, be sure to e-mail. I 'll try to solve your problem.

Good luck, exploring the API !!!

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