The Basic programming language began as a procedural language, based on variables, function calls, and statements. It is evolving toward an object-oriented language, based on objects, properties, methods, and collections.
Visual Basic has always had support for objects, even though you couldn't always create objects in VB. The ability to create objects from classes was added in VB 4.0, while the ability to create an ActiveX object is new in VB 5.0.
But while purists would argue that Visual Basic isn't a fully object-oriented language (mainly because it doesn't have all the object-oriented features of C++, the 800-pound gorilla of object-oriented languages), it has become more object oriented over time.
You have already seen a number of object-oriented concepts at work in Visual Basic; indeed, ActiveX control creation itself demonstrates a number of object-oriented concepts, such as encapsulation. In the remainder of this chapter you'll see examples of Visual Basic's object-oriented features as they apply to control creation.
An object-oriented system focuses on the elements of the problem (the objects) rather than the method of getting there (the procedures).
When designing an object-oriented system, you often find yourself working backward from the solution (for example, a fully functional inventory system for an ice cream factory) to the objects involved in that solution (a tub of ice cream, a freezer, a truck) to the business rules that govern those objects (a truck can hold 50 tubs of ice cream, a freezer must be kept at 0 degrees Celsius). You may find that working backwards in this way represents a bit of a stretch if you're accustomed to procedural programming, but in time you'll probably find that focusing on the problem rather than the solution is a more natural way to program.
An object-oriented system contains language elements that provide the following:
So far in this book, you've seen how to build ActiveX controls that act as the basis for user-interface objects. In this chapter, you'll see how to use classes, elements of code that enable you to apply these object-oriented principles to your VB applications.
When you create a class, you're giving yourself the ability to instantiate objects in your application. But unlike ActiveX controls, objects created from classes:
Consider the ice cream factory example introduced earlier in this chapter. If you get a job as a programmer for an ice cream factory, there are certain elements of the business you know will change very infrequently, if ever: the freezing point of orange sherbet, or the number of quarts in a gallon, for example. These concepts are referred to as business rules. It makes sense to abstract these concepts into objects early in the game. If you do this, you'll be more efficient. You'll also suffer fewer (ice cream) headaches, because you won't have to rewrite the code that converts quarts to gallons in each new program you write; you'll simply add a reference to your already existing business rules contained in the classes you've authored. This goal of code reuse is another prime goal of object-oriented programming.
Much of the programming of classes in Visual Basic will be familiar to you from your work with ActiveX controls. For example, when you create a public subroutine in your class, it is exposed as a method of your object. You can also include Property Let and Property Get procedures in your class in order to expose properties of the objects created from the class.
Although you may get the impression that classes are a watered-down version of ActiveX controls, it happens that classes are a very relevant part of control creation. Many controls (such as the Data control, for example) expose their own object models. In order to implement such object models, you must understand how classes work.
Every class you create exists in its own file, known as a class module. In this example, you'll create a class that will store and process Internet uniform resource locators (URLs). Later, we'll incorporate this class into a collection, and finally, you'll see how to incorporate the collection of URLs into an ActiveX control.
To create the class, do the following:
' Declarations Private mstrDescription As String Private mdatDateCreated As Date Private mstrURL As String Public Property Get Description() As String Description = mstrDescription End Property Public Property Let Description(ByVal strNewValue As String) mstrDescription = strNewValue End Property Public Property Get URL() As String URL = mstrURL End Property Public Property Let URL(ByVal strNewValue As String) mstrURL = strNewValue End Property Public Property Get DateCreated() As Date DateCreated = mdatDateCreated End Property Public Property Let DateCreated(ByVal datNewValue As Date) ' this value is read-only; raise an error Err.Raise 512 + vbObjectError, "URL", "Property is read-only" End Property
Private Sub Class_Initialize() mdatDateCreated = Now End Sub
Public Sub Navigate() If mstrURL <> "" Then Dim objExplorer As Object Set objExplorer = CreateObject("InternetExplorer.Application") objExplorer.Navigate mstrURL objExplorer.Left = 20 objExplorer.Top = 20 objExplorer.Visible = True Else Err.Raise 1025 + vbObjectError, "URL", _ "No URL has been specified." End If End Sub
Now that you have a minimal class defined, you can test it in an application. To do this:
Private Sub Command1_Click() Dim MyURL As New URL MyURL.Description = "The News Babe Page" MyURL.URL = "http://www.well.com/user/jeffreyp/newsbabe1.html" Debug.Print "Created: " & MyURL.DateCreated MyURL.Navigate End Sub
The Dim statement at the beginning of this procedure declares the object variable that will store the instance of the object defined by the URL class. (The rule in Visual Basic is that all object variables must be declared explicitly, even if your code has no Option Explicit.)
You can see that once the URL object has been instantiated, you can address its properties and methods just as you would any other object. The complexity involved in calling Internet Explorer as an Automation object has been abstracted behind your class' Navigate method; by placing the Automation code in a method of your class, you've essentially reduced the lines of code required to display a Web page from ten to three.
A collection is a group of related data elements. In Visual Basic, a collection represents a group of variables or objects. You use collections as a handy way to organize program data.
If you're accustomed to using arrays to store data in Visual Basic, you may want to consider using collections instead. Collections have a number of built-in tools that make manipulating data easier-and, in some cases, more efficient-than manipulating arrays.
Using collections is easier than using arrays because collections provide a predefined set of methods for manipulating data. For example, when you have an array of 10 elements and you want to add an 11th element, you have to write several lines of code, redimensioning the array and then adding the new element. The collection, in contrast, has an inherent Add method. You don't have to write any code to implement this method, so adding a new element to the collection requires a grand total of one line of code.
In the next sections, you'll get a look at how to add new usefulness to your URL class by building a class that contains a collection.
You use the inherent methods of the collection to manipulate its
members. (I know that sounds perverse; stick with me, it gets
even better later on.) Collections have the properties and methods
listed in Table 16.1.
|Add method||Adds an item to the collection|
|Delete method||Removes an item from the collection|
|Item method||Returns an element of the collection|
|Count property||The number of items in the collection|
If your collection is wrapped in a class module, you can extend the set of properties and methods of your collection, making it even more flexible (we'll do that later on in this chapter). For now, we'll implement a simple collection and expand upon it.
In this example, you'll take your first stab at using a collection to store a number of URLs. This first example won't be perfect, but it will demonstrate the rudiments of how collections work; we'll refine the example in the following sections of this chapter.
To set up your collection, modify the demonstration code in Form1 so that rather than launching a browser, it adds a few URLs to a collection of URLs called colBookmarks. To do this, modify the existing code so it looks like this:
' Declarations Dim colBookmarks As New Collection Private Sub Command1_Click() Dim MyURL1 As New URL, MyURL2 As New URL, MyURL3 As New URL MyURL1.Description = "The News Babe Page" MyURL1.URL = "http://www.well.com/user/jeffreyp/newsbabe1.html" 'Debug.Print "Created: " & MyURL1.DateCreated 'MyURL.Navigate colBookmarks.Add MyURL1 MyURL2.Description = "Electric Minds" MyURL2.URL = "http://www.minds.com/" colBookmarks.Add MyURL2 MyURL3.Description = "The Onion" MyURL3.URL = "http://www.theonion.com/" colBookmarks.Add MyURL3 Debug.Print colBookmarks.Count End Sub
You declare the collection colBookmarks in the Declarations section of the form for two reasons: first, because it is an object variable, it must be explicitly declared somewhere; second, because it is a collection, you want its lifetime to be that of the form, not of the procedure. Declaring the variable in the Declarations section of the form, then, causes the collection to persist throughout the lifetime of the form.
At this point, try a few experiments to see how to use the methods of the collection. To do this:
The question mark character is shorthand for the Print method.
Unlike arrays in Visual Basic, collections are one-based, which means that the first item in the collection is numbered 1. (In contrast, the first item in a default array is numbered 0.) There is a problem with using a collection in this manner, however. To see this problem, type the following code in the Immediate window:
colBookmarks.Add "Yadda yadda yadda." ?colBookmarks.Count
No error is returned, and the Immediate window reports the count of items in the collection to be four. This is a problem, because this is supposed to be a collection of URL objects, not a collection of random gibberish text strings.
You can avoid this problem by wrapping your collection in a class. Among the benefits of classes we've discussed earlier in this chapter is that putting collections in classes enables you to enforce type-safety. This prevents procedures in your code from incorrectly adding the wrong data types to your collections.
In order to type-safe your collection, you can place it in a class. To do this:
' Declarations Private mcolBookmarks As New Collection Public Property Get Count() As Long Count = mcolBookmarks.Count End Property Public Function Add(theURL As URL) mcolBookmarks.Add theURL End Function Public Function Item(ByVal varKey As Variant) As URL Set Item = mcolBookmarks.Item(varKey) End Function
Note that if you were building a real Bookmarks class, you'd also include code to wrap the collection's other methods, such as Remove. But this code will do for our demonstration.
You can see that these procedures access the private collection mcolBookmarks in much the way that an ActiveX control delegates properties and methods of its constituent controls. The difference in this method of accessing the collection, though, is that the Add and Item methods of the collection are strongly typed. That is, they will only accept URL objects as their input.
Also, note that at no time does the user of this class have direct access to the data stored in mcolBookmarks; she must instead go through the class's interface to get to that data. This is a good thing, because it provides your collection with a simple and safe interface. If something changes down the road (such as the addition of a new property of the URL object), you can be fairly certain that the change will not break existing code.
You'll want to make the Item method of your collection class the interface default of the class. This is so the following two lines of code will be the same:
In other words, you're adding another feature of the default collection object to your collection class.
You've seen this kind of thing before, when you set methods and properties of your control projects as the default. However, the way to do this for classes is slightly different than for controls. To set the Item method as the default procedure for your collection class:
The Bookmarks collection class is now ready to test:
Private Sub Command2_Click() Dim myMarks As New Bookmarks Dim myURL As New URL With myURL .Description = "The News Babe Page" .URL = "http://www.well.com/user/jeffreyp/newsbabe1.html" End With myMarks.Add myURL Stop End Sub
Programmers often cast a wary eye on certain elements of object-oriented programming because they fear they're going to spend time adopting a new paradigm only to find their applications run dog-slow.
It is true that collections require more memory overhead than conventional arrays. This is because collections are variants, and variants always consume 16 bytes, no matter which type of data is stored in them. For example, if you use a variant to store a lowly integerÐa data type that normally requires two bytes of storageÐthe variant still requires 16 bytes to store it. However, this increased memory consumption may not necessarily lead to a performance degradation, depending on how you use collections.
When comparing collections vs. arrays, the real measure of performance comes when you consider that you don't have to iterate through an entire collection to retrieve a particular value. Instead, you can use a For Each block, which is always more efficient than iterating through an array.
Now that you have functional URL and Bookmarks classes, you can incorporate them into a control. Not only will this example demonstrate how you can use a class module as a component of a control project. It will also demonstrate how easy it is to re-use code in class modules. To do this:
Make sure you add these classes to your control project, not to the EXE test project.
' Declarations Private mbkmBookmarks As New Bookmarks Private Sub UserControl_Resize() List1.Move 0, 0, ScaleWidth, ScaleHeight End Sub Public Sub AddURL(strDescription As String, strURL As String) Dim urlNew As New URL With urlNew .Description = strDescription .URL = strURL End With mbkmBookmarks.Add urlNew List1.AddItem urlNew.Description End Sub Private Sub List1_DblClick() mbkmBookmarks.Item(CLng(List1.ListIndex) + 1).Navigate End Sub
You can see that the AddURL subroutine (which, because it is public, is exposed as a method of the HotList control) adds the URL to the Bookmarks collection and displays the description of the URL in the constituent list box.
In the DblClick event of the constituent list box, you can see that the ListIndex property of the list box has to be converted into a long integer and incremented by 1 before it can be used to retrive an item from the collection. The reasons for this are:
Although the user doesn't have access to the URL object in this example, there's no reason why you couldn't give the user access to a URL object similar (or identical) to the URL object in the UserControl project. That way, instead of adding items to your HotList procedurally (using the AddURL method) your control would have a more fully object-oriented interface. I chose to provide access to the Bookmarks collection through a method rather than by adding an object because it greatly simplified this example.
To test your control, place an instance of the HotList control on the EXE project form, then place the following code in the form's Load event:
Private Sub Form_Load() HotList1.AddURL "The News Babe Page", _ "http://www.well.com/user/jeffreyp/newsbabe1.html" HotList1.AddURL "The Onion", "http://www.theonion.com/" HotList1.AddURL "Home Page", _ "http://www.well.com/user/jeffreyp/activex/" End Sub
Then run the EXE project. When the project runs, double-click
on one of the items in the hot list. Internet Explorer should
launch, displaying the page you chose. Because it stores URLs
in a collection, the HotList control should support a hypothetically
unlimited number of URLs.
If you're looking for more information on object-oriented programming in VB, you might want to check out Deborah Kurata's Doing Objects In Microsoft Visual Basic 4.0. This unique book covers not only the nuts-and-bolts of programming using classes in Visual Basic, but the just-as-important design principles behind object-oriented programming that every programmer should know about.
An interface, as you know, is the set of properties and methods exposed by an object. You've learned that the interface of a programmable software component can't be changed for fear of losing backward compatibility with existing code. If you developed an enhanced version of a control or class that omitted support for a particular property or method, code based on that component would break.
So how, then, do you modify or enhance the interface of an existing component? One way is to build multiple implementations of an interface based on a type library or abstract class.
A type library is created with a utility called mktyplib, which is found in the ActiveX Software Development kit. (The mktyplib utility, like most of the ActiveX SDK, is targeted primarily at developers using C++.)
As a Visual Basic developer, you will probably find it easier to create abstract classes. An abstract class is a class module that contains declarations for the class's interface but no code to implement the interface elements. The particulars of how each interface element is implemented are left to the classes that implement the abstract class.
Once you have an abstract class in your project, you can refer to it in other classes by using the Implements keyword. For example, let's say you need to build a class to handle your ice cream factory. You know you're going to transport ice cream to customers in vehicles (ice cream trucks). There are different kinds of ice cream trucks, but all ice cream trucks have a number of common properties, such as size, carrying capacity, and number of wheels.
The Implements statement, which is new to Visual Basic 5.0, enables your object to exercise the object-oriented property of polymorphism. Polymorphism means that an object can be manipulated using more than one interface-a useful thing, because it means that you can build new interfaces for your objects without disturbing the old ones. Leaving old interfaces in place means that new versions of your object won't break the old versions.
In this example, you'll build an abstract class to represent an ice cream truck, then build classes that implement the abstract class. This example will demonstrate how easy it is to write multiple interfaces to a single abstract class. To do this:
The letter "I" prefixing the name of an abstract interface class is an ActiveX convention; it's done this way to remind the programmer that this class is designed to provide an interface for other classes. You are free to call your classes anything you want, but for this example we'll stick with the convention.
Public Sub LoadTruck() End Sub
' Declarations Implements ITruck
Private Sub ITruck_LoadTruck() Debug.Print "Truck Loaded." End Sub
This simplified example prints to the Immediate window when the LoadTruck method is called; your real-world implementation of the LoadTruck method would obviously include code that performed whatever processing that needed to be done to implement the loading of a truck.
Private Sub Form_Load() Dim MySmallTruck As New SmallDeliveryTruck Dim TruckAtLoadingDock As ITruck Set TruckAtLoadingDock = MySmallTruck TruckAtLoadingDock.LoadTruck End Sub
We're now at the point, finally, where you can begin to see the usefulness of multiple interfaces. Let's say that your ice cream factory has started using a new, larger kind of delivery truck. Your application must represent the new kind of truck with a class that is a new implementation of the ITruck interface. To do this:
' Declarations Implements ITruck Private Sub ITruck_LoadTruck() Beep MsgBox "Attention! The big truck is now being loaded!" End Sub
Private Sub Form_Load() 'Dim MySmallTruck As New SmallDeliveryTruck Dim MyBigTruck As New BigDeliveryTruck Dim TruckAtLoadingDock As ITruck Set TruckAtLoadingDock = MyBigTruck TruckAtLoadingDock.LoadTruck End Sub
You can see from this example that by implementing multiple interfaces of classes, you enable your application to grow and change without the intense level of grief that is usually associated with changing a software design after it has been initially written.
You can declare methods as Friend so private data members can be accessed from other objects that know how to access them safely. Friend procedures are not an object-oriented concept as much as they are an exception to object-oriented rules.
A Friend method can be executed by another procedure in the same class as the method, but not by a procedure outside the class. But unlike a private procedure (which also can't be accessed from outside the class in which it resides), a Friend procedure has access to data that exists in other objects of the same type.
For example, say you have a MidsizedDeliveryTruck class. Because your fleet of mid-sized delivery trucks has a special loader device that enables a driver to dump all the tubs of ice cream into another truck at the flick of a switch, you decide to write a software class to emulate the behavior of the mid-sized ice cream truck. To see an example of how you might write such a class, do the following:
' Declarations Private mlngTubs As Long Public Property Get Tubs() As Long Tubs = mlngTubs End Property Public Property Let Tubs(ByVal lngNewValue As Long) mlngTubs = lngNewValue End Property Public Sub SwapTubs(TargetTruck As MidsizedDeliveryTruck) ' Takes all the tubs in the current ' truck and moves them to a new truck. If mlngTubs > 0 Then TargetTruck.Transfer mlngTubs, TargetTruck End If End Sub Friend Sub Transfer(lngTubs As Long, Truck As MidsizedDeliveryTruck) ' Can be called from another BigDeliveryTruck ' object, but not from a form or another ' type of object. Debug.Print "Transferred " & lngTubs & " tubs." mlngTubs = 0 Truck.Tubs = lngTubs End Sub
In this code, the public subroutine SwapTubs (exposed as a method of the MidsizedDeliveryTruck class) performs validation only; it hands off the actual work of swapping tubs to the Friend procedure called Transfer. Transfer is not a publicly accessible method of this class; it can only be called from other procedures in the class.
The Transfer subroutine, meanwhile, takes responsiblity for setting the number of tubs in the current truck object to 0 while setting the number of tubs in the target truck object to the appropriate number. To see how this code works:
Private Sub Form_Load() Dim RedTruck As New MidsizedDeliveryTruck Dim BlueTruck As New MidsizedDeliveryTruck RedTruck.Tubs = 20 RedTruck.SwapTubs BlueTruck Debug.Print "BlueTruck now has " & BlueTruck.Tubs & " tubs." End Sub
Of course, instead of using a Friend procedure, you could instead
place the code that performed the work of transferring tubs into
the public SwapTubs method-or simply make the Transfer method
public instead of a Friend procedure. But this would mean that
the user of this class would have more control over exactly how
many tubs were transferred, something you might want to prohibit.
You can see that by using a Friend procedure in this way, you
get more control over the contexts in which your code can be used.
Although we used a class module in this example for the sake of simplicity, you can use Friend procedures in your ActiveX control projects as well. There is no syntactical difference between Friend procedures in classes and ActiveX control projects.
In this chapter, we covered the object-oriented features of Visual Basic 5.0. We discussed object-oriented programming in conceptual terms, then demonstrated them with an ActiveX project that contained its own object model created with Visual Basic classes.
In the next chapter, we'll take a look at some of the database features of Visual Basic 5.0 and demonstrate how you can make your ActiveX control project database-aware.