Talking Objects

Rob Macdonald

Rob confides that he was very tempted to call this article "The Message is the Medium" but decided not to, realizing that the pun would only work for McLuhanites, referring to the Canadian scholar who popularized the phrase, "the medium is the message." Pun or not, however, you'll find another multi-faceted article that probes not only the notion of persistence and Property Bags, but also explores some of the intricacies of using Microsoft Message Queue (MSMQ).

Several things are coming together to change the way we think about objects. Making objects persist after your application closes down has always been something of a chore. You've had to write special code to store objects in a database or file and then read them back in.

In this article, I'll discuss object persistence in VB6 and show you how to store objects in a convenient, flexible, and standard way. We'll see how you can send entire objects (including all their dependent objects) to another program on another computer using an asynchronous technique that will work even if the program (or computer) on the receiving end isn't running or if the network is down. To do this, we'll use Microsoft Message Queue (MSMQ), which, like Microsoft Transaction Server (MTS), is free (both ship as part of Windows NT 4.0 Option Pack). Both represent key components of the "Microsoft Way" of programming. Fortunately, MSMQ is easier to program than MTS.

Bags of fun
To understand how to make objects persistent, stop for a moment and think about what's required to capture all the information about an object. An object's class defines nearly everything there is to know about it. The class defines what properties and methods the object has and contains the code for all of its behavior (in other words, the implementation of its methods). This is quite a lot of information, but for any public class (one that's defined as public within an ActiveX EXE or DLL), all of this information is encapsulated by the CLASSID or GUID with which the object's class was registered.

All that remains is to capture the specific values of the properties of the object and associate these values with the CLASSID. VB6 makes it easy for you to do this using Property Bags. If you've written custom controls in VB5, you might have already experienced Property Bags and probably thought they were a bit cumbersome to use. If so, stay tuned, because VB6 introduces a new version of the Property Bag, which is not only available for use with classes but also offers some subtle new capabilities.

The basic idea of a Property Bag is that you can put any number of object descriptions into it and convert the whole lot into an array of bytes called a stream. An array of bytes is a very versatile beast. You can assign it to a string or variable, and store it or send it just about anywhere. You can even encrypt it and stick it on the Internet. When you assign the stream to another Property Bag, the bag will convert it back into the original object structure with complete fidelity. The versatility of streams, coupled with the Property Bag's ability to convert easily and efficiently between streams and objects, is at the heart of object persistence and portability. The rest of this article is concerned with how you create the stream and some of the things you can do with it.

A safe bet
The examples we'll build in this article are going to be based on horse racing, so I've defined two multi-use classes called Bet and Horse in an ActiveX DLL project called "Turf."

 'definition of Bet class
 Public RaceCourse As String
 Public RaceTime As String
 Public Horse As Horse
 Public Odds As Double
 Public Amount As Currency
 Public Function CalcWinnings() As Currency
 CalcWinnings = Amount * Odds
 End Function
 'definition of Horse class
 Public Name As String
 Public AgeInMonths As Integer
 Public Owner As String


Note that the Bet object only has one property -- a Horse object. This simple object hierarchy will provide enough object complexity to see how object persistence works. In the real world of a betting shop, a high proportion of all bets on a race is captured within the last few minutes before a race begins. In an automated betting shop, all the cashier's PCs will be busy capturing bets for the next race, and it's vital that they can keep capturing bets -- even if the shop's server can't cope with the load or if the network fails. We'll see later how MSMQ makes this possible. But for now, we'll just look at the situation of persisting a single bet on a single horse.

Making an object persistent
In VB5, a class module's property box contained just items -- the Name and the Instancing properties -- but VB6 classes can have a number of extra items. I looked at one of these, "DataSourceBehaviour," in my December 1998 article, "Do-It-Yourself Recordsets," but this time, we'll focus our attention on the "Persistable" property. You need to set this property to "1 -- Persistable" before making the class persistent.

In COM terms, making a class persistable means having it implement IPersist, IPersistStream, and several other COM interfaces, but most of the complexity is hidden in VB so that when you make your object persistable, you automatically get three events added to your definition. In addition to the standard Initialize and Terminate events, persistable classes have InitProperties, ReadProperties, and WriteProperties events available in their event procedure box (see Table 1 ).

Table 1. Three key events are associated with persistable classes.
Event Usage
WriteProperties When you add the object into a Property Bag, this event fires. Use the event to write code that adds all the object's property values into the bag.
ReadProperties When the object is extracted from the bag, this event fires. Use the event to write code that reinstates the object's properties from the bag.
InitProperties When a persistable object is created using "New" or "CreateObject," this event fires. It doesn't fire when an object is extracted from a bag, as ReadProperties will be called instead. In both cases, the object's Initialize event will fire. Use this event to write initialization code that only fires when an object is first created.


Listing 1 shows the ReadProperties and WriteProperties code for the Bet class, and there are two points worth noting. The first (for those of you who haven't used property persistence with UserControls) is the use of the PropertyBag object, PropBag. This is passed in as an argument to the events and is used to read or write individual properties. Each property has a name that's unique for the object. Different objects stored in the same bag can have properties with the same name, so there's no scope for name clashes.

Listing 1. Property persistence code for the "Bet" object.
 
 Private Sub Class_ReadProperties(PropBag As _
    PropertyBag)
 Me.RaceCourse = PropBag.ReadProperty("RaceCourse")
 Me.RaceTime = PropBag.ReadProperty("RaceTime")
 Set Me.Horse = PropBag.ReadProperty("Horse")
 Me.Odds = PropBag.ReadProperty("Odds")
 Me.Amount = PropBag.ReadProperty("Amount")
 End Sub
 Private Sub Class_WriteProperties(PropBag _
    As PropertyBag)
 PropBag.WriteProperty "RaceCourse", Me.RaceCourse
 PropBag.WriteProperty "RaceTime", Me.RaceTime
 PropBag.WriteProperty "Horse", Me.Horse
 PropBag.WriteProperty "Odds", Me.Odds
 PropBag.WriteProperty "Amount", Me.Amount
 End Sub


The second point concerns the data types of the properties. VB knows how to persist standard data types, but it won't know how to persist the Horse property, which is a Horse object according to my class definition. However, the Horse object is also persistable and has its own persistence events. When the Bet object's Horse property is persisted, the Horse object's own WriteProperties event fires automatically. This event chain can be arbitrarily complex and allows sophisticated object structures to be persisted with a single request. While this is extremely convenient behavior, there are two potential "gotchas." First, for an object to be persisted successfully, all its dependent objects must be persistable (otherwise, you'll get an error). Second, you need to be sure to avoid circular references!

The ReadProperty and WriteProperty methods of the PropertyBag allow default values to be specified. While I haven't used these in my examples, they can be very useful, because when a property value is the same as the default, it isn't written to the bag, thus keeping the bag as lightweight as possible.

Once the property events have been coded, persisting and extracting the object is straightforward, as you can see in the following code, which assumes the existence of a Bet object called oBet:

 Dim oBag As PropertyBag
 Dim byteArray() As Byte
 Set oBag = New PropertyBag
 'persist the bet object
 oBag.WriteProperty "Bet1", oBet
 byteArray = oBag.Contents
 Set oBag = Nothing
 Set oBet = Nothing
 'extract the object
 Set oBag = New PropertyBag
 oBag.Contents = byteArray
 Set oBet = oBag.ReadProperty("Bet1")
 MsgBox oBet.CalcWinnings


The code creates a Property Bag object and writes the Bet object to the bag with a single call to WriteProperty (remember that this call triggers the WriteProperties event defined in Listing 1). The contents of the bag are then copied into a byte array before both Bag and Bet objects are destroyed. The byte array is then assigned to a completely new bag (which could be in a completely different program or session), and the original Bet
object extracted. To prove that the object is completely re-created, its CalcWinnings method is called to round things off.

Typically, you'd destroy the object in one session or program and re-create it in another. However, there are plenty of situations when this kind of trick can be useful, even when the original object is simply kept in memory (see the sidebar, "Don't Overdo Persistence").

Persisting to a file
In Figure 1, you can see the simple user interface I created to display and capture bet information. This form has a single variable called mBet, which holds a Bet object, and two procedures: createBet, which creates a Bet and a Horse object from the form's controls, and displayBet, which displays a Bet object in the form's controls.

Although the crucial parts of the sample program simply deal with persisting the Bet object and making sure I can read and write Bet objects using either a file or a message queue, I wanted the program to be able to persist and recreate objects using a range of different media, so I created a "BagHandler" class. It might sound like it should be more at home at the airport check-in area, but its job is to convert any object structure to or from a stream (byte array) and to handle the different media for persistence.

If this sounds heavy, don't worry. All the code required for streaming to and from files is available in the accompanying Download file. The Stream and UnStream methods convert objects to streams and vice versa, while the writeToFile and readFromFile write and read a stream to and from a file. For each new media, I just need to add a "writeTo . . ." method and a "readFrom . . ." method, as we'll see later with Message Queuing.

The user interface can use the BagHandler class to implement the File-Write and File-Read event procedures as follows:

 Private Sub cmdFWrite_Click()
 Dim b() As Byte
 Dim oHandler As New BagHandler
 Set mBet = createBet()
 If IsObject(mBet) Then
    b = oHandler.Stream(mBet)
    oHandler.writeToFile b, txtFileName.Text
    End If
 End Sub
 Private Sub cmdFRead_Click()
 Dim b() As Byte
 Dim oHandler As New BagHandler
 b = oHandler.ReadFromFile(txtFileName.Text)
 Set mBet = oHandler.UnStream(b)
 displayBet mBet
 End Sub


MSMQ
If you've ever tried writing a file system, you know how deceptive the idea of a file being simple really is, and if you've ever tried doing without a file system for a while, you'll appreciate how vital they are. Right up there on my list of useful computer science inventions is the queue. Queues are amazingly convenient. Stuff gets put in at one end, and then, at some later time, stuff gets taken out of the other end. All the management of the stuff while it's in the queue, however big or small it gets, is handled by the queue software. Microsoft Windows itself is entirely dependent on queues, because it uses special application message queues to ensure that such things as mouse and keyboard events get safely delivered to the correct program. Without these queues, your programs would never get any user input. Admittedly, this would simplify error handling, but it would rather take the spice out of programming.

E-mail provides another classic example of queues at work. You can send an e-mail to someone who's asleep and has his or her computer switched off, and you can even send the mail if the link between their message handler and yours is currently unavailable, thanks to message systems' use of "store and forward." Each component will look after your message until it can make contact with the next component and pass it safely on. Eventually, the message will reach the recipient, who can take it off the "queue."

In both the Windows application queue and e-mail queues, "producers" place messages onto the queue, and, eventually, consumers take them off. If messages are being created faster than the consumer can remove them, it doesn't matter. The queue guarantees to look after them until they're consumed.

Windows application queues only work on one machine and lose their contents when the machine crashes or is switched off. To provide developers with a single and very powerful concept of a queue to program with, Microsoft has developed MSMQ, an NT service available free as part of the NT4 Option Pack.

To use MSMQ, you have to configure one NT Server as MSMQ's Primary Enterprise Controller (PEC). Additionally, with NT4 (but not NT5/Windows 2000), you have to have SQL Server installed to hold configuration data (SQL Server doesn't hold the queues and messages). You can install more than one MSMQ PEC, and all other Windows computers can be MSMQ clients. A queue has to live somewhere, and it can be created on (just about) any machine where MSMQ is installed. Any computer can access any queue so long as it has the required privileges.

