Search the Catalog
COM+ Programming with Visual Basic

COM+ Programming with Visual Basic

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

By Jose Mojica
June 2001
1-56592-840-7, Order Number: 8407
364 pages, $34.95

Chapter 11
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 about shortly. A number of other language compilers are also being developed by third parties to emit .NET-compatible code, such as Cobol.NET, Component Pascal, Eiffel, and others.

The second part of .NET is called Visual Studio.NET. Visual Studio.NET is composed of the IDE that you use to write code, several programming tools, and online documentation. Visual Studio.NET uses the .NET SDK compilers to compile your program. In reality, if you like using NotePad.exe, you do not need Visual Studio.NET; you could write your VB programs in Notepad and then run the command-line compiler.

One important thing to understand about .NET is that it is a lot more than its name implies. At first glance, it may seem like a technology geared toward writing Internet applications. Although this is true in some sense, it is a lot more than that. .NET is primarily an architecture for writing applications that are object-oriented in nature, and both hardware and operating system agnostic. You may have heard similar claims from another language--Java. Under the covers, .NET is very different from Java, but conceptually the two architectures have the same goal.

IL

The heart of .NET is the Intermediate Language (IL). IL is a hardware-independent object-oriented form of assembly language. The following code shows a "hello world" program written in IL:

.assembly hello {}
.assembly extern mscorlib {}
.method static public void main(  ) il managed {
	.entrypoint
	.maxstack 1
	ldstr    "Hello World"
	call       void [mscorlib]System.Console::WriteLine(class System.String)
	ret
}

A line-by-line discussion of the preceding code is beyond the scope of this book; however, we will discuss some of the most interesting parts of the code shortly. Notice for now that the code resembles assembly language but uses a different set of commands, and it does look like a much higher-level language than pure .asm.

You could take the previous code and save it as a text file with Notepad, giving it the extension .IL (helloworld.il, for example). If you have the .NET SDK installed, you could run the IL compiler from the command line by entering the following command:

ILASM helloworld.il

The output of that command would be the executable helloworld.exe. You could then run the program and witness "Hello World" appear on your console.

IL-generated code is processor and operating system independent. The IL source code must be changed to native code before it is run. IL programs are not interpreted; they are instead converted to native code using one of three compilers provided in the .NET SDK.

The default compiler is the Just-in-Time (JIT) compiler. The JIT compiler takes IL and first compiles the entry-point function and any code that the function needs; then as the code executes, any other code that that code needs is also compiled; and so on. Sometimes some of the earlier compiled code may be thrown out from memory to make room for other code, then recompiled when needed.

Another option for compilation is to use the EconoJIT compiler, which is due to come out in a future release of the Platform SDK. The EconoJIT compiler does the same job as the JIT compiler, but it produces less efficient code. Sometimes developers feel that having code compiled at runtime may decrease the performance of the program considerably. Although this may be the case, depending on how the compiler is written, it is more likely that your code may see better performance when it is JIT compiled than when it is compiled in a traditional way. The reason for this is that the JIT compiler can take into consideration your hardware and optimize the code to function well with it. If you think about it, when code is precompiled from the factory, it follows a "one size fits all" approach; it is often optimized to run on a machine that has an Intel processor. If you ran your program on a machine with an AMD processor, a JIT compiler would be able to use the AMD extensions as needed. This is the way that the JIT compiler is supposed to work, and it produces high-quality machine code at the price of load time. The EconoJIT compiler, on the other hand, compiles faster at the cost of execution performance.

The third type of compilation is called OptJIT. The OptJIT compiler is due to come out in a future release, but the idea is that some third-party vendors will emit a subset of IL called Optimized IL (OptIL). OptIL is IL with instructions embedded into it that tell the OptJIT compiler how to optimize its output for certain tasks. For example, the third-party language may be optimized to do mathematical calculations and would like the generated code to do mathematical calculations in a certain fashion. The third-party OptIL output would embed information in the IL that would tell the OptJIT compiler how to optimize calculation code when it generates the machine code.

The operating system that you are using does not know how to take IL, run it through the JIT compiler, and run the results directly. Later versions of Windows (probably beyond Windows XP) will be IL ready. This means that the operating system may be able to see a text file with IL in it and run it as is. However, Windows 2000 cannot do this, so IL must be embedded into an executable or a DLL. The ILASM compiler can take the IL and build a PE file around it. PE stands for portable executable and is the format that .EXE files and .DLL files use. The PE wrapper that the ILASM compiler generates has code to invoke the runtime loader found in mswks.dll and other files shipped with the .NET SDK. The PE file has the IL embedded in it. The IL may be embedded as "text," although some formatting of the text is done to make it easier to parse, or it may be prejitted (turned into native code). Microsoft will ship some files that have IL already changed to native code or prejitted. The runtime is able to run compiled IL code or text IL code.

Most of the time, you will not be writing IL code from scratch. Instead, you will use your language of choice. A number of language compilers have been rewritten to generate IL instead of native code. Visual Basic is one of these languages. In addition Microsoft has created a brand new language called C# (C-Sharp). C# is a C++-like language that in many ways also resembles Visual Basic. It eliminates a number of features from C++ that, although they provided a lot of "power," also produced a lot of confusion. For example, C# does not have pointers. It also does not have macros or templates. The successor to VB 6 is VB.NET. VB.NET is a completely new version of Visual Basic. Many things have changed, and later in this chapter you will learn about some of the new features.

Assemblies

Along with a new form of assembly language and a new set of compilers, Microsoft has also redefined what it means to be an application. If you think about the current operating system boundaries, there are two main entities: processes and threads. Threads give us an order of execution. You may recall from Chapter 5 that a program may launch another thread in order to do two tasks seemingly simultaneously. A process determines primarily a memory boundary. Two processes do not share memory. Their memory is isolated, and although it is possible to share memory using low-level functions, that is not the standard. However, more than a memory boundary, the process also serves as a security boundary. In Chapter 10, you learned that each process in the operating system runs under a certain set of credentials.

Microsoft has redefined what it means to be a process. In fact, the new world does not address processes per se; the new world uses assemblies. You're probably wondering whether assemblies are DLLs or EXEs. The answer is that assemblies are neither (it is almost better to forget that EXEs and DLLs ever existed). In many ways, processes are better matched to a new boundary in the .NET architecture known as AppDomains.

As you already know, the runtime runs IL. A developer may declare one file or a number of files containing IL as being part of an assembly. An assembly is the smallest unit that can be versioned; it determines the boundary for which classes are made public or private; it is also a unit that can be secured. Because IL files are packaged in EXEs and DLLs, an assembly can be a single EXE, a single DLL, or a combination of EXEs and DLLs. An assembly may also contain other files, such as resource files and even help files. Figure 11-1 shows the relationship between EXEs, DLLs, and assemblies.

Figure 11-1. The relationship between modules, assemblies, EXEs, and DLLs

 

Each image file containing valid IL is called a module. To be an assembly, one of the files in the group must have an assembly manifest. The assembly manifest is created with an assembly definition. If you look at the "hello world" example presented earlier, the code begins with the directive .assembly hello. Without the .assembly directive, the assembler would produce only a module. Having the .assembly directive does two things: it declares an assembly with the name hello, and it creates the assembly manifest.

A manifest is metadata, which is a fancy word for descriptive text. You can think of the manifest as the type library for the assembly. It defines properties of the assembly such as the version number of the assembly and the culture (or locale) that the assembly was built for. The manifest also lists all the modules, all the files, and all the external assemblies on which the assembly depends. For example, if your assembly requires data access, you will need to reference the Microsoft.Data assembly. In the earlier IL example, the second line of code, .assembly extern mscorlib, tells the loader that the assembly uses a system assembly known as mscorlib. The reason the code needs to reference this assembly is because it uses a class called System.Console. This class has a method called WriteLine that the IL code uses to output "Hello World" to the console.

As you may suspect, Microsoft provides a number of prebuilt assemblies that you may use. These assemblies contain a series of public classes, interfaces, and attributes (you will learn about attributes shortly). These sets of classes, interfaces, and attributes constitute the .NET Common Type System.

The Common Type System

If you think about the type system in Visual Basic 6, you may separate the types into two main groups: objects and intrinsic types. We can say that intrinsic types include things like integers, strings, doubles, and so on, while object types refer to classes you define. C++ has its own type system. It also includes some native types like int, double, short, and long, and it includes object types--classes that you define. C++ also has several class libraries, among them the Microsoft Foundation Classes (MFC) and the Active Template Library. If you look at a third-generation language like Delphi, for example, you see that that language also has its own type system. A common problem was making these type systems communicate with one another. COM+ handled this by letting each compiler decide how they were going to map types to a few C++ types. If you wanted your component to be VB compatible, you had to figure out what subset of all the C++ types mapped neatly to VB types, and the VB compiler had to look at a C++ interface definition and translate it as best it could to VB.

