OOP in Visual Basic and .NET


Conceptually, OOP really isn't that complex. Because both humans and programmers interact with real-world objects and instances every day, it's pretty easy to wrap their minds around the idea of programming with objects. But how easy is it to communicate these object concepts to the computer through the Visual Basic compiler and the .NET Framework? Can it be done without weekly sessions on a shrink's comfy sofa? Duh! It's Visual Basic; of course it's easy.

One reason objects are so easy in .NET is that they have to be. Everything in your .NET program is part of an object, and if everything about .NET was hard, you'd be reading a book on Macintosh development right about now. But it's not too hard because the Visual Basic implementation of objects parallels the conceptual ideas of objects.

Classes

Visual Basic uses classes to define objects. The Class keyword starts the definition.

Class Superhero    ' ----- Class-related code goes here. End Class 


That's most of it: the Class keyword, and a name for the class ("Superhero" in this case). All classes reside in a namespace (discussed way back in Chapter 1). By default, your classes appear in a namespace that is named after your project. You can alter this in the project properties (to set the top-level namespace for your assembly) and with the Namespace statement (to indicate relative namespaces from your assembly's top-level namespace).

Namespace GoodGuys    Class Superhero    End Class End Namespace 


You can add any number of classes to a namespace. Classes that use the same name, but that appear in different namespaces, are unrelated.

The members of a class appear between the Class and End Class clauses. You can also split a class's definition into multiple source code files. If you do split up a class like this, at least one of the parts must include the keyword Partial in the definition.

Partial Class Superhero 


As with variable definitions, classes are defined using one of the access modifier keywords: Public, Private, Protected, Friend, or Protected Friend. Flip back to Chapter 6, "Data and Data Types," in the "Variables" section, if you need a refresher course.

The .NET Framework Class Libraries are simply loaded with classes and objects, and they are all pretty much defined with this simple keyword: Class.

Class Members

Calling your class Superhero won't endow it with any special powers if you don't add any members to the class. All class members must appear between the Class and End Class boundaries, although if you use the Partial feature to break up your class, you can sprinkle the members among the different class parts in any way you wish.