MSMQ configurations can get quite elaborate, but in my test rig I have one NT4 Server acting as the PEC and two "independent" clients (which means they can store their own messages): one Win95 and one W2000 (beta 2). It took about two minutes to install MSMQ on each computer. MSMQ is a very big topic, and all I can do is give you a flavor of how it works and show you some working programs. My hope is that this will inspire you to experiment on your own.

You can store all kinds of data in a message queue, but the whole point of this article is to show how objects can be passed between programs using a queue. Therefore, we'll create a queue on the server to hold Bet objects. Our Bet Capture program will be able to keep adding Bet objects into the queue (multiple clients can write to the same queue), and they can be taken off as and when a consumer program is willing or able to read them.

MSMQ has an Explorer-style administration application that can be used to create and manage queues, but you can also manage MSMQ programmatically. Figure 2 shows a queue called "flutters" being created on a server called "server" using the MSMQ Explorer.

Depending on the security options used when MSMQ was installed, you might need to set access rights to the queue. On my configuration, new queues have "Send" access by default. Right-clicking on a queue name in the Explorer takes you to a "Properties" dialog box, where you can change permissions to ensure full Send and Receive access.

Programming with MSMQ
After all this buildup, you'll undoubtedly be delighted to know that basic MSMQ programming is a breeze. To access MSMQ from VB, you need to set a reference to "Microsoft Message Queue Object Library," which gets added when MSMQ is installed. The object hierarchy lets you find your way to any queue and then create, send, or receive message objects. The following code shows the two methods added to my BagHandler class to provide basic queue-handling functionality:

 Public Sub writeToQ(bStream() As Byte, _
    sQName As String)
 Dim oQInfo As New MSMQQueueInfo
 Dim oQueue As MSMQQueue
 Dim oMessage As New MSMQMessage
 oQInfo.PathName = sQName
 Set oQueue = oQInfo.Open(MQ_SEND_ACCESS, _
    MQ_DENY_NONE)
 With oMessage
   .Label = "Betting Message"
   .Body = bStream
   .Send oQueue
   End With
 oQueue.Close
 End Sub
 Public Function readFromQ(sQName As String) _
    As Byte()
 Dim oQInfo As New MSMQQueueInfo
 Dim oQueue As MSMQQueue
 Dim oMessage As New MSMQMessage
 oQInfo.PathName = sQName
 Set oQueue = oQInfo.Open(MQ_RECEIVE_ACCESS, _
    MQ_DENY_NONE)
 Set oMessage = oQueue.Receive
 readFromQ = oMessage.Body
 oQueue.Close
 End Function


