Book Review: Beginning Visual Basic 6 Objects
We've discussed interfaces a few times, now, when we've been talking about objects, classes, and black boxes; but what exactly is an interface, and why do we care?
Class Interfaces and Object Interfaces
Well, we already know that an interface is really nothing more than the name given to the collection of properties and methods within a class. That sounds simple enough, but it's worth looking at interfaces in some more detail now.
A class interface is the collection of properties and methods within a class: that's the public stuff, the public variables, property routines, subroutines and functions that we put into our classes.
The private variables and routines within our class, on the other hand, are typically called Member routines.
It's worth noting that since we create objects from classes in Visual Basic, there is a clear relationship between class interfaces and object interfaces. Once we've created an instance of a class, our object will have an object interface as defined by its underlying class interface.
The Implements Command
Visual Basic 6 comes with a special command that lets us do some neat things with class interfaces, and this is the Implements command.
Placed at the top of a class module, the Implements command forces us to write (or implement) the same methods and properties as those implemented in another class module.
In effect, this forces us to implement the same class interface for one class as that of another class. Consider this: if the class interface for Class1 consists of three methods and four properties, and we use the following command at the top of a second class called Class2:
then this forces us to implement the same three methods and four properties within Class2 as those defined in Class1.
However, the methods, properties, etc. that make up a class interface do not have to be implemented using the precise same lines of code that were present in the original class interface.
The methods in the original class interface must all be defined within the implemented class interface, it's true, but the actual lines of code within those methods may be quite different from the original class interface that's being implemented. This takes us back to the theme of the black box: just as long as the buttons (or methods) on the outside of the black box work, we don't have to worry about what's going on behind the scenes.
Not every television, for instance, has the same internal workings inside it: all that matters is that the key functions we expect from a television are available to us. And so it is with our class interfaces: the lines of code behind each implementation of a class interface may be quite different.
If you are still a bit unsure about this idea of implementing, then you could take a quick peek at the section on interface inheritance that appears in a few pages time. It contains a good example of the use of the Implements command.
Right, I hope you've returned to this point, as we are about to consider the subject of multiple interfaces and see how handy the Implements command can be.
This concept is real simple: a class can support more than one interface.
Let's stay with our Class1 and Class2 example for a moment. Assuming we'd just implemented the interface for Class1 within our code for Class2, we could then proceed to add a new set of methods and properties to Class2 that were above and beyond the methods and properties implemented from Class1.
At which point, Class2 happens to support more than one interface: first, the default class interface for Class1; and second, the class interface defined by the new methods and properties that belong to Class2 itself:
By arranging for a number of classes to implement the same interface, we're forcing the developers of those classes to conform to a particular set of standards. This makes life a lot easier for the people using those classes, since they can expect a certain interface to always be available.
Think about a car, a small van, and a large semi-truck. All have a common interface: press the gas pedal to go faster, press the brake to slow down, and turn the steering wheel to change direction. Each has at least one door, requires fuel, and contains a set of dials and gauges to show your current speed, fuel remaining, oil pressure and so on.
All of these classes implement a common interface: a Vehicle interface, if you like. Aside from implementing that Vehicle interface, however, each class has its own unique interface - which it bolts on to the base Vehicle interface. A car has rear seats, for example; a van has the ability to carry more goods than a car, but has less seats; and a semi has a detachable freight carrying device (its trailer), possibly a bed (if it's a long haul truck), and so on.
Let's carry this line of thought on for a little longer. A car and van could conceivably have the same chassis, the same engine, and to all intents and purposes they could be identical in every way if we ripped the bodywork off. A semi, on the other hand, is completely different underneath the surface. Now since all these cars implement the same interface as a basic Vehicle, we could take any qualified driver and tell them to Drive each of the vehicles from one point to another. Drive is a common method that most drivers know how to use, and the chances are that most drivers could get all three vehicles moving as requested. So despite the differences, and indeed the similarities, between these three vehicles, the Drive method will get us driving - whichever car we're in.
Visual Visual Basic works the same way. If we have a Vehicle class defined with a Drive method (among others), we can implement the Vehicle interface within new Car, Van and Semi classes. Then, if we want to get our Car, Van and Semi objects moving, Visual Basic will allow us to pass each of these objects to a routine that simply expects a Vehicle type object and then calls its appropriate Drive method.
Implementing Interfaces in Visual Basic 6
In our example application for DataDamage Inc, multiple interfaces will be very useful.
So far, we have a neat title browser. It works quite well, and this browser interface represents something that we would ideally like to see in the Authors class a little later.
So although we will ultimately need to produce both a clsTitles class (done that) and a distinct clsAuthors class, these classes will have some common methods. For instance, each class will allow the browsing of data. At the same time, however, each class will have its own unique properties, and the clsAuthors class will need to have its own unique methods of changing the underlying Author data (since Author data will be arranged differently in the database than the Title data).
So here's our plan: we'll define the common browsing interface between these two classes - that is, the interface that contains the MoveFirst, Next, Previous and Last methods. We'll then implement that common browsing interface in both the clsTitles class and the clsAuthors class.
Anyone who later modifies or deals with the code in our application will be grinning from ear to ear when they see that all data access objects in our application actually have a common object interface.
So how do we define a common interface between two classes? Well, the best way to do this is to start to think in terms of class frameworks.
The framework of a class is actually the bare bones of the interface for that class. A framework is therefore just a list of the public routines and properties that form the interface for a class or object, without any real code to actually implement that interface.
Frameworks are therefore the briefest possible description of a class interface or object interface, since they contain none of the actual code behind that interface - just empty routines and properties. Frameworks are useful things to know about, so let's get on with some code and see it all in action.
Try It Out Using Implements
1 Since our clsTitles and clsAuthors classes are going to share a common interface, the first thing we must do is move the basic framework of the Titles interface out into another object.
Add a new class module to your project and call it clsRecordset. When that's done, add the following code.
Option Explicit ' clsRecordset object - a generic object designed to wrap ' up the functionality of a recordset in a generic re-useable ' object. Private Sub Class_Initialize() ' End Sub Private Sub Class_Terminate() ' End Sub ' Class methods Public Sub MoveNext() ' End Sub Public Sub MovePrevious() ' End Sub Public Sub MoveLast() ' End Sub Public Sub MoveFirst() ' End Sub Public Sub AddAsNew() ' End Sub Public Sub SaveChanges() ' End Sub Public Sub DeleteCurrent() ' End Sub
Be careful when you define your own interfaces like this though. The Visual Basic compiler has a real nasty habit of wiping out any empty routines that you might have in your project, hence the reasoning behind putting a single quote (comment) in each of the routines above.
Notice that all these routines are just bare definitions - with no real code in them. Also notice that we're adding three routines to this framework that weren't present within our original clsTitles class (AddAsNew, SaveChanges and DeleteCurrent) which are specifically concerned with adding, saving and deleting data. These are of little relevance for clsTitles, but will feature in our clsAuthors class.
2 With our clsRecordset framework interface defined, the next step is to actually let Visual Basic start enforcing it on our code - which is a perfect job for the Implements command.
Bring up the code for your clsTitles class and add a line just underneath the Option Explicit statement so that your code now reads like this:
Option Explicit ' clsTitles - wraps up the Biblio Titles table. By keeping the interface ' consistent it should be possible to move the class to point at some ' other data source, such as an Rdo Resultset, or flat file, without ' users of the class ever noticing. Implements clsRecordset Public Event DataChanged() : :
If we now try to compile this program and run it, we find that it no longer works: Visual Basic instead gives us a host of new error messages:
I wasn't kidding before, when I said that using Implements tells the compiler to force you to write certain routines! But this is just what we want, in our case, since we need to produce a consistent interface across the clsTitles class and the clsAuthors class that we're going to create shortly!
You may also notice how useful this could be in a team development environment where you would need to manage a group of developers and make sure that they were all developing to certain interface standards.
3 The previous error message is telling us that, in order for the clsTitles class to implement the interface of the clsRecordset class, we need to write a method called MoveNext.
But hang on a second! We did write a MoveNext method; it does exist! If it didn't exist, then the application as it existed before we put that new clsRecordset class in would not have worked at all. The truth of the matter is that Visual Basic is just expecting a little more than we have already done. Time to find out just what.
Try going back to the code editor for the clsTitles source and drop down the Objects Combo box at the top of the editor:
Notice how the list now includes a clsRecordset object, just as if we had dropped a control on to a form and we were editing the form's code.
Select clsRecordset from this drop down list, just as I have done here.
4 Now drop down the Events list Combo box, which sits at the top right of the code window. You'll see a list of all the clsRecordset methods that we can implement in the CTitles class:
What does all this tell us? Something quite simple really: that our clsTitles class has two interfaces: the original clsTitles class interface, and now also the clsRecordset class interface that we added to clsTitles (we used Implements to do that).
The upshot is clear enough: all the routines in this drop down list for the clsRecordset interface will need to be defined within our clsTitles class.
As we noted before, it's perfectly true that there are routines by these names already present in our clsTitles class; but those routines are currently part of the default class interface for clsTitles. We now need to implement the clsRecordset class interface within clsTitles, which we committed ourselves to with the Implements clsRecordset command.
Next question: how do we implement the clsRecordset class interface within the clsTitles class? Take a look at this routine (but don't type it in yet):
Public Sub clsRecordset_MoveNext() If Not m_recTitles.EOF Then m_recTitles.MoveNext Reload_Members End If End Sub
This is our MoveNext routine all right, but notice that it's prefixed with clsRecordset_ which tells Visual Basic that this routine belongs to the clsRecordset interface. This prefix method is how we tell Visual Basic which routines and properties belong to which interface.
5 Okay, in order to get the clsTitles class working again, go through and change all the Public method names in clsTitles so that they're prefixed with clsRecordset.
Be careful though: you only need to do this for Public methods that are also in the clsRecordset interface.
You should end up with all your Public routines, which looked something like this:
Public Sub MoveFirst() m_recTitles.MoveFirst Reload_Members End Sub
now looking more like this:
Public Sub clsRecordset_MoveFirst() m_recTitles.MoveFirst Reload_Members End Sub
6 Since we added three new methods to the clsRecordset class, to create an interface for updating data (AddAsNew, SaveChanges and DeleteCurrent), we'll also need to code these routines into our clsTitles class.
Therefore add the following routines to clsTitles:
Public Sub clsRecordset_AddAsNew() ' Do nothing End Sub Public Sub clsRecordset_SaveChanges() ' Do nothing End Sub Public Sub clsRecordset_DeleteCurrent() ' Do nothing End Sub
Notice that these routines are commented out again. We don't want them to actually do anything, at this stage, since our clsTitles class is just to allow browsing. We still need to implement them, however, since we've committed ourselves to implementing the entire clsRecordset interface within clsTitles.
7 When you're done, try running the application again, and you'll quickly find that it doesn't work. If you're in any doubt, try pressing the First button on our browsing form!
It's actually very interesting why things aren't working yet. Take a look at the code behind the First button on the browsing form:
Private Sub cmdFirst_Click() Titles.MoveFirst End Sub
Visual Basic looks at this code and says "Ok, the programmer wants me to run the MoveFirst method on the Titles object". It then takes a peek at the default interface for the Titles object (which is the clsTitles interface) and sees that it doesn't actually have a MoveFirst method. There's one on the clsRecordset interface, which clsTitles implements, but not on the clsTitles interface itself anymore (we changed it, remember?). The net result: Visual Basic gives you an error.
As we've already seen, if we need to make calls to code that lives in an implemented interface, then we must prefix the method name with the name of the interface. Therefore, the code above should actually read:
Private Sub cmdFirst_Click() Titles.clsRecordset_MoveFirst End Sub
Also prefix the method name with clsRecordset_ in the cmdLast_Click, cmdNext_Click and cmdPrevious_Click routines.
Okay, let's move on to the Author class of the DataDamage Inc application.
Try It Out - Adding the Author Class
1 On the whole, the Author class is very similar to the Titles class, with the simple addition that we'll allow the user to add, edit and delete records. Given the code that we have, this should be pretty easy to do.
Put a new class module into the application, and copy and paste the entire clsTitles class code into it. Change the name of this new class to clsAuthors.
Something called containment provides us with a workaround to save us doing this, but more on that later.
2 Once we have the clsTitles code pasted into our new class, there are a few routines that we obviously need to change: the Authors table in the Biblio database, for example, has a totally different set of fields. I've listed the entire class below, with the relevant changes highlighted:
Option Explicit ' clsAuthors - wraps up the functionality to browse and ' maintain the authors table in the Biblio database Implements clsRecordset Public Event DataChanged() ' Public members to implement properties Public lngAu_ID As Long Public strAuthor As String Public intYear_Born As Integer ' Private members to hold connection to database, ' and to the recordset itself Private m_recAuthors As Recordset Private m_dbDatabase As Database ' Class event handlers Private Sub Class_Initialize() ' When an instance of the class is created, we want to ' connect to the database and open up the Authors table. On Error GoTo Class_Initialize_Error Set m_dbDatabase = Workspaces(0).OpenDatabase _ (App.Path & "Biblio.mdb") Set m_recAuthors = m_dbDatabase.OpenRecordset("Authors", _ dbOpenTable) Exit Sub Class_Initialize_Error: MsgBox "There was a problem opening either the Biblio " & _ "database, or the Authors table.", vbExclamation, "Problem" Exit Sub End Sub Private Sub Class_Terminate() ' When the instance of the class is destroyed, we need to ' close down the recordset, and also the connection to the ' database. Error handling is needed since there could have ' been a problem in the Initialize routine making these ' connections invalid On Error Resume Next m_recAuthors.Close m_dbDatabase.Close End Sub ' Generic Data management methods Private Sub Reload_Members() ' Reloads the member variables (properties) with the field ' values of the current record On Error GoTo Reload_Members_Error With m_recAuthors lngAu_ID = .Fields("au_id") strAuthor = "" & .Fields("author") If Not IsNull(.Fields("Year Born")) Then intYear_Born = .Fields("Year Born") Else intYear_Born = 0 End If End With RaiseEvent DataChanged Reload_Members_Error: Exit Sub End Sub Private Sub Save_Members() ' Assumes that the recordset is in either Edit or Addnew mode. On Error GoTo Save_Members_Error With m_recAuthors .Fields("Author") = "" & strAuthor .Fields("Year Born") = intYear_Born End With Save_Members_Error: Exit Sub End Sub ' Class methods Public Sub clsRecordset_MoveNext() If Not m_recAuthors.EOF Then m_recAuthors.MoveNext Reload_Members End If End Sub Public Sub clsRecordset_MovePrevious() If Not m_recAuthors.BOF Then m_recAuthors.MovePrevious Reload_Members End If End Sub Public Sub clsRecordset_MoveLast() m_recAuthors.MoveLast Reload_Members End Sub Public Sub clsRecordset_MoveFirst() m_recAuthors.MoveFirst Reload_Members End Sub Private Sub clsRecordset_SaveChanges() ' With m_recAuthors .Edit Save_Members .Update End With End Sub Private Sub clsRecordset_AddAsNew() ' With m_recAuthors .AddNew Save_Members .Update End With End Sub Private Sub clsRecordset_DeleteCurrent() ' m_recAuthors.Delete On Error Resume Next m_recAuthors.MoveFirst Reload_Members End Sub
As you can see, it's really pretty similar to the Titles class. The obvious differences are the names of the member properties to get at the field values, as well as the name of the member recordset. Finally, we added some code this time around to handle the Add, Edit and Delete methods, as well as a new private routine, Save_Members, to copy the values of the member variables out to the corresponding fields.
3 Just as before, we now need to create a browsing and maintenance form. Add a new form to the project and place controls on the form so that it looks like this:
Once again, make the form an MDI child, and name it frmAuthor. Next up, name the two text boxes txtAuthor and txtYearBorn respectively, and the command buttons cmdFirst, cmdPrevious, cmdNext, cmdLast, cmdAdd, cmdUpdate and cmdDelete.
4 Now we're ready to add code. Because of the way we've designed the code so far, the code under this form should be quite familiar to you, and as brief as ever. Take a look at this and add it all to frmAuthor:
Option Explicit Private WithEvents Authors As clsAuthors Private Sub Authors_DataChanged() ' When the data in the Authors object changes (by moving to a new ' record, update the controls on the form with the revised data txtAuthor = Authors.strAuthor txtYearBorn = Authors.intYear_born End Sub Private Sub cmdAdd_Click() Update_Authors Authors.clsRecordset_AddAsNew End Sub Private Sub cmdDelete_Click() Authors.clsRecordset_DeleteCurrent End Sub Private Sub cmdFirst_Click() Authors.clsRecordset_MoveFirst End Sub Private Sub cmdLast_Click() Authors.clsRecordset_MoveLast End Sub Private Sub cmdNext_Click() Authors.clsRecordset_MoveNext End Sub Private Sub cmdPrevious_Click() Authors.clsRecordset_MovePrevious End Sub Private Sub cmdUpdate_Click() Update_Authors Authors.clsRecordset_SaveChanges End Sub Private Sub Form_Load() Set Authors = New clsAuthors End Sub Private Sub Update_Authors() ' Updates the members in the Authors object with the data currently ' on the form Authors.strAuthor = txtAuthor Authors.intYear_born = txtYearBorn End Sub
The big difference between this and the code we have on the Titles form is that this has code to update the data in the class and ultimately the Authors database itself. When the application is running, we can enter data into the form and then click on the Add button, for example. This will trigger the Update_Authors routine to copy the fields out to the class members, and then run the AddAsNew method on the class to copy the information it holds out to a new record. The Update button works in exactly the same way, calling SaveChanges instead of AddAsNew.
Something for you to consider about this code: the DeleteCurrent routine that we've written will only be successful at deleting Authors that we've entered into the database ourselves. Other Authors in the database have further records in related databases, which prevents the simple deletion of an Author. A nice extension to this code would therefore be an error trapping routine so that our application doesn't crash out with an error message every time this occurs. We discuss error trapping at various points through this book.
5 Toe To run this full application, you'll need to add some code to the MDI form to load the Author Browser form up.
Bring up the MDI form now, and click on the Maintain item under the Authors menu heading. Just as before, the code window will come into view for the click event of that menu item. Just add a line of code so that it looks like this:
Private Sub mnuAMaintain_Click() Load frmAuthor End Sub
Now run the program, and you'll be able to see both browser forms in action. DataDamage are clearly on their way to a great application.
The most important point to note, in all this, is that even though we have quite a lot of code in the form, it's all very simple stuff, really easy to understand and follow, and therefore really easy to maintain.
This is the benefit of the object-oriented design of the application. Although it took some effort at the start to get the code into the clsTitles and clsRecordsets class modules, and then to provide a neat interface to them from the code module, the result is that we have an application that is extremely easy to extend, should the need arise.
Page 7 of 9