There are 11 different kinds of members you can include in your Visual Basic classes. Other books or documents may give you a different number, but they're wrong, at least if they organize things the way I do here.

  • Variable fields. Value type and reference type variables can be added directly to your class as top-level members. As full class members, they are accessible by any code defined in the same class, and possibly by code that uses your class. Variables are defined using one of the access modifiers.

    Class Superhero    Public Name As String    Protected TrueIdentity As String End Class 

  • Constant fields. You define constants just like variable fields, but include the Const keyword. As with local procedure-level constants, you must assign a value to the constant immediately in source code, using literals or simple non-variable calculations.

    Public Const BaseStrengthFactor As Integer = 1 

  • Enumerations. Enumerations define related integral values. Once defined, you can use them in your code just like other integer values.

    Public Enum GeneralSuperPower    Flight    Strength    Speed    VisionRelated    HearingRelated    WaterRelated    TemperatureRelated    ToolsAndGadgets    GreatCostume End Enum 

    Enumerations can also be defined at the namespace level, outside of any specific class.

  • Sub methods. Classes include two types of methods, Subs and Functions. All logic code in your application appears in one of these method types, so don't bother looking for such code in an enumeration. Sub methods perform some defined logic, optionally working on data passed in as arguments.

    Public Sub DemonstrateMainPower( _       ByVal strengthFactor As Integer)    ' ----- Logic code appears here. End Sub 

    The DemonstrateMainPower method, as a public member of your class, can be called either by code within the class, or by any code referencing an instance of your class. This method includes a single parameter, strengthFactor, through which calls to the method send in data arguments.

    You can jump out of a sub method at any time by using the Return statement, or the older pre-.NET Exit Sub statement.

  • Function methods. Function methods are just like sub methods, but they support a return value. You define the data type of the return value with an As clause at the end of the function definition. Assignment of the return value can be done using the Return statement, or by assigning the function name directly within the code.

    Public Function GetSecretIdentity( _       ByVal secretPassword As String) As String    If (secretPassword = "Krypton") Then       ' ----- I created a class field named       '       TrueIdentity earlier.       Return TrueIdentity    Else       GetSecretIdentity = "FORGET IT BAD GUY"    End If End Function 

    If you use the assignment-to-function-name style of return value assignment, use the Exit Function statement to return to the calling code at any time.

  • Properties. Properties combine the ideas of fields and methods. You can create read-write, read-only, or write-only properties through the Get and Set accessors. The following code defines a write-only property:

    Public WriteOnly Property SecretIdentity() As String    Set(ByVal value As String)       TrueIdentity = value    End Set End Property 

  • Delegates. Delegates define arguments and return values for a method, and encase them in a single object all their own. They are generally used to support the event process, callback procedures, and indirect calls to class methods.

    Public Delegate Sub GenericPowerCall( _    ByVal strengthFactor As Integer) 

    Because delegates are pretty generic, they can also be defined at the namespace level, outside of any class definition.

  • Events. Adding events to your class allows consumers of your class to react to changes and actions occurring within a class instance. The syntax used to define events looks a lot like a method definition, but an alternative syntax uses previously defined delegates to indicate the signature of the event.

    ' ----- Non-delegate definition. Public Event PerformPower( _    ByVal strengthFactor) As Integer ' ----- Delegate definition. Public Event PerformPower As GenericPowerCall 

  • Declares. The Declare statement lets you call code defined in external DLL files, although it works only with pre-.NET DLL calls. The syntax for declares closely resembles the syntax used to define methods.

    Public Declare Function TalkToBadGuy _    Lib "evil.dll" (ByVal message _    As String) As String 

    Once defined, an externally declared sub or function can be used in your code as if it were a built-in .NET sub or function definition. The .NET Framework does a lot of work behind the scenes to shuttle data between your program and the DLL. Still, care must be taken when interacting with such external "unmanaged" code, especially if the DLL is named "evil.dll."

  • Interfaces. Interfaces allow you to define abstract classes and, in a way, class templates. A section near the end of this chapter discusses interfaces. Interfaces can also be defined at the namespace level, and usually are.

  • Nested types. Classes can include other subordinate classes for their own internal or public use. If you make such a "child" class public, you can return instances of these classes to code that uses the larger "parent" class. (You can also return private class instances, but the caller wouldn't be able to do much with them.)

    Class Superhero    Class Superpower    End Class End Class 

    You can nest classes to any depth, but don't go overboard. Creating multiple classes within the same namespace will likely meet your needs without making the code overly complex. But that's just my idea; do what you want. It's your code after all. If you want to throw away your life on a career in the movies, that's fine with me.

Adding a nice variety of members to a class is a lot of fun. You can add class members in any variety, in any order, and in any quantity. If you add a lot of members, you might even get a quantity discount on Visual Studio from Microsoft, but I wouldn't hold your breath.

Shared Class Members

Normally, objects (class instances) are greedy and selfish; they want to keep everything to themselves and not share with others. That's why each instance of a class you create has its own version of the data elements defined as class members. Even class methods and properties give the appearance of being distinct for each class instance. It's as if each object was saying, "I've got mine; get your own." It's this attitude that has led to what is now commonly called "class warfare."

In an attempt to promote affability among software components and push for "kindlier and gentler" classes, Microsoft included the Shared keyword in its class design. The Shared keyword can be applied to Variable field, Sub method, Function method, and property members of your class. When defined, a shared member can be used without the need to create an instance of that class. You reference these shared members using just the class name and the member name.

Class ClassWithSharedValue    Public Shared TheSharedValue As Integer End Class ...later, in some other code... ClassWithSharedValue.TheSharedValue = 10 


Shared members are literally "shared" by all instances of your class, and if public, by code outside of the class as well. Because they don't require an object instance, they are also limited to just those resources that don't require an object instance. This means that a shared method cannot access a non-shared variable field of the same class. Any class members that are not marked Shared are known as instance members.

Overloaded Members and Optional Arguments

Overloading of a method occurs by attaching the Overloads keyword to each of the overloaded members.

Class House    Public Overloads Sub PaintHouse()       ' ----- Use the same color(s) as before.    End Sub    Public Overloads Sub PaintHouse(ByVal baseColor As Color)       ' ----- Paint the house a solid color.    End Sub    Public Overloads Sub PaintHouse(ByVal baseColor As Color, _          ByVal trimColor As Color)       ' ----- Paint using a main and a trim color.    End Sub    Public Overloads Sub PaintHouse(ByVal baseColor As Color, _          ByVal coats As Integer)       ' ----- Possibly paint with many coats, of paint       ' that is, not of fabric.    End Sub End Class 


When you call the PaintHouse method, you must pass arguments that match one of the overloaded versions. Visual Basic determines which version to use based on the argument signature. If you pass the wrong type or number of arguments, the program will refuse to compile.

Two of the overloaded members in this class look alike, except for the second coats argument.

Public Overloads Sub PaintHouse(ByVal whichColor As Color) Public Overloads Sub PaintHouse(ByVal baseColor As Color, _    ByVal coats As Integer) 


Instead of defining two distinct methods, I could have combined them into a single method, and defined an optional argument for the coats parameter.

Public Overloads Sub PaintHouse(ByVal baseColor As Color, _    Optional ByVal coats As Integer = 1) 


The Optional keyword can be used on any number of parameters, but no non-optional parameters can appear after them; the optional arguments must always be last in the list. Although the calling code might not pass a value for coats, .NET still requires that every parameter receive an argument. Therefore, each optional argument includes a default value using a simple assignment within the parameter definition. The optional argument coats uses a default value of 1 through the "= 1" clause.

Inheritance

Visual Basic supports inheritance, the joining of two classes in an ancestor-descendant relationship. To implement inheritance, define the base class, and then add the derived class using the keyword Inherits. What a surprise!

Class Animal    ' ----- Animal class members go here. End Class Class Mammal    Inherits Animal    ' ----- All members of Animal are automatically    '       part of Mammal. Add additional Mammal    '       features here. End Class 


The Inherits statement must appear at the start of the class definition, before the definition for any class members. It must include the name of exactly one other class, the base class. If you split up your derived class using the Partial keyword, you only need to use the Inherits statement in one of the parts. And because a derived class can only use a single base class, you're pretty much limited to using the Inherits statement only once per class. (The base class can be used in several different derived classes, and the derived class can further be used as a base class for other derived classes.)

Derived classes automatically receive all defined members of the base class. If a derived class needs to provide special functionality for a member defined in the base class, it overrides the member in the base class. This is a two-step process: (1) the base class must allow its member to be overridden with the Overridable keyword; and (2) the base class must supply the overriding code using the Overrides keyword.

Class Animal    Public Overridable Sub Speak()       MsgBox("Grrrr.")    End Sub End Class Class Canine    Inherits Animal    Public Overrides Sub Speak()       MsgBox("Bark.")    End Sub End Class 


Any class that derives from Animal can now supply its own custom code for the Speak method. But the same is true for classes derived from Canine; the Overridable keyword is passed down to each generation. If you need to stop this attribute at a specific generation, use the NotOverridable keyword. This keyword is valid only when used in a derived class, because base class members are non-overridable by default.

Class Canine    Inherits Animal    Public NotOverridable Overrides Sub Speak()       MsgBox("Bark.")    End Sub End Class 


There are times when it is not possible to write a truly generic method in the base class, and you want to require that every derived class define its own version of the method. Using the MustOverride keyword in the member definition enables this requirement.

Class Animal    Public MustOverride Sub DefenseTactic() End Class 


Members marked as MustOverride include no implementation code of their own because it would go unused. (Also notice that DefenseTactic has no closing "End Sub" statement.) Because there is no code associated with this member, the entire Animal class has a deficiency. If you created an instance of Animal and called its DefenseTactic method, panic would ensue within the application. Therefore, it is not possible to create instances of classes that contain MustOverride members. To note this limitation, the class is also decorated with the MustInherit keyword.

MustInherit Class Animal    Public MustOverride Sub DefenseTactic() End Class 


It won't be possible to create an instance of Animal directly, although you can derive classes from it, and create instances of those classes. Also, you can create an Animal variable (a reference type) and assign an instance of an Animal-derived class to it.

Dim zooMember As Animal Dim monkey As New Simian ' Simian is derived from Animal zooMember = monkey 


Such code doesn't really seem fair to the base class. I mean, it defined all the core requirements for derived classes, but it doesn't get any of the credit because it can't be directly instantiated. But there is a way for a base class to control its own destiny, to take all of the glory for itself. It does this with the NotInheritable keyword.

NotInheritable Class Animal End Class 


The only way to use a NotInheritable class is to create an instance of it; you cannot use it as the base class of another derived class.

Inherits, MustInherit, NotInheritable, Overrides, Overridable, NotOverridablethis certainly isn't your grandmother's Visual Basic anymore. And there's still one more of these inimitable keywords: Shadows. When you override a base class member, the new code must use a definition that is identical to the one provided in the base class. That is, if you override a function method with two String arguments and an Integer return code, the overriding code must use that same signature. Shadowed members have no such requirements. A shadowed member matches an item in the base class "in name only;" everything else is up for grabs. You can even change the member type. If you have a Sub method named PeanutButter in a base class, you can shadow it in the derived class with a variable field (or constant, or enumeration, or nested class) also named PeanutButter.

Class Food    Public Sub PeanutButter()    End Sub End Class Class Snack    Inherits Food    Public Shadows PeanutButter As String       ' Hey, it's not even a "Sub" End Class 


Without the Shadows keyword in the Snack class, a compile-time error would occur.

Creating Instances of Classes

Step one: designing classes. Step two: deriving classes. Step three: creating class instances. Step four: cha-cha-cha. Visual Basic uses the New keyword to create instances of your custom classes.

Dim myPet As Animal = New Animal ' ----- Or... Dim myPet As New Animal ' ----- Or... Dim myPet As Animal myPet = New Animal 


The instance can then be used like any other .NET instance variable. Member access occurs using "dot" notation.

myPet.Name = "Fido" 


You can also (within reason) pass instance variables between their base and derived variations.

Dim myPet As Animal Dim myDog As Canine myDog = New Canine myDog.Name = "Fido" myPet = myDog       ' Since Canine derives from Animal MsgBox(myPet.Name)  ' Displays "Fido" 


If you have Option Strict set to On, there will be limits on your ability to convert between types, especially narrowing conversions (where the source data type will not always "fit" in the target variable). In such cases, you must use the CType function (or one of a few similar .NET and Visual Basic supplied functions) to enable the conversion.

myDog = CType(myPet, Canine) 


Referring to class instances is simply a matter of referring to the variable or object that contains the instance. That is true for code that uses an instance from outside of the class itself. For the code within your class (such as in one of its methods), you refer to members of your class as if they were local variables (with no qualification), or use the special Me keyword.

Class Animal    Public Name As String    Public Sub DisplayName()       ' ----- Either of these lines will work.       MsgBox(Name)       MsgBox(Me.Name)    End Sub End Class 


A similar keyword, MyClass, usually acts like the Me keyword, but it has some different functionality when a class instance is stored in a variable from a different (base or derived) class type. For instance, if you create an instance of Canine, but store it in an Animal variable, references using Me will focus on the Canine code, whereas references to MyClass will focus on the Animal code. I won't be using MyClass in the Library Project, and for most simple uses of class instances, you will never use it either. But there are times when it is important to differentiate between base and derived code, and this is the way to do it.

The MyBase keyword references elements of the base class from which the current class derives. It references only the closest base class; if you have a class named Class5 that derives from Class4, which in turn derives from Class3, which derives from Class2, which derives from Class1, which eventually derives from System.Object, references to MyBase in the code of Class5 refer to Class4. Well, that's almost true. If you try to use MyBase.MemberName and MemberName doesn't exist in Class4, MyBase will search back through the stack of classes until it finds the first definition of MemberName.

Class Animal    Public Overridable Sub ObtainLicense()       ' ----- Perform Animal-specific licensing code.    End Sub End Class Class Canine    Inherits Animal    Public Overrides Sub ObtainLicense()       ' ----- Perform Canine-specific licensing code, then...       MyBase.ObtainLicense()  ' Calls code from Animal class    End Sub End Class 


Constructors and Destructors

Class instances have a lifetime: a beginning, a time of activity, and finally, thankfully, an end. The beginning of an object's lifetime occurs through a constructor; its final moments are dictated by a destructor before passing into the infinity of the .NET garbage collection process.

Each class includes at least one constructor, whether explicit or implicit. If you don't supply one, .NET will at least perform minimal constructor-level activities, such as reserving memory space for each of the instance variable fields of your class. If you want a class to have any other startup-time logic, you must supply it through an explicit constructor.

Constructors in Visual Basic are Sub methods with the name New. A New constructor with no arguments acts as the default constructor, called by default whenever a new instance of a class is needed.

Class Animal    Public Name As String    Public Sub New()       ' ----- Every animal must have some name.       Name = "John Doe of the Jungle"    End Sub End Class 


Without this constructor, new instances of Animal wouldn't have any name assigned to the Name field. And because String variables are reference types, Name would have an initial value of Nothing. Not very user friendly. A default constructor gives you a chance to provide at least the minimum needed data and logic for a new instance.

You can provide additional custom constructors by adding more New methods, each with a different argument signature.

Class Animal    Public Name As String    Public Sub New()       ' ----- Every animal must have some name.       Name = "John Doe of the Jungle"    End Sub    Public Sub New(ByVal startingName As String)       ' ----- Use the caller-supplied name.       Name = startingName    End Sub    Public Sub New(ByVal startingCode As Integer)       ' ----- Build a name from a numeric code.       Name = "Animal Number " & CStr(startingCode)    End Sub End Class 


The following code demonstrates each constructor.

MsgBox((New Animal).Name)    ' Displays "John Doe of the Jungle" MsgBox((New Animal("Fido")).Name)    ' Displays "Fido" MsgBox((New Animal(5)).Name)    ' Displays "Animal Number 5" 


You can force the consumer of your class to use a custom constructor by excluding a default constructor from the class definition. Also, if you're deriving your class from anything other than System.Object, it's usually a good idea to call the base class's constructor as the first line of your derived constructor, although the default constructor in the base class will be called by default.

Class Canine    Inherits Animal    Public Sub New()       MyBase.New() ' Calls Animal.New()       ' ----- Now add other code.    End Sub End Class 


Killing a class instance is not as easy as it might seem. When you create local class instances in your methods, they are automatically destroyed when that method exits if you haven't assigned the instance to a variable outside of the method. If you create an instance in a method, and assign it to a class member, it will live on in the class member for the lifetime of the class, even though the method that created it has exited.

But let's think only about local instances for now. An instance is destroyed when the routine exits. You can also destroy an instance immediately by setting its variable to Nothing.

myDog = Nothing 


Setting the variable to a new instance will destroy any previous instance stored in that variable.

myDog = New Canine myDog.Name = "Fido" myDog = New Canine ' Sorry Fido, you're gone 


When an object is destroyed, .NET calls a special method named Finalize, if present, to perform any final cleanup before removing the instance from memory. Finalize is implemented as a Protected method of the base System.Object class; you must override this method in your class to use it.

Class Animal    Protected Overrides Sub Finalize()       ' ----- Cleanup code goes here. Be sure to call the       ' base class's Finalize method.       MyBase.Finalize()    End Sub End Class 


So what's with that crack about killing instances being so hard? The problem is that .NET controls the calling of the Finalize method; it's part of the garbage collection process. The Framework doesn't continually clean up its garbage. It's like the service at your house; it gets picked up by the garbage truck once in a while. Until then, it just sits there, rotting, decaying, decomposing, and not having its Finalize method called. For most objects, this isn't much of a problem; who cares if the memory for a string gets released now or 30 seconds from now? But there are times when it is important to release acquired resources as quickly as possible. For instance, if you acquire a lock on an external hardware resource and only release it in the destructor, you could be holding that lock long after the application has exited. Talk about a slow death.

There are two ways around this problem. One way is to add a separate cleanup method to your class that you expect any code using your class to call. This will workuntil some code forgets to call the method. (You should therefore also call this routine from the destructor.) The second method is similar, but it uses a Framework-supplied interface called IDisposable. (I'll talk about interfaces in a minute, so don't get too worried about all the code shown here.)

Class Animal    Implements IDisposable    Protected Overrides Sub Finalize()       ' ----- Cleanup code goes here. Be sure to call the       ' base class's Finalize method.       MyBase.Finalize()    End Sub Public Overloads Sub Dispose() _       Implements IDisposable.Dispose       ' ----- Put cleanup code here. Also make these calls.       MyBase.Dispose() ' Only if base class is disposable.       System.GC.SuppressFinalize(Me)    End Sub End Class 


The SuppressFinalize method tells the garbage collector, "Don't call Finalize; I've already cleaned up everything." Any code that uses your class will need to call its Dispose method to perform the immediate cleanup of resources. So it's not too different from the first method, but it does standardize things a bit. Also, it enables the use of the Visual Basic Using statement. This block statement provides a structured method of cleaning up resources.

Using myPet As New Animal    ' ----- Code here uses the myPet. End Using ' ----- At this point, myPet is destroyed, and Dispose is '       called automatically by the End Using statement. 


Interfaces

The MustOverride and MustInherit keywords force derived classes to implement specific members of the base class. But what if you want the derived class to implement all members of the base class? You could use MustOverride next to each method and property, but a better way is to use an interface. Interfaces define abstract classes, classes consisting only of definitions, no implementation. (OOP purists will point out that a class with even just one MustOverride flag is also an abstract class. Fine.) The Interface statement begins the interface definition process. By convention, all interface names begin with the capital letter I.

Interface IBuilding    Function FloorArea() As Double    Sub AlterExterior() End Interface 


As you see here, the syntax is a somewhat simplified version of the class definition syntax. All interface members are automatically public, so access modifiers aren't included. Only the definition line of each member is needed because there is no implementation. In addition to function and sub methods, interface members also include properties, events, other interfaces, classes, and structures. Interfaces can also derive from other interfaces (using the Inherits keyword), and automatically include all of the members of the base interface.

Classes make use of interfaces as they wish. It's not like inheritance, where all base members become part of the derived class. A class can pick and choose which interface members to implement. Often you'll implement all interface members, but as a class designer, you have the power. You attach interfaces to your class using the Implements keyword. This same keyword is used later to indicate which class member defines which interface member.

Class House    Implements IBuilding    Public Function FloorArea() As Double _          Implements IBuilding.FloorArea       ' ----- Add implementation here.    End Function    Public Sub PaintHouse() Implements IBuilding.AlterExterior       ' ----- Add implementation here.    End Sub End Class 


Class implementations of interface members are not required to maintain the original interface member name (although the argument signature must match the one in the interface). In the sample code, the FloorArea kept the name of the equivalent interface member, but the AlterExterior member was implemented using the PaintHouse method. This makes possible some interesting code.

Dim someHouse As New House Dim someBuilding As IBuilding someBuilding = someHouse someBuilding.AlterExterior()  ' Calls someHouse.PaintHouse() 


Classes can only inherit from a single base class, but there is no limit on the number of interfaces that a class can implement.

Class House    Implements IBuilding, IDisposable 


Also, a single class member can implement multiple interface members.

Public Sub PaintHouse() Implements _    IBuilding.AlterExterior, IContractor.DoWork 


So, why use interfaces? Interfaces provide a generic way to access common functionality, even among objects that have nothing in common. Classes named Animal, House, and Superhero probably have nothing in common in terms of logic, but they may all need a consistent way to clean up their resources. If they each implement the IDisposable interface, they gain that ability without the need to derive from some common base class.

Modules and Structures

In addition to classes, Visual Basic provides two related object definition features: structures and modules. Although they have different names than "class," they still act a lot like classes, but with different features enabled or disabled.

Modules provide a place to include general global code and data values in your application or assembly. All members of a module are Shared. In fact, a module acts just like a class with the Shared keyword added to each member, yet with one major difference: There is no inheritance relationship allowed with modules. You cannot create a derived module from a base class or module, nor can you use a module as a base for any other type. Modules are a carryover from pre-.NET versions of Visual Basic, which included "Modules" for all non-Form code.

Friend Module GenericDataAndCode    ' ----- Application-global constant.    Public Const AllDigits As String = "0123456789"    ' ----- Application-global function.    Public Function GetEmbeddedDigits( _          ByVal sourceString As String) As String    End Sub End Module 


You cannot create an instance of a module. As with classes, modules appear in the context of a namespace. Unlike a class with shared members, you do not need to specify the module name to use the module member. All module members act as global variables and methods, and can be used immediately in any other code in your application without further qualification.

Structures are much more like classes than are modules. Classes implement reference types, but structures implement value types. All structures derive from System.ValueType (which in turn derives from System.Object). As such, they act like the core Visual Basic data types, such as Integer. You can create instances of a structure using the same syntax used to create class instances. However, you cannot use a structure as the base for another derived structure. And while you can include a constructor in your structure, destructors are not supported.

Because of the way that structures are stored and used in a .NET application, they are well suited to simple data types. You can include any number of members in your structure, but it is best to keep things simple.




Start-to-Finish Visual Basic 2005. Learn Visual Basic 2005 as You Design and Develop a Complete Application
Start-to-Finish Visual Basic 2005: Learn Visual Basic 2005 as You Design and Develop a Complete Application
ISBN: 0321398009
EAN: 2147483647
Year: 2006
Pages: 247
Authors: Tim Patrick

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net