I won't explain every detail of this code. You can see that in the writeToQ method, an MSMQQueue is being opened for SEND access, using a queue name (such as "Server\flutters"). An MSMQMessage object is then created, and having had its Label and Body properties set, it's sent to the Queue. The readFromQ method basically reverses the process. In both cases, the body of the message is simply the contents of a Property Bag and is therefore completely general-purpose. This code hasn't been optimized for speed, and it might take a second or two to locate the queue each time it's called. A production program would need to be a bit smarter about opening the queue, and the MSMQ object model provides a range of access techniques.

You can use the MSMQ Explorer to monitor what's happening to a queue -- including viewing its contents. Figure 3 shows the Explorer taking a look at the "flutters" queue once four betting messages have been sent, and before any have been read. You can even open up a message and look at its contents.

Reading messages asynchronously
You might have been wondering how you find out if a message is waiting for you on a queue. By default, the MSMQQueue object's Receive method will wait indefinitely for a message to arrive if there isn't one on the queue already waiting. You can set a timeout parameter to make sure that your program doesn't hang on an empty queue, but even this means that you'd need to keep polling the queue if you wanted to respond dynamically to messages arriving. Fortunately, there's a better way.

If you're still wondering whether message queuing is for you, MSMQ's notification feature might well make your mind up. By subscribing to a queue on any machine, you can receive an event for every message that gets added to that queue. You can then read the messages from the queue and respond instantly. In other words, responding to a message arriving at a remote queue is no harder than responding to a button-click on a form.

