Export (0) Print
Expand All
Around the World with Visual Basic
Asynchronous Method Execution Using Delegates
Building a Progress Bar that Doesn't Progress
Calling All Operators
Create a Graphical Editor Using RichTextBox and GDI+
Creating A Breadcrumb Control
Creating a Five-Star Rating Control
Creating and Managing Secondary Threads
Data Binding Radio Buttons to a List
Deploying Assemblies
Designing With Custom Attributes
Digital Grandma
Doing Async the Easy Way
Extracting Data from .NET Assemblies
Implementing Callbacks with a Multicast Delegate
Naming and Building Assemblies in Visual Basic .NET
Programming Events of the Framework Class Libraries
Programming I/O with Streams in Visual Basic .NET
Reflection in Visual Basic .NET
Remembering User Information in Visual Basic .NET
Advanced Basics: Revisiting Operator Overloading
Scaling Up: The Very Busy Background Compiler
Synchronizing Multiple Windows Forms
Thread Synchronization
Updating the UI from a Secondary Thread
Using Inheritance in the .NET World
Using the ReaderWriterLock Class
Visual Basic: Simplify Common Tasks by Customizing the My Namespace
What's My IP Address?
Windows Forms Controls: Z-order and Copying Collections
Expand Minimize

From the August 2001 issue of MSDN Magazine

MSDN Magazine
Exploiting New Language Features in Visual Basic .NET, Part 2
Ted Pattison
T
his month is a continuation of the Basic Instincts column that appeared in the May 2001 issue of MSDN® Magazine. In May's column, I discussed how the Visual Basic® language has been enhanced to include many new conveniences. I explained how the language designers have improved the syntax of Visual Basic to promote higher levels of type safety and consistency. I also showed new syntax for dealing with familiar concepts such as properties and arrays.
      This month I'm going to cover three powerful object-oriented features that are going to be new to many experienced programmers who use Visual Basic. In particular, I'm going to discuss shared members, method and property overloading, and parameterized constructors. These features have existed for a while in other languages such as C++ and Java. If you've had no previous exposure to the features using one of these other languages, you should expect that it will take some time for you to master them. However, once you understand a few critical concepts and learn the new syntax for dealing with these features, you'll have design opportunities that you never had with earlier versions of Visual Basic.

Shared Members versus Instance Members

      Shared members have existed in languages such as C++ and Java for years and now they are a welcome addition to Visual Basic. You should note that in C#, C++, and Java, shared members go by the name of "static" members. However, I would argue that the term "shared" members used by Visual Basic is more to the point.
      The fundamental concept behind shared members is fairly simple. Certain fields, methods, and properties can be associated with the class itself rather than an instance of the class. For example, a shared field is allocated in memory only once for the entire class independent of how many objects have been created. An instance field, on the other hand, is allocated in memory once per object. Here's a simple example:
Class Class1
  ' instance field
  Public Field1 As Integer
  ' shared fields
  Public Shared Field2 As Integer
End Class
In this example, Field1 has been defined as an instance field, while Field2 has been defined as a shared field. An instance field like Field1 should be fairly familiar to you. This was the only way you could define a field in earlier versions of Visual Basic. In order to access Field1, you must first create an instance of the class.
Dim obj As New Class1
obj.Field1 = 10
      A shared field such as Field2, on the other hand, opens some new possibilities. Shared members are accessible to clients even when no objects have been created (as long as the member is public, of course). For example, Field2 is created and initialized in memory only once regardless of how many objects have been created from the class. A client can access this shared field using the class name like this.
Class1.Field2 = 10
      Visual Basic allows you to access a shared field in two different ways. The technique shown previously using the class name is usually preferred for readability. The other way to access a shared member is through an object reference. However, this style can be confusing and leads to reduced readability. A shared member isn't really associated with the object behind the object reference; it is associated with the class itself. Look at the following client-side code, which accesses the shared field called Field2.