To resolve most of the issues of compatibility between languages, Microsoft is also introducing a common type system. I say most, because it turns out that every type that can be represented in the type system is not necessarily available to every language. Instead Microsoft defines the Common Language Subset (or CLS) to be a subset of the type system that every language should support. However, for the most part, having a common type system means that every language that produces IL knows how to use types declared in any other language. Three things make the type system particularly interesting:

What makes value types in the new type system different is that they also derive ultimately from System.Object. In other words, even value types are classes with methods, fields, and interface implementations. For example, when you dim a variable as type Integer, VB turns that declaration into a variable of type System.Int32. System.Int32 is a class.

What distinguishes a value type from other classes is that value types are derived from a class called System.ValueType. Sometimes it may be necessary to take a value type and cast it to a variable of type System.Object (think of System.Object as the VB 6 Variant type or the VB 6 Object type). However, System.Object declarations are reference types, and the runtime treats them differently from value types. To allow this conversion, the runtime supports an operation known as boxing. Boxing means that the system duplicates the data stored in the value type and creates a copy of the object on the heap. The reverse procedure, in which a value type is created from a reference type and the data is replicated once again, is called unboxing. You are going to see an example later on in the chapter.

To make it possible for every object, including value types, to derive from System.Object, the CLR uses inheritance. There are two types of inheritance in the system: class inheritance and interface inheritance. You can inherit from only a single class, and, in fact, every class must inherit from at least one class, System.Object. On the other hand, you can implement any number of interfaces. That stated, let me complicate things by saying that interfaces are also classes derived from System.Object. However, they are a special type of class marked as abstract, and every member in the interface is really a pointer to a function (the concept of vtables to vptrs is still the same).

Why .NET and Not COM+ 2.0?

The first question that people often have is why Microsoft had to come up with a different component technology. Why not improve COM+? Let's talk about some of the limitations in COM and how .NET addresses them.

One problem with COM+ was the lack of a common type system. We have talked a little about this problem. To summarize, each language involved in COM had its own type system, and the best the compilers could do was match a type from one language to another by the amount of memory that the type consumed and the semantics of the type. With .NET, we have a common type system, each language creates types that follow the rules of the type system, and every type is a subtype of System.Object.

Another problem with COM was how to advertise the types that the server exposed. C++ developers relied primarily on header files that described the set of interfaces exposed by the server. VB relied on type libraries. Often an interface would originate from one of the Microsoft groups in C++ syntax. Then a developer would have to write a type library for Visual Basic that had VB-friendly syntax. .NET uses a better approach. Assemblies expose types, and an assembly can be referenced directly when building a new assembly. Thus, if you create a program (an assembly) that relies on a database class, for example, when you compile your assembly, you will tell the compiler to reference the database assembly. Visual Studio.NET will give you an easy way to tell the compiler what assemblies you need. In fact, there is almost no difference visually between referencing a type library in VB 6 and referencing an assembly. Later on you will see how to expose a class in an assembly and how to reference the assembly in another assembly.

A third problem with COM was that the architecture did not have perfect knowledge of the types in your process. For example, there was nothing in COM that told the operating system what COM servers your client program was dependent on at load time. The type library told COM about what types your server exposed, but the client relied on CreateObject or New. So there was no way for the OS to know at load time if your EXE needed a server that wasn't available in the system. The OS didn't know until it executed the line of code that tried to create a type in the server whether that server was available. With .NET, the manifest contains a list of not only the types that your server exposes, but also the types that the server needs. The CLR loader verifies that it can find all the assemblies that your assembly is dependent on before running your program.

Another related problem concerned versioning. How many versions of ADO could you use on one computer at a time with COM+? Only one. When you registered ADO on the machine, there was a single registry key, InProcServer32, that told the system where ADO was located. If you had another version on the system, there was no way to use it side-by-side with the first one. You would have to register the new version, and that would override the InProcServer32 key to point exclusively to the new version, and every program would use the new version. One problem resulting from this approach was that there was no guarantee that existing client programs could use the new version of ADO. Nothing in your client process told the OS what version of ADO your program was dependent on and whether you could use a new version or not.

In contrast, .NET has an improved versioning scheme. When you build a client assembly, the manifest tells the loader the version of each assembly that you are dependent on. In addition, .NET recognizes shared assemblies and private assemblies. Private assemblies are used with just your applications. Something like ADO.NET would be a shared assembly--an assembly that many assemblies count on. Shared or public assemblies are signed with a private and public encryption key. The process of signing the code produces an originator. Once your assembly has an originator, you may put it in the global access cache (GAC). The GAC physically lives under the WINNT\Assembly directory and stores a copy of all shared assemblies. The CLR loader looks at the list of assemblies you are referencing, and if it does not find a private assembly that is a later version than the version the client was compiled against, then it will try to find the assembly in the GAC. The GAC can store multiple versions of the same assembly. For example, you may have ADO.NET Version 1, 2, and 3 in the GAC. When you build an assembly, the manifest will contain a reference not only to the assembly name, but also to the assembly's version number. It could happen that your application requires one version of the shared assembly and another assembly requires another version, and it could happen that the same process may be running both versions at the same time. Therefore, .NET now gives you the capability of having side-by-side versions of shared components. Later in this chapter, I will show you how to sign your code with a key and add it to the GAC.

Yet another problem resulting from the lack of perfect knowledge of the types you used internally was knowing when to release an object from memory. You can tell if a programmer has done COM+ at the C++ level if you ask about circular references. As VB developers, we do not have to deal with things like reference counting and circular references directly, but C++ COM+ developers do. A common problem with COM components is that object A may be holding a reference to object B, and, because B needs to make a callback call into A, it may be holding a reference to object A. B will never be released from memory as long as A is holding a reference to it, and A will never be released because B is holding a reference to it. This is known as a circular reference.

.NET does not use reference counting to determine when an object's memory should be reclaimed. Instead the system implements garbage collection. Managed code can no longer ask for a chunk of memory directly. To take advantage of garbage collection and other services, your code creates an instance of a type or of an array of types, and the memory needed is allocated by the runtime in a managed heap. When there is no more memory to hand out, the system will release memory for objects no longer in use. The garbage collection system differs from reference counting in the way that memory is cleaned up--no longer is memory reclaimed as long as there are no references to the object. Instead, the system waits to release memory until there is need for more memory. There are also commands for telling the garbage collector to collect memory immediately.

You should now have a basic understanding of the .NET architecture and how .NET improves on the existing COM+ architecture. Shortly, you are going to learn about how to have COM+ components using .NET components and vice versa. But before talking about interoperability and about the new features in VB.NET, let's talk about how to compile and version .NET assemblies.

Developing Assemblies

There are two ways to create VB.NET applications. One way is to use the next version of Visual Studio, Visual Studio.NET. Visual Studio.NET is a development environment built on top of the .NET SDK. The .NET SDK is packaged separately from the Visual Studio.NET environment. The SDK includes a C# command-line compiler, a VB.NET command-line compiler, and the DLLs and EXEs necessary to run your .NET applications.

I have decided that in order to make the information in this chapter last, I am not going to use the Visual Studio.NET designer. One reason is that the product has not been released yet. Another reason is that the product is not yet stable. A third reason is that, for the first time, Visual Basic has a true command-line compiler that can be used in conjunction with NMAKE and MakeFiles. You may be familiar with MakeFiles if you have ever worked with C++. MakeFiles are text files that tell NMAKE.EXE how to build your program. For all these reasons, I have decided to use the second most widely used development environment in the Windows platform, Notepad.EXE. So for the next set of examples, you will need three things: the .NET SDK downloadable from Microsoft, Notepad.EXE, and a command prompt. Let's start with a simple Hello World application to get a taste for how to use the command-line compiler.

Run Notepad.EXE and enter the following text:

Public class HelloSupport
    Shared Public Function GetGreeting(ByVal sName As String) As String
        return "Hello " + sName
    End Function
End Class

This code declares a class called HelloSupport. At first it seems strange to create a class for just one function, but in VB.NET every function is exposed through a class. Even when you declare a module with functions, the module becomes a class when it is compiled, and all of the functions in the class become shared, very much like in the preceding example.

So what is Shared, anyway? Classes in VB.NET have two types of methods: instance methods and shared (or static) methods. VB 6 class methods were instance methods: you had to create an instance of the class to use them, and the method performed a task on the data for that instance only. Shared methods are new to VB.NET. They can be executed without creating an instance of the class. The only limitation is that if your class has both shared methods and instance methods, you cannot call the instance methods from within the shared methods. You can call shared methods only from within shared methods. If you need to call the instance method, then you have to create an instance of your class and make the method call just as any other function outside the class would. You also cannot use any of the member fields in the class from shared methods unless the fields are also marked as shared. In a module, the VB.NET compiler turns all functions to shared.