You might have noticed that our Bet Capture user interface has a check box labelled "Receive Queue Notifications." If you run up the program on two different computers and then set this check box on one of them, you can write an object to the queue from one machine and instantly recreate that same object on the other. You can even configure things so that you immediately receive an event (on the consumer machine) for each new message stored on the queue. In the remainder of the article, we'll do this. First, declare two object variables at the module level of the form:

 Private moQueue As MSMQQueue 
 Private WithEvents moNotify As MSMQEvent


The first is a simple queue object. The second, an MSMQEvent object, has been declared "WithEvents" in order to receive notifications. Then we code the check box to enable notification when it's clicked:

 Private Sub chkNotify_Click()
 Dim oQInfo As New MSMQQueueInfo
 oQInfo.PathName = txtQName.Text
 Set moQueue = oQInfo.Open(MQ_RECEIVE_ACCESS, _
    MQ_DENY_NONE)
 Set moNotify = New MSMQEvent
 moQueue.EnableNotification moNotify
 End Sub


This code opens the queue and links the queue object to the MSMQEvent object. This ensures that an event will fire the next time a message is added to the specified queue. Finally, we need to code the event procedure for the oNotify object:

 Private Sub moNotify_Arrived(ByVal Queue As Object, _
    ByVal Cursor As Long)
 Dim b() As Byte
 Dim oHandler As New BagHandler
 b = oHandler.readFromQ(txtQName.Text)
 Set mBet = oHandler.UnStream(b)
 displayBet mBet
 DoEvents
 moQueue.EnableNotification moNotify
 End Sub