Dim obj1 As New Class1
obj1.Field2 = 10
Dim obj2 As Class1
obj2.Field2 = 10
In this example, both obj1 and obj2 are reference variables of type Class1. In other words, they are both reference variables that are capable of pointing to an instance of Class1. As you can see from this example, both variables can also be used to access the shared field called Field2. What's interesting to note here is that obj2 has not been assigned to an instance of Class1, yet it can still be used to access a shared member. However, an attempt to access an instance member through an object reference that equals Nothing results in a runtime exception.
      You should be able to see that accessing shared members through reference variables results in somewhat confusing code. For this reason, I recommend that you use the ClassName.MemberName syntax when you write client-side code to access shared members. I'd also like to point out that the designers of C# agree with this point because they only allow access to shared (that is, static) members through use of the class name.
      Shared methods and shared properties are like shared fields in the sense that they can be accessed without the need for the client to create an object beforehand. When you want to add a shared method or shared property to a class, you just define the member using the Shared keyword in the exact same manner you define a shared field, as shown here:
Class Class1
  Shared Sub Method1()
    ' implementation
  End Sub
  Shared Property Property1() As String
    ' implementation  
  End Property
End Class
      Shared methods are great when you want to create a class that exposes a set of utility functions. For example, many classes inside the common language runtime's (CLR) base class libraries contain shared methods. The Math class and the Console class in the System namespace provide good illustrations of this. Here's the code you would write to call shared methods from these classes.
Imports System

Sub Main()
  Dim d1, d2, d3 As Double
  d1 = Math.Sqrt(225)
  Console.WriteLine(d1)
  d2 = Math.Pow(15, 2)
  Console.WriteLine(d2)
  d3 = Math.Log(225)
  Console.WriteLine(d3)
End Sub
      You should observe that some classes shouldn't require clients to create objects. When a design doesn't call for separate data layouts for multiple objects, you can often resort to creating classes that expose shared methods and properties exclusively. In other more complicated designs, you might find that a class needs a mixture of both shared members and instance members.
      Keep in mind that there are a few notable limitations when it comes to writing the implementation for a shared method or a shared property. Shared methods and properties can access other shared members, but they cannot access instance members. That makes sense because a shared member is not associated with a specific object. Your code will not compile if you attempt to access an instance member from a shared method or a shared property. For the same reasons, it is illegal to use the Me keyword inside the implementation of a shared method or a shared property.

Designing with Shared and Instance Members

      When it comes to designing classes with shared members and instance members, the possibilities are only limited by your imagination. I'd like to demonstrate one possible design that uses both kinds of members. This should give you an idea of what's possible with Visual Basic .NET that's not possible with Visual Basic 6.0.
      Let's say you want a class that tracks a unique ID for each object that's created from the class. You could add a shared field to hold a class-wide counter for the objects that have already been created. You could also expose a factory function as a shared method. Doing so would allow clients to create objects without having to use the New operator directly.
      Take a moment to examine the class design in Figure 1. Note that a shared field is only allocated a single time in memory for the class. That means there's only one instance of the field objectCount regardless of how many objects have been created. This makes it possible to create a field that holds a class-wide count of objects.
      You could write the following code to create and use objects from the class in Figure 1.
Dim obj1 As Class1 = Class1.GetNextObject()
Console.WriteLine(obj1.ObjectInfo) ' output = Object #1
Dim obj2 As Class1 = Class1.GetNextObject()
Console.WriteLine(obj2.ObjectInfo) ' output = Object #2
Dim obj3 As Class1 = Class1.GetNextObject()
Console.WriteLine(obj3.ObjectInfo) ' output = Object #3
This example demonstrates a familiar pattern of having a class expose a shared factory method to its clients. This is just one of countless ways to employ shared members. One thing you should observe about this example is that all the code associated with the class is defined inside the class itself. Due to the fact that earlier versions of Visual Basic did not support shared members, programmers often resorted to far less elegant approaches such as maintaining class-specific code in .BAS modules.

Overloading Methods and Properties

      The ability to overload methods and properties is another feature that's been around for a while in other languages such as C++ and Java. Overloading is a technique in which a designer creates two or more members of the same name inside a type. For example, you can create a class that has two different methods named GetCustomer, where one takes a single integer parameter with the customer ID and the other takes a single string parameter with the customer name. Note that you can overload shared members as well as instance members. In addition to overloading members in classes, you can also overload members of structures and interfaces.
      When you overload a method name, each method must have a signature that has a unique parameter list. In other words, methods of the same name must differ in terms of the number of their parameters and/or the type of their parameters. For example, you could create a class with two overloaded methods that looks like the class shown here:
Class CustomerManager
  Overloads Function GetCustomer(ID As Integer) As Customer
    ' implementation
  End Function
  Overloads Function GetCustomer(Name As String) As Customer
    ' implementation
  End Function
End Class
Note that it is illegal to overload two methods based on their return type. Similarly, it is illegal to overload two methods where the parameter lists only differ by use of ByVal and ByRef or by parameter names. Once again, you must make the parameter list of each overloaded method unique with respect to the number and/or type of its parameters.
      Visual Basic .NET requires you to add the Overloads keyword whenever you have two or more methods or two or more properties of the same name. This can be a little tedious. For example, when you create a class with two methods named GetCustomer, you must use the Overloads keyword in front of both methods. However, if you remove one of these methods, there will be only one remaining method named GetCustomer and, consequently, your code will no longer compile until you remove the Overloads keyword. Yes, it can be tedious—but you'll get used to it. (And it's scheduled to change in Beta 2.)
      Writing client-side code against overloaded methods is quite simple. For example, to call the overloaded methods shown in the previous example, you could write the following code:
Dim c1, c2 As Customer
Dim mgr As New CustomerManager
c1 = mgr.GetCustomer(23)
c2 = mgr.GetCustomer("Bob")
As you can see from this example, a call to an overloaded method is directed to one implementation or the other depending on how it defines its parameters. At compile time, the Visual Basic .NET compiler is able to determine which specific overloaded method implementation it should call.
      If there is not an exact match between the number and type of parameters of the caller and a parameter list of an overloaded method, the Visual Basic .NET compiler will try to find a match using a set of promotion rules. For example, the Visual Basic .NET compiler knows it can promote a Short (16-bit integer) to an Integer (32-bit). Examine the following code:
Dim c3 As Customer
Dim ID3 As Short = 48
c3 = mgr.GetCustomer(ID3) ' calls GetCustomer(Integer)
However, when the Visual Basic .NET compiler cannot find a match, you will receive a compile-time error. For example, the caller cannot pass a Long (64-bit integer) when the overloaded method's parameter is based on an Integer:
Dim c4 As Customer
Dim ID4 As Long = 48
C4 = mgr.GetCustomer(ID4) ' doesn't compile without casting
      When you create a class or structure with overloaded methods, you'll find that multiple methods that share the same name often need a common implementation. Since you don't want to write and maintain redundant code, it's important that you learn to call one implementation of an overloaded method from another. Take a moment to consider this simple example.
Class Class1
  Overloads Shared Sub Foo()
    Me.Foo(100) ' forward call passing default value
  End Sub

  Overloads Shared Sub Foo(i As Integer)
    ' implementation
  End Sub
End Class
      If you have two methods named Foo, one implementation can simply call the other by name. As long you pass the appropriate parameter list for the target method when making the call, it's easy to forward the call from one overloaded method implementation to another. You should note that the use of the Me keyword in the previous example is optional.
      Given the definition of Class1 shown previously, the client-side code you would need to write to access these overloaded methods would look like this.
Class1.Foo()
Class1.Foo(99)
These two calls to Foo use different entry points into the class in the sense that they are directed to different method implementations. However, the design of Class1 allows both calls to use a common implementation.
      As you can see from the previous example, overloaded methods can be used to simulate optional parameters with default values. This is a valuable design technique that you should add to your bag of tricks. In fact, you should usually use this design technique rather than a design that uses optional parameters.
      As you probably recall from earlier versions of Visual Basic, you can mark a parameter in a method signature as optional. Note that, unlike earlier versions, Visual Basic .NET requires you to include a default value on all option parameters like this:
Class Class2
  Sub Foo(Optional i As Integer = 100)
    ' implementation
  End Sub
End Class
      Now that I've covered the basics of method overloading, I'd like to point out a few good reasons to use method overloading rather than optional parameters. The first problem with optional parameters is that they are not supported across all languages. For example, programmers using C# cannot take advantage of the optional parameters you define inside your methods. A C# programmer sees an optional parameter as any other mandatory parameter. However, if you use overloaded methods to simulate optional parameters, you can achieve the same result in a language-neutral fashion.
      The second problem with optional parameters is that the default value is compiled into the client's executable image, which can cause unexpected results. For example, imagine you have a DLL that includes a method with an optional parameter having a default value of 10. When you compile a client against this DLL that makes a call to the method and omits the parameter, the client contains the logic to make the call and pass a value of 10 for the parameter.
      What if you modify the code inside the DLL to change the parameter's default value from 10 to 20? When you recompile the DLL and rerun the old client, what value do you expect for the parameter? The client still passes a value of 10, and that's what you should expect because 10 was the default value when the client was compiled. Your change to the default value is not used unless the client is recompiled against the DLL with the new default value. Once again, if you design using overloaded methods instead of optional parameters, you can avoid these problems.
      The last issue I want to point out about optional parameters is that they can create problems when versioning an assembly. For example, imagine you have the following class definition inside a DLL:
Class Class2
  Sub Foo()
    ' implementation
  End Sub
End Class
Assume you've compiled this class into the DLL and you've also compiled several client applications against it. If you add an optional parameter to the method Foo and recompile your DLL, you'll also be forced to recompile all your clients. However, if you keep the implementation of Foo that takes no parameters and add a second overloaded version that takes a parameter, you don't have to recompile any clients.
      You will encounter times when you have to decide between using either optional parameters or overloaded methods. While you might already be familiar with the use of optional parameters, they don't always produce the best results. The overloading of methods and properties represents a new technique that can be confusing at first. However, you should see that there are many reasons to prefer overloading to optional parameters. If you find yourself or another programmer on your team creating methods with optional parameters, consider whether overloading could represent a better design approach.

Parameterized Constructors

      You're probably aware that support for object initialization in earlier versions of Visual Basic has been very limited. In Visual Basic 6.0, for example, your only means for controlling object initialization is to add a custom Class_Initialize method to your class. Let's quickly review what happens when you do this.
      When you supply a custom implementation of Class_Initialize, the Visual Basic runtime is guaranteed to call this method whenever a client creates an object from your class. However, this style of initialization isn't very flexible because the Class_Initialize method cannot take parameters. Since the Class_Initialize method cannot take parameters, there's no way for the client (that is, the object creator) to pass instance-specific data when creating an object. Therefore, the only thing that can be initialized inside Class_Initialize are data that have the same values for every instance of the class.
      Many experienced developers using Visual Basic found that the best way to work around the limitations of Class_Initialize was to add a public method designed exclusively for object initialization. For example, you can add a public method named Init to your class and give it the parameters that are necessary to properly initialize an object. When a class includes a public method for initialization, the client has the responsibility of calling this method after creating a new object. Here's an example of a class that is based on this type of design approach.
Class Person
  Private name As String
  Private age As Integer
  Public Sub Init(n As String, a As Integer)
    name = n
    age = a
  End Sub
End Class
      Given a class definition such as this, the client should call the Init method immediately after creating the object. Here's an example of what the client code usually looks like.
Dim obj As New Person
obj.Init("Bob", 32)
' now access other members
      A design technique such as the one I've just shown is often called two-phase construction. The object is created in phase one and it's initialized in phase two. It's important to keep in mind that designs that use two-phase construction place a responsibility on the programmer writing client-side code against the class. It is incorrect for client code to access any other member defined inside the class before calling Init. If the client-side programmer forgets to call Init, the class code will probably not behave as the author intended. It's unfortunate, but there's nothing you can do as a class author to make the compiler force a client-side programmer to call the Init method.
      The introduction of parameterized constructors provides a much more elegant solution to the problem that I've just described. Let's take a look at how to redesign the Person class using this new feature.
      Unlike previous versions of Visual Basic, Visual Basic .NET does not support the Class_Initialize method. Instead, you add initialization support to your class by adding one or more constructors. You add a constructor to a class by adding a Sub procedure named New. Constructors are very flexible because each one can have its own custom parameter list. Here's an example.
Class Person
  Private name As String
  Private age As Integer

  ' parameterized constructor
  Public Sub New(n As String, a As Integer)
    name = n
    age = a
  End Sub
End Class
Once you've added this parameterized constructor to your class, the client-side programmer can pass the instance-specific initialization data when calling the New operator.
Dim obj1 As Person = New Person("Bob", 32)
' or
Dim obj2 As New Person("Betty", 29)
      A parameterized constructor such as this gives the class author a much greater degree of control. While the Person class shown in the last example allows a client-side programmer to pass initialization data in a call to New, it also prohibits calls to New that don't pass the set of initialization values defined by the constructor's parameter list. For example, given this new definition of the Person class, it is illegal for the client-side programmer to write the following code.
Dim obj As New Person ' does not compile
You should observe that a client-side programmer using the New operator is always calling a constructor. When you call the New operator, there must be a constructor in the class that has a parameter list that matches the parameter list you're passing. Just as with methods and properties, you can overload constructors to provide more flexibility.
      There are a few critical points to understand about how constructors are created and used. First, a class can only be used to create objects if it has one or more constructors. However, this can be confusing because it's possible to create an object from a Visual Basic .NET class that doesn't contain a constructor. How is this possible? The answer is that the Visual Basic compiler automatically adds a constructor to your class when you haven't added one explicitly. For example, imagine you create a class definition like this and compile it into a DLL.
Class Class1
  Private Field1 As String
End Class
Since your class doesn't have a constructor of its own, the Visual Basic compiler adds one for you. If you wrote the same type of constructor yourself, it would look something like this:
Class Class1
  Private Field1 As String

  Sub New()
    ' empty implementation
  End Sub
End Class
      If you're curious or skeptical, you can verify what I've just told you by compiling a Visual Basic .NET class into a DLL and inspecting its metadata using the utility ildasm.exe. This utility allows you to see the class definition that's been generated by the Visual Basic .NET compiler. Note that you will be looking at intermediate language (IL) when you inspect the class definition and that constructors are always named .ctor. When you look at the class definition with ildasm.exe, you should be able to verify that the Visual Basic .NET compiler has added a constructor to classes that don't already have any.
      Any constructor that is automatically added by the Visual Basic compiler takes no parameters. A constructor that takes no parameters goes by a special name: default constructor. Only classes that provide a public default constructor allow a client-side programmer to create an object without passing initialization data like this:
Dim obj As New Class1 ' call to default constructor
      As you have seen, the Visual Basic compiler automatically adds a default constructor to your class when your class hasn't been given a constructor of its own. It's equally important to understand that the Visual Basic compiler doesn't supply a default constructor when you have explicitly added one or more constructors to your class.
      When I think about how constructors work, I like to think of an analogy to the Miranda Rights. Every class has the right to a constructor. If a class cannot afford a constructor, one will be appointed to it by the compiler. When the compiler appoints a constructor to your class, it's more like the public defender than a high-priced lawyer. It's a constructor that takes no parameters and does no explicit initialization. However, this constructor does allow clients to create objects. And for this reason, you should see that it has a purpose.
      Most importantly, note that once you add a parameterized constructor, your class does not automatically get a default constructor. If you want a default constructor in addition to one or more parameterized constructors, you have to explicitly add it to your class because Visual Basic won't.
      When you are designing a new class, always consider whether you need a default constructor. Look at the two class definitions in Figure 2. The big difference between these two classes is that ClassA provides a default constructor and ClassB does not. Now look what happens to the client-side code.
Dim obj1 As New ClassA ' compiles
Dim obj2 As New ClassB ' does not compile 
      In some designs it makes sense to force clients to pass initialization data when creating an object. As you can see from the definition of ClassB, creating a class that doesn't include a default constructor gives you far more control than you had in Visual Basic 6.0. When client-side programmers don't pass the initialization data you want, their code doesn't compile.
      The design of ClassA is different from ClassB. ClassA provides more flexibility to client-side programmers. It has a default constructor that provides default initialization values. As you can imagine, this type of design is appropriate for some situations, but inappropriate for others.
      Going back to my previous example with the Person class, I could extend my design to include a default constructor that looks like the following.
Class Person
  Private name As String
  Private age As Integer

  Overloads Public Sub New()
    name = "John Doe"
    age = -1
  End Sub

  Overloads Public Sub New(n As String, a As Integer)
    name = n
    age = a
  End Sub
End Class
      When you're designing and implementing large classes, you'll probably find yourself overloading constructors. Such a design provides more flexibility to your clients. Note that each constructor has its own implementation. Yet, you'll often create several constructors in a class that all need to run common initialization code. In order to avoid writing and maintaining duplicate code in several constructors, it's important to learn how to call one constructor implementation from another. Let's modify the definition of the Person class once more to call the parameterized constructor from the default constructor.
Class Person
  Private name As String
  Private age As Integer

  Overloads Public Sub New()
    MyClass.New("John Doe", -1)
  End Sub

  Overloads Public Sub New(n As String, a As Integer)
    name = n
    age = a
  End Sub
End Class
      As you can see, it's fairly simple to call one constructor from another. You simply call MyClass.New (or Me.New) as if you were calling a method. Calling an overloaded constructor is just like calling an overloaded method. The parameters in your call must match the parameter list of the overloaded constructor that you want to call.

Adding a Class Constructor

      So far, I've described how to use constructors to initialize objects. These types of constructors are known as instance constructors. In addition to instance constructors, Visual Basic .NET allows you to provide class constructors (also known as shared constructors). The purpose of a class constructor is to initialize class-level data such as shared fields.
      A class constructor is never called by clients—it's called by the CLR. Also note that a class constructor cannot take parameters and cannot be overloaded. Here's an example of a class the supplies its own class constructor.
Class Class1
  Shared Private field1 As String

  ' shared constructor
  Shared Sub New()
    field1 = "some value"
  End Sub
End Class
      When you supply a class constructor, you are guaranteed that the CLR will execute its implementation sometime between the time when the program starts and the time when the class is first accessed by a client. That means you can be sure that the class constructor runs before any client accesses any member of the class. You're guaranteed that the class constructor runs before any instance constructor runs.

Const Fields and ReadOnly Fields

      The programming of the CLR and Visual Basic .NET support the concept of both Const and ReadOnly fields. The use of Const and ReadOnly keywords allows you to document certain assumptions you make about the data you're storing in your fields. It also allows the Visual Basic compiler to perform optimizations and to run extra compile-time checks to ensure the data is being used in the way you intended. Examine the following class definition.
Class Class1
  Const Private PI As Double = 3.141592
  ReadOnly Private Birthday As Date
  Sub New()
    Birthday = System.Date.Now
  End Sub
End Class
This class shows an example of both a Const field and a ReadOnly field. The difference between the two is that the value of Const field must be known at compile time, while the value of a ReadOnly field can be determined at runtime. For that reason, Const fields must be initialized using the inline syntax, as shown with the field named PI in the previous example.
      The value of a Const field cannot vary between different instances of the class. For this reason, the Visual Basic compiler makes all Const fields as implicitly shared. It would be inefficient if Visual Basic allocated a separate instance of a Const field for each object created from the class.
      In contrast to Const fields, the initialization values of ReadOnly fields do not need to be known at compile time. The values of a ReadOnly field can also be different for each object created from the class. However, ReadOnly fields must be initialized when the object is being constructed. This means you can only assign a value to a ReadOnly field inside a constructor or by using the same inline syntax that's required for Const fields. It is illegal to assign a value to a ReadOnly field in any other place.
      The last thing I want to point out is that you can also use the inline initialization syntax for fields that are neither Const nor ReadOnly. The inline style for initialization works for shared fields as well as instance fields. Take a look at the example in Figure 3.
      The inline initialization syntax for fields has the same effect as adding the equivalent initialization code to the beginning of each constructor. For example, the shared fields s1 and s2 (shown in Figure 3) both get initialized when the class constructor runs. The instance fields i1 and i2 both get initialized when the instance constructor runs. If you initialize a field using inline syntax and also assign a value to it in the constructor, the value you assign in the constructor will overwrite the previously assigned value.

Conclusion

      You have just seen three object-oriented programming features that are new to Visual Basic .NET. The addition of shared members, overloading, and parameterized constructors makes Visual Basic .NET a much more powerful language than previous versions and really levels the playing field to give its programmers the same power that other programmers have had in languages such as C++ and Java. However, these features involve a good deal of complexity. The average programmer who uses Visual Basic will have to invest a considerable amount of time and effort to master these features effectively.
      In the next installment of this column, I plan to discuss the support for inheritance in Visual Basic .NET. I will describe how and when you should inherit one class from another. However, all the topics I talked about this month will be very important to any discussion of inheritance. It's important to have a solid grasp on how shared methods, member overloading, and constructors work within a single class before you begin to consider how these features work across classes when using inheritance.

Send questions and comments to Ted at basic@microsoft.com.
Ted Pattison is an instructor and researcher at DevelopMentor (http://www.develop.com), where he co-manages the Visual Basic curriculum. The second edition of Ted's book, \ Programming Distributed Applications with COM and Microsoft Visual Basic 6.0, was published by Microsoft Press in June 2000.

Show:
© 2014 Microsoft