The GetGreeting function returns a string that says "Hello x", where x is the string that the calling program passed to it. Save the preceding file as hellogreeting.vb. Then run the VB command-line compiler, VBC.EXE. If you installed the SDK, the path to the VBC.EXE compiler should be reflected in the environment so that you can run it from any folder. Open a command window and switch to the directory where you saved the files, then enter the following command:

vbc /t:library hellogreeting.vb

This command creates a DLL file with the name hellogreeting.dll. If you look at the command line, you will notice that the first option is the /t switch, which tells the compiler the target type. VB.NET is able to produce Windows applications, console applications, DLLs, and modules that can be linked with other modules to produce a multimodule assembly. They can be packaged as EXEs or as DLLs. An assembly is the smallest unit of code that can be versioned.

The keyword library produces a DLL. If you are unsure about the syntax, you can enter vbc /? for a list of command-line switches. Now it's time to create an executable that can use the function in the DLL. Create a new file in Notepad and enter the following text:

class Helloworld
 
    Shared Sub Main(  )
        Dim sName As String = "World"
        Dim sGreeting As String = HelloSupport.GetGreeting(sName)
        System.Console.WriteLine(sGreeting)
    End Sub
 
End Class

Save the file as helloworld.vb. To compile the program, use the following command line:

vbc /t:exe helloworld.vb /r:hellogreeting.dll

The preceding code declares a class called Helloworld. The class has a procedure called Sub Main. The Sub Main function is the equivalent of Sub Main in VB 6. The only difference is that the function must be declared as a shared (or a static) subroutine. If you are a hard-core VB 6 (or earlier) developer, you will appreciate the fact that you can now declare variables and assign them a value all in the same line, which is what I have done in the first line of code inside Sub Main. The function declares two variables: sName holds the name that the GetGreeting function will use for the greeting, and sGreeting holds the response from the GetGreeting function. Notice that to use the GetGreeting function, you have to qualify it with the name of the class. Because the GetGreeting function was declared as a shared function, you do not have to create an instance of the class to use it. The code in Sub Main then prints the greeting to the console using the System.Console.WriteLine function.

System.Console.WriteLine is a new function in the CLR. The function is part of the System assembly. Microsoft has defined a set of classes that gives support for the operating system functionality. Not only are these classes a replacement for using the WIN32 API functions directly, but also for other COM libraries that Microsoft ships, such as MSXML and ADO. The runtime has assemblies that Microsoft has included for XML, for HTTP communication, for data access, for threading, for IIS programming--for practically anything you can think of. There is still a way to call WIN32 APIs directly using the Interop classes; however, jumping outside of the runtime is a costly operation, and you are discouraged from doing so.

To build the executable, you must specify the name of the DLL (and the path to the DLL) that your executable needs to resolve all functions. The result of running the previous command-line command is the Helloworld.exe image. If you run helloworld.exe, you should see the phrase "Hello World" displayed in the command prompt.

Microsoft ships a disassembler with the SDK called ILDASM.EXE. Let's run ILDASM.EXE on the resulting executable to see how the helloworld.exe assembly references the hellogreeting.dll assembly. Run the following command from the command prompt:

ILDASM.EXE HelloWorld.EXE

You should see the window in Figure 11-2.

Figure 11-2. ILDASM program

 

Once you open ILDASM, double-click on the MANIFEST branch, and you should see the following code:

.assembly extern mscorlib
{
  .originator = (03 68 91 16 D3 A4 AE 33 )                         // .h.....3
  .hash = (52 44 F8 C9 55 1F 54 3F 97 D7 AB AD E2 DF 1D E0   // RD..U.T?........
           F2 9D 4F BC )                                     // ..O.
  .ver 1:0:2204:21
}
.assembly extern Microsoft.VisualBasic
{
  .originator = (03 68 91 16 D3 A4 AE 33 )                         // .h.....3
  .hash = (5B 42 1F D2 5E 1A 42 83 F5 90 B2 29 9F 35 A1 BE   // [B..^.B....).5..
           E5 5E 0D E4 )                                     // .^..
  .ver 1:0:0:0
}
.assembly extern hellogreeting
{
  .hash = (12 09 10 58 91 53 C7 13 7D 3D 53 87 A4 62 79 4F   // ...X.S..}=S..byO
           14 63 47 99 )                                     // .cG.
  .ver 1:0:0:0
}
.assembly helloworld as "helloworld"
{
  .hash algorithm 0x00008004
  .ver 1:0:0:0
}
.module helloworld.exe
// MVID: {4C781D06-B3E4-4376-A19D-E068C05D7A39}

The manifest shows a list of assemblies that your assembly is dependent on. That is nothing new; after all, before COM, DLLs had a list of imported types that you could see with the depends.exe program. However, even with the depends.exe tool, the system had no idea what DLLs you were loading dynamically in code. For example, the OS knew that your VB program was dependent on MSVBVM60.DLL (the VB runtime), but it had no knowledge of any other DLLs you might have been using through Declare statements, not to mention any COM DLLs you loaded with CreateObject. What is also interesting is the difference between private assemblies and public assemblies. If you notice, the manifest shows that you are dependent on Version 1.0.0.0 of the hellogreeting assembly. The version number for this assembly is not crucial because hellogreeting is not a public assembly; it is meant to be used only with this application. If you look at the rest of the manifest, however, you will notice that the helloworld assembly is also dependent on the mscorlib assembly and the Microsoft.VisualBasic assembly. These latter assemblies are public assemblies; the creator went through the process of creating a public assembly, and because it is a shared assembly, the rules for using the assembly with our program are more stringent. For example, we have to tell the runtime whether our assembly can use a newer version of Microsoft Visual Basic or whether it must have the same assembly it was built with.

Versioning Assemblies

To illustrate the concept of versioning in the CLR, let's turn the hellogreeting assembly into a shared assembly, install it to the GAC, then build a second version of the assembly and create a configuration file to control which version of hellogreeting the helloworld assembly will use.

The first step is to digitally sign the code with a public/private digital key. It is impossible for us to discuss the encryption algorithm in this chapter, but the basic idea is that with a public/private key, encryption is done with the private portion of the key and decryption is done with the public portion. The various compilers first run an algorithm over the manifest information and produce what is called a hash. Then they use the public/private key file (from now on referred to as a private key file) and run an encryption algorithm through this hash to encrypt it. The signature is then embedded into the image. Also, the manifest for the signed assembly includes an originator field. This field advertises the public key to the world. Anyone can therefore use this public key to decrypt the encrypted manifest hash, run the hash over the manifest once again, and detect if there was any tampering to the manifest itself. In addition, any client programs referencing the assembly have a token of the public key known as the public token. This token is the result of a one-way hash algorithm run through the public key. Anyone can run the same algorithm. This step is done to verify that the assembly the runtime has found is really the assembly that the client program was compiled against.

To generate the private key, you run sn.exe, the strong name utility that ships with the .NET SDK. Enter the following command in a command-prompt window:

sn.exe -k "widgetsusa.snk"

The name of the file--even its extension--is something you make up entirely, although tools like VS.NET look for the .SNK extension. I'm using the name of a fictional company, since a company would likely use its name for the filename and use this key to sign every shared assembly it produces.

Once you have generated the private key file, now you can sign your assembly with that code. Modify the hellogreeting.vb source code as follows:

<Assembly: AssemblyKeyFile("widgetsusa.snk")>
<Assembly: AssemblyVersion("1.0.0.0")>
 
Imports System.Runtime.CompilerServices
 
Public class HelloSupport
    Shared Public Function GetGreeting(ByVal sName As String) As String
        return "Hello " + sName
    End Function
End Class

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

major.minor.build.revision

You will see how it is used shortly. Once you have modified the code, you must build the assembly again. At the time of this writing there was a problem with the VB.NET compiler; it would ignore the AssemblyVersion attribute. This should be fixed in Beta 2. For now, there is a workaround: the VB.NET compiler lets you specify the version number as a command-line parameter. Compile the hellogreeting assembly as follows:

vbc /t:library hellogreeting.vb /version:1.0.0.0

Because we have changed the version number of the assembly and assigned the assembly a strong name, you must rebuild the client program. Rebuilding is necessary so that the client program's manifest will contain information about the strong name and version of the hellogreeting assembly.

If you look at the manifest of the client program once again with ILDASM, you will notice that two things have changed for the external reference to the hellogreeting entry:

.assembly extern hellogreeting
{
  .originator = (A9 FE 7C 65 17 60 13 3E )                         // ..|e.`.>
  .hash = (4E 89 F0 45 B1 1E 0F 4B 31 65 BB 75 9D A8 71 6E   // N..E...K1e.u..qn
           64 0A 93 27 )                                     // d..'
  .ver 1:0:0:0
}

As you can see, the client program has a dependency on an assembly named hellogreeting. Furthermore, it expects the assembly to come from a certain originator (this is the public key token that was generated from the hash of the public key at the time the client program was compiled), and it expects a certain version number, 1.0.0.0. You will see how important the version number is shortly.

Once the assembly has been signed, it can be moved to the GAC. You do this with another program, Gacutil.exe. Gacutil.exe has a very simple set of commands. You can see the complete list by using /? as the command-line switch. The command-line switch to add an assembly to the GAC is -i. Enter the following command at the command prompt:

Gacutil -i hellogreeting.dll

Adding the assembly to the GAC turns the assembly into a public shared assembly. If you use Windows Explorer, you should be able to see the list of shared assemblies under \WINNT\Assembly, as depicted in Figure 11-3.

Figure 11-3. Global Assembly cache

 

You can now delete hellogreeting.dll and run helloworld.exe. The program runs without the DLL being in the same directory, because a copy has been moved to the GAC.

Suppose that we change the greeting to "New Hello" + sName, as follows:

<Assembly: AssemblyKeyFile("widgetsusa.snk")>
<Assembly: AssemblyVersion("1.0.0.1")>
 
Imports System.Runtime.CompilerServices
 
Public class HelloSupport
    Shared Public Function GetGreeting(ByVal sName As String) As String
        return "New Hello " + sName
    End Function
End Class

Next, rebuild with the same version number as before. If you run the program again, you will notice that the client program still outputs "Hello World"--it uses the assembly in the cache. What if we recompile with the version number of 1.0.0.1? Then the story changes. The client program uses the local assembly instead of the one in the GAC. We can move the new version to the GAC as follows:

gacutil -i hellogreeting.dll

This is the exact same command as before--nothing interesting, unless you look at the assembly list in WINNT\Assembly once again (see Figure 11-4).

Figure 11-4. Global Assembly cache with two versions of the assembly

 

You can see that there are two versions of the assembly in the cache. If you run the client program again, it would use the latest version. However, let's make another change to the code:

<Assembly: AssemblyKeyFile("widgetsusa.snk")>
<Assembly: AssemblyVersion("2.0.0.0")>
 
Imports System.Runtime.CompilerServices
 
Public class HelloSupport
    Shared Public Function GetGreeting(ByVal sName As String) As String
        return "New Hello 2.0.0.0" + sName
    End Function
End Class

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

In either case, by default, if you have a private copy in your directory, the CLR assumes that you put it there because that's the one you want to use. You can control the process of locating a certain assembly with a configuration file. The following code shows an application configuration file:

<BindingPolicy>
    <BindingRedir Name="hellogreeting"
                  Originator="a9fe7c651760133e"
                  Version="*" VersionNew="2.0.0.0"
                  UseLatestBuildRevision="yes"/>
</BindingPolicy>

You would save the code in a file with the same name as the executable but with the extension .CFG and in the same directory as the executable.

Notice that the syntax of the application configuration file is XML. The <BindingPolicy> tag has a <BindingRedir> subtag. This subtag has a Name attribute. The Name attribute points to the name of the assembly that needs to be resolved, in this case the DLL file. The next attribute is Originator; this is a public key token. The easiest way to obtain this number is from the GAC. If you look back at Figure 11-4, you will see the Originator field. The third attribute is Version, the version number. This attribute lets you redirect any requests for a certain revision number. In this case, we are redirecting any requests. The next attribute is VersionNew; this is the version number we would like to use. Finally, there is a UseLatestBuildRevision attribute, which is set to yes in our example. This attribute says that if there is a later build differing only by build/revision, then that one should be used. Therefore, if the GAC contained 2.0.1.0, it would use that one.

What if you wanted to use Version 1.0.0.0? After all, that is exactly the version that the client program was built in. By default, the resolver uses the assembly with the latest build revision. You could do that two ways. The first way is to change the VersionNew attribute to 1.0.0.0 and set the UseLatestBuildRevision attribute to no, as shown in the following configuration file:

<BindingPolicy>
    <BindingRedir Name="hellogreeting"
                  Originator="a9fe7c651760133e"
                  Version="*" VersionNew="1.0.0.0"
                  UseLatestBuildRevision="no"/>
</BindingPolicy>

The second way is to use what is called safe mode. Safe mode uses a different subtag named <AppBindingMode>, as illustrated in the following code:

<BindingPolicy>
   <BindingMode>
      <AppBindingMode Mode="safe"/>
   </BindingMode>
</BindingPolicy>

This code shows the <AppBindingMode> subtag's Mode attribute set to safe; by default, it is set to normal. When you use safe mode, the resolver tries to locate an assembly with the exact same version number as the one used to compile the client program. Thus, running helloworld would produce the original output, "HelloWorld." If you were to set the mode equal to normal or just delete the configuration file, you would obtain the second output, "New Hello World." To obtain the Version 2.0.0.0 output, you would have to use the redirection technique shown earlier and redirect any assembly (or any 1.0.0.0 assemblies in our case) to Version 2.0.0.0.

At this point, you should have a good feel for how to build assemblies and how to turn private assemblies into shared assemblies. Now let's discuss some of the more exciting language features in VB.NET.

VB.NET Features

Now that you know the basics of running the VB compiler and the difference between private assemblies and shared assemblies, let's discuss some of the new features in VB.NET. In many ways, VB.NET is a different language. The language has been extended to have a number of object-oriented features that it did not have before, such as method inheritance, method overloading, and exception handling.

Inheritance

In Chapter 2, you learned the difference between interface inheritance and code inheritance. VB 6 enabled you to do only one type of inheritance--interface inheritance. VB.NET adds support for the second type of inheritance, code inheritance. The way it works is that you first create a base class. In our ongoing example of a banking application, let's suppose that your design accounted for two main classes: a Checking class and a Savings class. It is likely that these two classes have functionality in common. In fact, a better design may be to create a base class named Account from which these two classes are derived. The following code shows an application that declares three classes: Account, Checking, and Savings. The Checking and Savings classes inherit all their functionality from the Account class:

Public class Account
    Dim m_Balance As Decimal
    Public Sub MakeDeposit(ByVal Amount As Decimal)
        m_Balance += Amount
    End Sub
 
    Public ReadOnly Property Balance(  ) As Decimal
        Get
            return m_Balance
        End Get
    End Property
End Class
 
Public Class Checking
Inherits Account
End Class
 
Public Class Savings
Inherits Account
End Class
 
Public Class App
 
    Shared Sub Main(  )
        Dim Acct As Account = new Checking
        Call Acct.MakeDeposit(5000)
        System.Console.WriteLine(Acct.Balance)
    End Sub
 
End Class

The Account class has a member variable, called m_Balance, that stores a Decimal (Decimal replaces the VB 6 Currency type). The class has a MakeDeposit method that accepts an amount in its parameter and adds the amount to the balance. The class also has a Balance property that reports the internal balance. The code then declares two subclasses, Checking and Savings, that inherit all their functionality from the Account class. The rest of the code declares the class that will host the Sub Main function. In Sub Main we are declaring a class of type Account and assigning an instance of the class Checking. Why can we assign an instance of Checking to a variable of type Account? Because inheritance establishes an "IS A" relationship between the base class and the derived class. For all purposes, a Checking class "IS AN" Account. The opposite is not true; Account classes are not Checking classes.

The fact that Checking is derived from Account means that we can send an instance of Checking to a function that receives an Account as its parameter. Consider the following code:

Module GenFunctions
    Public sub ReportBalance(ByVal AnyAccount As Account)
        System.Console.WriteLine(AnyAccount.Balance)
    End Sub
End Module

The preceding code defines a module. Even though it seems as if the module provides a way to export standalone functions without having to declare a class, in reality, VB changes the module to a class and makes any functions inside of it shared. The previous code declares a function called ReportBalance that reports the balance of any class derived from Account. As expected, you can send the function an instance of the Checking class or an instance of the Savings class. In fact, the following code should work as expected:

Public Class App
    Shared Sub Main(  )
        Dim check As Checking = new Checking
        check.MakeDeposit(500)
        Dim sav As Savings = new Savings
        sav.MakeDeposit(100)
        Call ReportBalance(check)
        Call ReportBalance(sav)
    End Sub
End Class

Suppose that there is a request for the Savings class to work differently. The MakeDeposit method should add $10 to every deposit (this is a very good Savings account). That means that we have to change the functionality of the MakeDeposit method. The following code shows how you might do this. First you have to modify the MakeDeposit method in the Account class as follows:

Public class Account
    Dim m_Balance As Decimal
    Public Overridable Sub MakeDeposit(ByVal Amount As Decimal)
        m_Balance += Amount
    End Sub

Notice that there is a change in the MakeDeposit method--it has the keyword Overridable. It is necessary for the developer writing the Account class to have foresight and add the word Overridable to any methods that may be modified in subclasses. The code for the Savings class has also been modified as follows:

Public Class Savings
Inherits Account
    Public Overrides Sub MakeDeposit(ByVal Amount As Decimal)
        Amount += 10
        MyBase.MakeDeposit(Amount)
    End Sub
End Class

In the Savings class we add the MakeDeposit method with the attribute Overrides. The code adds 10 to Amount, then forwards the call to the base implementation of MakeDeposit. This is done with the MyBase object. MyBase is a new global object that references your most direct base class. The runtime supports only single inheritance. All classes must derive from at most one class, and all classes must derive from System.Object. If you do not specify a class to derive from in code, then the compiler automatically makes System.Object the base class.

Suppose that we add to the Account class a withdrawal method that subtracts a certain amount from the balance:

    Public Overridable Sub MakeWithdrawal(ByVal Amount As Decimal)
         If m_Balance - Amount >= 0 Then
             m_Balance -= Amount
         End If
    End Sub

In the preceding example, MakeWithdrawal subtracts the amount from the balance only if the resulting balance is greater than or equal to 0. What if the MakeWithdrawal method in the Checking account needs to work differently? Suppose that the Checking account allows a client to overdraw the account up to $1,000. If you were to write the following code, you would get an error:

Public Class Checking
Inherits Account
    Public Overrides Sub MakeWithdrawal(ByVal Amount As Decimal)
         If m_Balance - Amount >= -1000 Then
             m_Balance -= Amount
         End If
    End Sub
End Class

The code is attempting to access the m_Balance variable, which is marked as Private in the Account class. The problem is that m_Balance must be private to the client but public to the derived class. For this reason, VB.NET has a third category named Protected. You must change the declaration of the m_Balance variable as follows:

Public class Account
    Protected m_Balance As Decimal

You can specify that it is illegal to write a derived class from your class with the NotInheritable attribute. NotInheritable results in what the runtime refers to as a sealed class. For example, we could have said that the Checking class cannot be inherited as follows:

Public NotInheritable Class Checking

On the other hand, we may want to prevent a developer from creating instances of the Account class directly. It may be our rule that a developer must create instances of a derived class. This is done with the MustInherit attribute, as in the following:

Public MustInherit Class Account

This doesn't prevent a client from declaring a variable of type Account, only from creating instances of the Account, as in var = new Account. It is possible also to force a developer into not only creating derived classes, but also overriding certain methods. For example, we could have stated that every derived class must override the MakeWithdrawal method. Of course, that change produces a number of changes. If a developer must always override the MakeWithdrawal method in the Account class, then the MakeWithdrawal method should not have any code in the Account class, and the compiler should issue an error if there is code. In addition, since everyone must override the method, a client cannot just create an instance of the class. Therefore, the class must also be marked with the MustInherit attribute. Suppose that we marked every method with the MustOverride method; we would have the equivalent of an interface.

An interface is a class in which every method must be implemented in a concrete class. There is a shorthand for defining interfaces in VB.NET using the keyword Interface instead of the Class keyword. The IAccount interface can be defined in VB.NET as follows:

Public Interface IAccount
    Overloads Sub MakeDeposit(ByVal Amount As Decimal)
    Overloads Sub MakeDeposit(ByVal Source As Account)
    ReadOnly Property Balance(  ) As Decimal
End Interface

The preceding definition shows the new way of defining interfaces in Visual Basic. Notice that the methods in the interface do not have an End Sub statement (or its equivalent). Also notice that there is no Public attribute--that is because all the methods must be public in an interface. Another interesting thing is that there is method overloading in an interface (a feature you will learn about shortly). This code is roughly equivalent to the following:

Public MustInherit Class IAccountClone
    Overloads MustOverride Sub MakeDeposit(ByVal Amount As Decimal)
    Overloads MustOverride Sub MakeDeposit(ByVal Source As Account)
    ReadOnly MustOverride Property Balance(  ) As Decimal
End Class

The only difference is that interfaces do not have a constructor--code that is executed when an instance of a class is instantiated. The fact that they do not have constructors means that they cannot be subclassed except by an entity that would also not have a constructor. This means you cannot inherit from an interface unless you are an interface. Classes can produce subclasses, and interfaces can produce subinterfaces (if that were a term). For example, interfaces can derive from other interfaces, as in the following example:

Public Interface IAccount2
Inherits IAccount
    Sub CloseAccount
End Interface

You are still required to implement the entire interface. If you implement the preceding interface in a class, the class will support both the IAccount and IAccount2 interface. Let's take a look at how implementing an interface has changed slightly:

Public Interface ISaveToDisk
Public Sub Save(  )
End Interface
 
Public Class Checking
Implements ISaveToDisk
Public Sub Bark(  ) Implements ISaveToDisk.Save
End Sub
End Class

Notice that you now have to specify in each method implementation what method in the interface you are implementing. You do this by declaring the method, then adding Implements Interface.Method at the end of the method declaration.

Method Overloading

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

    Public Overridable Overloads Sub MakeDeposit(ByVal Amount As Decimal)
        m_Balance += Amount
    End Sub
    Public Overridable Overloads Sub MakeDeposit(ByVal Source As Account)
        m_Balance += Source.Balance
        Source.m_Balance = 0
    End Sub

You do not have to override every form of the overloaded method in a derived class.

Value Versus Reference Type

By default, to use a class, you must declare a variable of the type of class you wish to use and create a new instance of the class. In VB 6 there was a difference between a UDT and a class. The same is true for VB.NET. In VB.NET you can define a UDT using the keyword Structure, as in the following:

Public Structure MyPoint
Public x As Long
Public y As Long
End Structure

Structures are classes that are derived from a class called System.ValueType. Ordinarily, when you create a class that is not derived from System.ValueType, the variable that holds the instance of the object really contains a pointer to a vptr that points to the location in memory where your data is stored. You must create an instance of the class with the New operator. If your class is derived from System.ValueType, the runtime treats your class differently. With a structure, you do not have to call New--when you Dim a variable of the structure type, the system allocates space for the structure automatically and the variable points to the storage directly. If you send in an instance of a structure as a parameter (marked as ByVal), the contents of the structure are put on the stack, and the receiving procedure gets a copy of the structure. If you pass a class instance to a function as a parameter (also marked as ByVal), then you are passing the pointer to the class' storage, and the receiving side gets a copy of the pointer. In the case of the structure, any changes made to the members of the structure do not change the original values in the caller's structure. In the case of the class reference, if you change the value of a class variable, the changes affect the caller's instance of the class, since both the caller and the function share the same instance of the class. Other examples of classes that derive from System.ValueType are the Integer, Decimal, and Boolean classes--any basic datatype. (Yes, Integer, Decimal, and Boolean are actually classes in the runtime that are ultimately derived from System.Object.)

Structures have more functionality than Types had in VB 6. Structures can now have methods and can implement interfaces. Perhaps the most interesting new feature of structures is Boxing. Boxing creates a clone of your structure whenever a reference type is needed. Consider the following code:

Public Interface IAccount
    Sub MakeDeposit(ByVal Amount As Decimal)
    ReadOnly Property Balance(  ) As Decimal
End Interface
 
Public Structure AccountInfo
Implements IAccount
    Public m_Balance As Decimal
    Public Sub MakeDeposit(ByVal Amount As Decimal) Implements _
                                          IAccount.MakeDeposit
        m_Balance += Amount
    End Sub
    Public ReadOnly Property Balance(  ) As Decimal Implements _ 
                                          IAccount.Balance
        Get
            return m_Balance
        End Get
    End Property
End Structure
 
module modMain
    Sub Main(  )
        Dim Acct As AccountInfo
        Acct.MakeDeposit(500)
        Dim AcctBoxed1 As IAccount
        AcctBoxed1 = Acct
        AcctBoxed1.MakeDeposit(300)
        Dim AcctBoxed2 As IAccount
        AcctBoxed2 = AcctBoxed1
        AcctBoxed2.MakeDeposit(300)
        System.Console.WriteLine("Acct.Balance=" & Acct.Balance)
        System.Console.WriteLine("AcctBoxed1.Balance=" & AcctBoxed1.Balance)
    End Sub
end module

This code defines an interface called IAccount. There should be no surprises in the interface definition if you have been following along in this book. The interface, as usual, has a MakeDeposit method and a Balance property. I am implementing the interface in a structure called AccountInfo. This structure has a member called m_Balance to store the balance. It also implements both the MakeDeposit method and the Balance property.

The code example begins by allocating an instance of the AccountInfo structure. Remember that you do not have to call New to allocate a structure's memory. The code then calls the MakeDeposit method to increase the Balance to 500. Next, the code declares a variable named AcctBoxed1 of type IAccount. The code then uses the AcctBoxed1 variable to make another deposit for $300. This is where it gets tricky. In the runtime, an interface is a reference type; it is not a value type, like the AccountInfo structure. So when you assign the reference type to the value type, the system creates a copy of the structure and assigns the pointer of the copy to the reference type variable. In the preceding example, after setting AcctBoxed1 to Acct, there will be two copies of the data members in memory. The second copy has a starting balance of 500 because that's what the structure had before the copy was made. However, when you call the MakeDeposit method through the structure, the values of the original remain at 500, while the balance of the copy increases to 800. The code then creates a second reference type, AcctBoxed2 from AcctBoxed1. Because both are reference types, AcctBoxed2 is set to point to the same memory as AcctBoxed1. Therefore, after calling MakeDeposit for the third time, the original balance in Acct1 is still at $500, but the balance of the reference type is now at $1,100. In fact, when you output the values in the last two lines of code, the value for Acct.Balance will be reported as 500, and the value for AcctBoxed1.Balance will be 1100.

Delegates

VB 6 had a limitation with function pointers. It was possible to get the address of a function in memory with the AddressOf operator. The AddressOf operator returned a Long with the location in memory of the function, but something that I always envied C++ for was that you couldn't take that Long value and turn it back into a function. For example, C++ lets you define what is called a function pointer declaration. You can define a function signature (function name, parameters, and return value) and use the definition as a datatype. With this datatype, you can declare a variable to hold the address to a function with the same signature. Then you can make a method call through the variable. In VB 6 you couldn't do this.

VB.NET now lets you create function pointer datatypes; they are called delegates. Delegates are classes derived from System.Delegate. The following example shows how to define a delegate. Suppose that you want to create a general function for our banking server that reports the balance but that, instead of accepting Account or Checking or Savings, will accept any class that has a subroutine to report the balance. To do this, we can define a delegate with the signature for the ReportBalance function as follows:

Delegate Sub ReportBalanceSig(  )

The delegate declaration defines a datatype that can be set to the AddressOf any function with the same signature. Let's suppose that the BankServer application has the following classes:

Public Class Checking
    Public Sub CheckingBalance(  )
        System.Console.WriteLine("CheckingBalance")
    End Sub
End Class
Public Class Savings
    Public Sub SavingsBalance(  )
        System.Console.WriteLine("SavingsBalance")
    End Sub
End Class

Notice that the two classes, Checking and Savings, each has a method to report the balance, CheckingBalance and SavingsBalance, respectively. These two methods serve the same purpose but do not have the same name and are not implementing any interface. They do, however, have the same signature. Let's now define the ReportBalance function and the client code in a module:

module modMain
    Sub ReportBalance(ByVal Func As ReportBalanceSig)
        Func
    End Sub
    Sub Main(  )
        Dim Check As Checking = new Checking
        Dim Sav As Savings = new Savings
        ReportBalance(AddressOf Check.CheckingBalance)
        ReportBalance(AddressOf Sav.SavingsBalance)
    End Sub
End module

The first function in modMain, ReportBalance, accepts as a parameter a function of type ReportBalanceSig. It does not matter what the function is called; it just needs to have the same signature as the delegate. Notice that the code in ReportBalance simply calls the routine that was sent in. (It looks a little awkward because calling the function can be done by just writing the name of the variable holding the function pointer.) The second function in the module is Sub Main. In Sub Main we create an instance of the Checking class and then an instance of the Savings class. Then, the function calls ReportBalance, passing the address of the CheckingBalance function, followed by a second call to the ReportBalance function passing the address of the SavingsBalance function.

Constructors and Finalizers

There is no more Class_Initialize or Class_Terminate. Every class not derived from System.ValueType and not defined as a structure or an interface has a default constructor. The default constructor has the name New and takes no parameters; it is called when the developer uses the New operator. For example, the following code shows how to write code for the default constructor in the Checking class:

Public Class Checking
    Private m_Balance As Decimal
 
    Public Sub New
        m_Balance = 0
    End Sub
 
    Public Function ReportBalance(  ) As Decimal
        return m_Balance
    End Function
End Class
 
 
module modMain
    Sub Main(  )
        Dim Acct As Checking = New Checking
    End Sub
end module

In this code example, the runtime calls the New method in the class when the developer creates an instance of the class. In this case, this happens in Sub Main.

A more interesting feature is that you can now add parameterized constructors. For example, it may make more sense to require the developer using the Checking class to create the class with an initial balance as follows:

Public Class Checking
    Private m_Balance As Decimal
 
    Public Sub New(ByVal InitialBal As Decimal)
        m_Balance = InitialBal
    End Sub
 
    Public Function ReportBalance(  ) As Decimal
        return m_Balance
    End Function
End Class
 
 
module modMain
    Sub Main(  )
        Dim Acct As Checking = New Checking(500)
    End Sub
end module

In this code, there is a definition for a parameterized constructor. This is done by adding a New function that receives a parameter, in this case the initial balance. As soon as you add a parameterized constructor, the compiler no longer adds the default constructor. This means that the developer cannot just say New Checking without passing a parameter. As you can see in the code for Sub Main, the code creates an instance of Checking passing in the initial balance of $500.

As with other functions, you may overload the constructor. In fact, if you wish to have both the parameterized constructor and the default constructor, you could rewrite the class as follows:

Public Class Checking
    Private m_Balance As Decimal
 
    Public Overloads Sub New
        m_Balance = 0
    End Sub
 
    Public Overloads Sub New(ByVal InitialBal As Decimal)
        m_Balance = InitialBal
    End Sub

There are things to watch for when your class derives from a base class. Constructors are not inherited. If the base class has a default constructor, the system will call the base constructor first before calling your constructor. However, if the base class does not have a default constructor, then you must call a constructor for the base class in your constructor, and the call must be the first call in your constructor. The following code shows you how to do this:

Public Class Account 'base class
    'this class only has a parameterized constructor
    Public Sub New(ByVal InitialBal As Decimal)
 
    End Sub
End Class
 
Public Class Checking 'derived class
Inherits Account
    Public Sub New(ByVal InitialBal As Decimal)
        'call the base class' constructor
        MyBase.New(InitialBal)
    End Sub
End Class
 
module modMain
    Sub Main(  )
        Dim Acct As Account = new Checking(500)
    End Sub
end module

The Account class does not have a default constructor. Therefore, the runtime cannot call the base constructor for your class automatically. You must add a constructor to the derived class, then call the base constructor for your class programmatically. You can refer to your direct base class by using the MyBase object. Notice that you must call the base constructor first before doing anything else in the derived constructor.

Just as you can write constructors for your classes, you can also write finalizers. Finalizers are a little different from destructors because they do not necessarily happen when a client releases your object. When the client creates an instance of your class, the object becomes part of the global managed heap. When all clients release their instances of your object, your object becomes marked for garbage collection. The garbage collector will call your finalizer when it is time to destroy your object, and that may happen any time after all clients have released their references to your object but not sooner. To add a finalizer to the class, you must write a Finalize subroutine. (Interestingly, Finalize is an overridable method in System.Object.) The following code shows how to add a Finalize method:

Public Class Checking
 
    Protected Overrides Sub Finalize(  )
        'do cleanup code here
    End Sub
 
End Class

Exception Handlers

Error catching in VB 6 was done with the Err object and either On Error Resume Next or On Error Goto. If you found this aspect of programming very limiting, you will be glad to know that VB.NET supports exception handling. With exception handling, you write try...catch blocks. The try part of a try...catch block contains the code that you want to execute. Outside of the try block, you write handlers for different kinds of exceptions. Exceptions are classes derived from System.Exception. Because every exception is generated from System.Exeception, you can also write a general exception handler to handle any exception you may get. The System.Exception class has properties that provide very rich error information. Part of the try...catch block is the finally block. The finally block executes at the end of the function whether an exception occurs or not. This block lets you do cleanup for the function. The following code shows how to add a try...catch...finally block:

Public Interface IAccount
End Interface
Public Interface ISaveToDisk
End Interface
Public Class Checking
Implements IAccount
End Class
module modMain
    Sub Main(  )
        Dim Acct As IAccount = new Checking
        Try
        Dim Sav As ISaveToDisk = CType(Acct,ISaveToDisk)
        Catch e As System.InvalidCastException
            System.Console.Writeline("The cast failed")
        Catch e As System.Exception
            System.Console.WriteLine(e)
        finally
            System.Console.WriteLine("cleanup code here")
        End Try
    End Sub
End module

This code defines two interfaces, IAccount and ISaveToDisk. The code also defines a class called Checking. The Checking class implements only IAccount. The code in Sub Main creates an instance of the Checking class by asking for the IAccount interface, then tries to convert the type to ISaveToDisk using the new VB.NET command CType. Since the class does not support the second interface, the system generates an exception: System.InvalidCastException. The code places the cast attempt code within a Try block. Notice that there are different exception handlers after the code inside the Try block. The first exception handler handles System.InvalidCastExceptions only. If the code generates this exception (and the previous code will), the line System.Console.WriteLine ("The cast failed") is executed followed by the code in the Finally block. After the handler for System.InvalidCastException, the code has a general exception handler. This is done with a catch section that either has the word Catch by itself or by catching the System.Exception class.

In VB 6, generating an error was done with Err.Raise. In VB.NET you can create your own exception class derived from System.ApplicationException. System.ApplicationException is derived from System.Exception. Then use the Throw command to generate the exception. The following code shows you how to define your own exception class and generate it:

Public class MyException
             Inherits System.ApplicationException
    Sub New(  )
       MyBase.New("My Error Description")
    End Sub
End class
Module modMain
    Sub Main(  )
        Dim e As MyException = new MyException
        Throw e
    End Sub
End Module

Mixing COM+ and .NET

Many of you are feeling the urge to experiment with .NET components and like the new features in Visual Basic. Most likely, you have already made an investment in COM and would like to know how to use your existing COM components in VB 6 with the new .NET components. Microsoft is very aware of this need and has built in capabilities for mixing the two.

As you know from the previous sections in this chapter, code that runs in the CLR is managed code. It is in IL, and it is run by the common language runtime. The runtime has its own techniques for allocating objects and managing things like thread allocations and so forth. In the VB 6 world, code was unmanaged. The compiler translated your code to native code that the processor could run. Thus, if we are going to mix these two worlds, we must create intermediary components that enable us to travel to and from managed space into unmanaged space. Let's address the idea of using VB.NET components from your Visual Basic 6 code first.

Using VB.NET Components with VB 6

Microsoft provides a tool called tlbexp.exe with the .NET SDK that enables you to create a type library (.TLB) file from an assembly manifest but does not register it. A type library (especially an unregistered one) does not really provide the functionality of a COM server. If we were to try to use a managed DLL as a COM server, the managed DLL would have to have the functionality of an in-process COM server. For example, it would have to have the four entry points to any COM DLL: DllRegisterServer, DllUnregisterServer, DllGetClassObject, and DllCanUnloadNow. It would also have to add the necessary keys to the registry for the SCM to load the DLL. Remember that the SCM must find the CLSID followed by the InprocServer32 key pointing to the DLL.

Unfortunately for COM developers, CLR assembly DLLs do not have COM entry points, nor are they self-registering. However, Microsoft has made the CLR execution engine (MSCorEE.dll ) a full COM server. It can load your assembly at runtime and provide you with a proxy to your .NET classes. Microsoft provides another tool called RegAsm.exe, also included with the .NET SDK, that enables you to register your .NET assemblies. Let's look at the process. Consider the following VB.NET code:

Public Class Inventory
    Private m_Quantity As Integer
    
    Public Sub AddWidgetToInventory(ByVal Amount As Integer)
        m_Quantity += Amount
    End Sub
    
    Public ReadOnly Property Quantity(  ) As Integer
        Get
            Return m_Quantity
        End Get
    End Property
    
End Class

This code declares a single class named Inventory. The Inventory class has a private member named m_Quantity; it stores the number of widgets in inventory. The class also has a public method for adding widgets to inventory, cleverly named AddWidgetToInventory, as well as one property for retrieving the quantity. You can save the preceding code as inventory.vb and compile it as a DLL with the following command:

vbc /t:library inventory.vb

Let's suppose that we would like to use the Inventory class in a VB 6 client program. The best way to do this is to use the RegAsm.exe tool. From a command prompt, locate the inventory.dll .NET assembly and enter the following command:

Regasm inventory.dll /tlb:inventory.tlb

I mentioned earlier that Microsoft has another tool called tlbexp.exe that creates a type library. However, that tool does not automatically register the type library, nor does it add registry keys for a client program to be able to create an instance of your .NET classes. RegAsm does all of these. It adds registry keys so that your classes can be instantiated from COM and, if you use the /tlb command-line switch, it also creates a type library for all public classes in the assembly and registers the type library. After you run the RegAsm.exe tool, you should be able to use your .NET assemblies from VB 6.

RegAsm plays a trick with the registry. If you look at the registry, you should see that RegAsm has added COM registry keys (see Figure 11-5).

Figure 11-5. Registry keys added by RegAsm

 

First of all, in Figure 11-5, the CLSID key with the visible subkeys contains the class identifier of our assembly. In fact, your CLSID should be the same as mine, even if you typed the program from scratch. If this doesn't sound strange to you, then you may need to read Chapter 3 again. In VB 6, if two people compile the same program on different machines, VB generates different GUIDs by default unless there is an existing type library that both machines could reference using project compatibility and binary compatibility. VB 6 uses CoCreateGUID to generate a unique number, and, because it was guaranteed to be unique, it would be different from machine to machine (it would actually be different on the same machine if you compiled twice with no compatibility).

VB.NET does not use GUIDs in the same way as VB 6. They are present just for compatibility with COM, but internally they are never used. What's different about VB.NET is that it assigns a CLSID to each class, but it uses a different algorithm that is based on the combination of the name of the assembly (in our case, inventory) and the name of the class (in our case, also inventory). Therefore, if we name the class and the assembly the same thing, we are going to end up with the same CLSID. This has the potential of two companies having a conflict if they name their assemblies and classes the same. There is a solution to this--the developer can assign the class a specific GUID using the GuidAttribute class in System.Runtime.InteropServices.

Notice from Figure 11-5 that the InprocServer32 key has a number of values. First, notice that the path to the COM server points to MSCorEE.dll. MSCorEE.dll is the DLL that is responsible for loading your assembly into managed space. However, it also serves as a COM entry point. When a COM client requests a class through DLLGetClassObject, MSCorEE looks at the other values in InprocServer32, in particular the Assembly value entry. This value tells MSCorEE the name and location of your assembly, the version, localization information (such as EN_US), and the originator. You should know from reading the earlier sections how to assign an originator to the assembly by compiling it with a private key.

There is a little inconvenience with using RegAsm.exe. If you notice, there is no exact path to your assembly. You could manually set this path in the registry (as seen in Figure 11-6), although this capability may be removed in later versions of the SDK.

Figure 11-6. Adding the exact path to the assembly in the registry

 

Another solution, in fact the optimal one, is to sign the assembly with a private key and add it to the global assembly cache. Once the assembly is in the GAC, you do not have to specify a path to use the assembly. To use the assembly from VB 6, all you have to do is create a program as usual and find the assembly name in the Project References dialog box.

Using VB.NET for versioning VB 6 components

A good use for RegAsm.exe and VB.NET is to serve as a replacement for IDL. If you are hesitant to use IDL to version your components because it is yet a new syntax to learn and it is like C++, then you may want to define your interfaces and manage your GUIDs in VB.NET and use it for versioning purposes. Let's look at a short example of how to define your interfaces in VB.NET and manage the GUIDs using attributes. Examine the following VB.NET code:

<Assembly: System.Runtime.InteropServices.Guid("3C53B8E3-81FC-4645-B65F-ACABE77A79D0")>
 
Imports System.Runtime.InteropServices
 
Interface <Guid("1393732E-8D27-431a-A180-8EDA0E4499E2"), _
          InterfaceType(ComInterfaceType.InterfaceIsIUnknown)> IAccount
    Sub MakeDeposit(ByVal Amount As Currency)
    ReadOnly Property Balance(  ) As Currency
End Interface

The code is in VB syntax. What makes this code different from regular VB is the use of attributes throughout the code. These attributes are used by the various tools, like tlbexp, to dictate how the tool ought to do its job. In this case, attributes are used to control how the type library is generated. Notice that the library name is the name of the assembly. The LIBID for this library is the GUID specified with the Guid attribute at the Assembly level. The Guid attribute is part of System.Runtime.InteropServices. Actually, all the attributes used in the VB.NET code are part of the same assembly. The interface also uses the Guid attribute to assign an IID to the interface. In addition to the Guid attribute, it uses the InterfaceType attribute to tell tlbexp that the interface should be derived from IUnknown (the default is to make it a dual interface).

Once you compile the preceding code as a DLL, you can run the tool tlbexp.exe to generate the type library. Suppose you named your DLL bankinterfaces.dll; you can generate a type library entering the following command in console mode:

tlbexp.exe bankinterfaces.dll 

By default, the tlbexp tool uses the root filename of the DLL to name the type library; thus, the resulting file would be bankinterfaces.tlb. The resulting type library source is as follows:

// Generated .IDL file (by the OLE/COM Object Viewer)
// 
// typelib filename: bankinterfaces.tlb
 
[
  uuid(3C53B8E3-81FC-4645-B65F-ACABE77A79D0),
  version(1.0)
]
library BankInterfaces
{
    // TLib :     // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
    importlib("stdole2.tlb");
 
    // Forward declare all types defined in this typelib
    interface IAccount;
 
    [
      odl,
      uuid(1393732E-8D27-431A-A180-8EDA0E4499E2),
      oleautomation,
        custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "BankInterfaces.IAccount")    
 
    ]
    interface IAccount : IUnknown {
        HRESULT _stdcall MakeDeposit([in] CURRENCY Amount);
        [propget]
        HRESULT _stdcall Balance([out, retval] CURRENCY* pRetVal);
    };
};

Tlbexp.exe creates a type library but does not register it, but in Chapter 6, you learned that you could use regtlib.exe to register the type library. After you register the type library, you could use it like any other type library in Visual Basic. First, add it to your project through the Project References dialog box, then implement it in a concrete class. The interface methods resulting from the preceding VB.NET code would look like the following in VB 6:

Option Explicit
 
Implements IAccount
 
Private Property Get IAccount_Balance(  ) As Currency
End Property
 
Private Sub IAccount_MakeDeposit(ByVal Amount As Currency)
End Sub

Using VB 6 Components with VB.NET

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

Option Explicit
 
Private m_Balance As Currency
 
Public Sub MakeDeposit(ByVal Amount As Currency)
    m_Balance = m_Balance + Amount
End Sub
 
Public Function Balance(  ) As Currency
    Balance = m_Balance
End Function

This is the usual Checking class. Let's say for the sake of argument that this code represents a class in BankServer.DLL. You can create an assembly that contains the definitions in your type library using tlbimp. To do so, you would enter the following command in a command-prompt window in the same directory as your DLL:

tlbimp BankServer.dll /out:BankServerAsm.dll

It is very straightforward; you run the tool, and it will create an assembly you can reference from your .NET program. For example, you may write a client program as follows:

module mainmod
    public Sub Main
        Dim Acct As New BankServer.Checking
        Acct.MakeDeposit(new System.Currency(5000))
        System.Console.WriteLine(Acct.Balance)
    End Sub
End module

To compile the client, you would have to reference the BankServerAsm.DLL assembly as follows:

vbc /t:exe bankclient.vb /r:BankServerAsm.dll

The COM DLL and the .NET assembly do not have to be in the same directory. If at some point you need to debug your Visual Basic code, you can easily do this by running the VB 6 COM code in the VB 6 IDE. You can put breakpoints in the VB 6 code and run the .NET code. The code should stop at your breakpoints. The only requirement to make this work is that you must set the project to binary compatibility with the DLL you used for creating the assembly.

Using COM+ Services

I hesitate to write this section because many things are going to change down the road with respect to using .NET assemblies in COM+ applications. However, for the sake of completion, let's talk about how to add a .NET class to a COM+ application as of Beta 1.

In the future, the MTS (now COM+) team will migrate all the COM+ services to work seamlessly with all the .NET architecture. For now, we must use a few COM interop tricks. In essence, the interaction occurs through the same mechanism explained earlier, by which you must make your .NET class look like a COM class by adding information to the registry and using MSCorEE.dll as the COM server wrapper for your assembly. However, we must also add information to the COM+ catalog. Microsoft provides another tool called RegSvcs.exe that does the job of RegAsm with the /tlb command-line switch, plus adds information about the class to the catalog. In addition, RegSvcs.exe is able to interpret certain attributes from the assembly, which enables you to set the declarative attributes in COM+.

For Beta 1, if you want a .NET class to work with COM+ services, you must derive the class from System.ServicedComponent. Because RegSvcs is going to generate COM information for the registry, you should also manage the IIDs and CLSIDs with attributes. For example, the following VB.NET code shows an interface named IAccount implemented in a Checking class:

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

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

All these properties can be set easily in VB.NET through attributes. The advantage of doing this is that when you run the RegSvcs tool on your VB.NET DLL, the tool will automatically create the COM+ application for you and add your components to it. All the COM+ attributes--not only the ones at the application level, but also the ones at the class interface and method levels--have been replicated as attributes. They are contained in the Microsoft.ComServices namespace.

The sample code also uses the Transaction attribute at the class level to specify that this class requires transactions.

Something that might look strange is that the Checking class implements the IAccount interface and makes the implementation methods private. In addition, it also gives each method a different name. This has nothing to do with the fact that I am going to use this interface in COM+. If you make the implementation methods public, that means that there are two ways of calling the method: through the interface and through a class reference directly. If you make the implementation methods private, the only way to reach the methods is through the interface. Notice also that you do not need to name the implementation methods the same as the interface methods; you must only match the signatures and use the Implements directive at the end of the method. It is interesting that both of these aspects of implementing interfaces in VB.NET (making methods private and giving them different names) are just like implementing interfaces in VB 6--except that somehow it seems clearer in VB.NET.

To compile the code, you use the standard vbc command at the command prompt. Because the code uses a security key file, you have to create one with sn.exe. Also, because the code uses the Microsoft.ComServices assembly, you must reference this assembly in the command line. The following code shows how to compile the code in a command-prompt window:

vbc /t:library bankserver.vb /version:1.0.0.0 /r:Microsoft.ComServices.dll

The next step is to add the assembly to the GAC. To review, that is done with the gacutil.exe tool. Once the assembly is in the GAC, you can run the RegSvcs.exe tool as follows:

RegSvcs /fc bankserver.dll

The /fc switch tells the tool to find an application or create one. The name of the application is defined by the ApplicationName attribute in the preceding source code. Optionally, you can enter an application name in the command line after the assembly name. Figure 11-7 shows the end result of running the RegSvcs tool.

Figure 11-7. BankServer .NET application in Component Services administration program

 

Using the components is just like using any COM+ component. In fact, the following code should present no surprises:

    Dim acct As IAccount
    Set acct = CreateObject("bankserver.Checking")
    Call acct.MakeDeposit(500)
    MsgBox acct.Balance

One thing that may take you by surprise, however, is that as of Beta 1, unless you add an attribute to your class to turn on a specific declarative attribute, the attribute will be turned off by default. One nice thing about creating COM+ components with .NET is that the resulting component uses a different threading model than VB 6 components. If you recall, VB 6 components use the apartment-threading model. VB.NET COM components are marked as using the both-threading model. That means that VB.NET components run in the MTA by default instead of in the STA. Also, VB.NET COM components can be pooled. Pooling VB.NET COM components is beyond the scope of this chapter, but be aware that living in the MTA means that you must handle synchronization issues in your methods.

Summary

In this chapter, you have learned the basics of the .NET architecture. You learned some of the limitations in COM and why Microsoft has created the new architecture. A number of languages are being written to support the new architecture. These languages compile to a processor-independent form of assembly language known as Intermediate Language (IL). Two of the main languages for .NET are VB.NET and C#. Both of these compilers generate IL. When you run a program written in IL, a Just-in-Time compiler converts the code into native machine code.

VB.NET has a number of enhancements over VB 6. Among them are: code inheritance, method overloading, enhanced user-defined types, function pointers, parameterized constructors, and true exception handling.

.NET components follow a different versioning scheme than COM+. When you build an assembly that references another assembly, the client assembly's manifest contains the version number of the referenced assembly. The runtime matches the major and minor numbers in the version for the assembly. You can redirect the runtime to use a different version with a configuration file.

To use an assembly from a VB 6 program, you use a tool called RegAsm.exe. Alternatively, you can use the tlbexp.exe tool to create a type library. However, RegAsm.exe does the job of adding keys to the registry to make the public classes in the assembly creatable from COM. It also builds a type library and registers it. In addition to providing you with a way to use .NET components from COM, you can use this functionality for versioning your existing COM components. To use COM components from .NET, you can use the tlbimp.exe tool to create an assembly from your type library.

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

Back to: COM+ Programming with Visual Basic


O'Reilly Home | O'Reilly Bookstores | How to Order | O'Reilly Contacts
International | About O'Reilly | Affiliated Companies

© 2001, O'Reilly & Associates, Inc.
webmaster@oreilly.com