When the event fires, you simply read the queue in the normal way. The main thing to note here is that you need to re-enable notification after each event. In other words, calling the EnableNotification method only treats you to an event for the next message -- if you want more, you have to ask for them.

Conclusion
MSMQ and MTS have vastly extended the range of possibilities for VB programmers. By extending persistence from custom controls to standard VB classes and adding a Contents property to the Property Bag object, we can now treat entire objects as messages and send them around our programs and networks -- or store them safely and efficiently in a whole range of storage media.

Because the Property Bag mechanism is COM-based, it's entirely language neutral and comes with all the safeguards that COM provides. You could think of the ReadProperties method of a Property Bag as a third way to create objects. CreateObject and New allow you to create vanilla objects, while ReadProperties allows you to create "oven-ready" object structures with as much or as little content as you like.

I was investigating this new VB6 feature at exactly the same time I was coming to grips with MSMQ, and it was the fusion of these two ideas that provided the inspiration for this article. While I've covered Property Bags in some depth here, I've really only shown brief glimpses of what MSMQ is capable of. It's an industrial-strength product of major importance, and I have no doubt that in time (and not too much time), many people faced with data storage decisions will stop to decide whether they should use a database or simply a message queue to handle their data.

I haven't even touched on MSMQ's transactional capabilities. As far as MTS is concerned, writing a message to MSMQ is no different from storing data in a database -- both activities can be considered part of a transaction, both can be rolled back if needed, and both provide guaranteed durability of data.

MSMQ is particularly useful in three types of applications:


The most noticeable feature of these three types of applications is that they're becoming more and more common, especially in the brave new world of Internet-based commerce. "Talking objects" should have a bright future.

Download TURF.exe

Rob Macdonald is an independent software specialist based in London and southern England. In addition to consulting and training in Windows, client/server, VB, COM, and systems design and management, he also runs the U.K. ODBC User Group and is author of RDO and ODBC: Client Server Database Programming with Visual Basic, published by Pinnacle. +44 1722 782 433, salterton@msn.com.


Sidebar: Don't Overdo Persistence
This article has focused on reading and writing persistable objects to and from files and queues. But even if you never see yourself doing these things, some applications of using Property Bags might have come to mind. Here are some I've used: