You can choose from two ways to program with MSMQ. MSMQ exposes a C-level API as well as a set of ActiveX components. As a Visual Basic programmer, you're much better off using the ActiveX components when you program MSMQ. Note that some MSMQ functionality is accessible only through the C-level API. Fortunately, you can meet most of the common requirements for message passing in a distributed application by using the ActiveX components. In the rare circumstances in which you need to use the C-level API, you can use Visual Basic Declare statements and call into the API directly. However, this chapter will examine only the use of ActiveX components. Once you include a reference to MSMQ's type library (the Microsoft Message Queue Object Library) in your Visual Basic project, you can use these ActiveX components in your code.
I won't cover every possibility for programming with MSMQ's ActiveX components. MSMQ provides several ActiveX components, and each one has quite a few properties and methods. Instead, I'll simply get you started by offering MSMQ programming examples that demonstrate the most common types of code required in a distributed application. You should use the HTML document MSMQ Programmer's Reference to complement the material presented in this chapter.
Let's begin our tour of MSMQ programming by creating a queue. MSMQ lets you create both public queues and private queues. We'll start by creating a public queue. In this chapter, you should assume that all queues are public unless I indicate they are private.
You can create a public queue in one of two ways. You can create a queue by hand using the MSMQ Explorer, or you can create a queue programmatically. We'll create one by hand first. Simply right-click on a computer in the MSMQ Explorer, and choose Queue from the New menu. You must give the queue a name and specify whether you want the queue to be transactional. I'll defer a discussion of transactional queues until later in this chapter. For now, just create a queue by giving it a name, and leave the Transactional check box deselected. Click OK to create the queue.
After you create the queue, you can examine its attributes by right-clicking on it and choosing Properties. You'll see a tabbed dialog box in which you can modify various properties of the queue. When you examine the queue properties, you'll notice that MSMQ has automatically generated a GUID to identify the queue.
You can also create a queue programmatically using an MSMQQueueInfo object. First you must create the object and assign it a valid PathName. A queue's PathName should include the name of the computer and the name of the queue. For example, look at the following code:
Dim qi As MSMQQueueInfo Set qi = New MSMQQueueInfo qi.PathName = "MyComputer\MyQueue" qi.Label = "My Queue" qi.Create
This example uses an MSMQQueueInfo object to create a new queue. Once you set the PathName property, you can create a queue by invoking the Create method. This example also sets the Label property of the new queue. A label is optional, but it can be helpful when you need to locate the queue later on.
The Create method takes two optional parameters. The first parameter indicates whether the new queue is transactional. The second parameter, IsWorldReadable, lets you indicate whether the queue will be readable to users other than the owner. The default value for this parameter is False, which means that only the queue's owner is able to receive messages from the queue. If you pass True to this parameter, the queue can be read by all users. Whatever you pass, all users can send messages to the queue. You can also set queue security permissions by modifying the discretionary access control list (DACL) for the queue. You do this by opening the queue's Properties dialog box and navigating to the Security tab in the MSMQ Explorer.
Note that you can abbreviate the PathName for a local queue so that you don't have to hardcode the name of the computer. You must do this when you want to write generic code that will run on many different computers. A single dot (as in .\MyQueue) signifies that the queue path is defined on the local computer. You can use this abbreviated form when you create and open a local queue. For example, you can rewrite the previous code as follows:
Dim qi As MSMQQueueInfo Set qi = New MSMQQueueInfo qi.PathName = ".\MyQueue" qi.Label = "My Queue" qi.Create
In addition to creating queues, you can use an MSMQQueueInfo object when you want to search for or open an existing queue. Let's say you want to get a little tricky and create a queue when one with a predefined caption doesn't already exist. First, you can run a query against the MQIS with an MSMQQuery object to determine whether a queue with a certain label already exists. You run a query by invoking the LookupQueue method, which returns an MSMQQueueInfos object. The MSMQQueueInfos object is a collection of MSMQQueueInfo objects that match your lookup criteria. Here's an example of conducting a lookup by a queue's caption:
Dim qry As MSMQQuery Set qry = New MSMQQuery Dim qis As MSMQQueueInfos Set qis = qry.LookupQueue(Label:="MyComputer\MyQueue") Dim qi As MSMQQueueInfo Set qi = qis.Next If qi Is Nothing Then ' The queue did not exist. Set qi = New MSMQQueueInfo qi.PathName = "MyComputer\MyQueue" qi.Label = "MyComputer\MyQueue" qi.Create End If
In this example, a new queue is created only if a queue with the label MyComputer\MyQueue doesn't already exist. Note that you can also use other types of criteria when you run a lookup query.
Now let's open a queue and send a message. The next object you need to understand is an MSMQQueue object. At first, the relationship between MSMQQueueInfo objects and MSMQQueue objects can be a little confusing. It's reasonable to conclude that an MSMQQueue object represents a physical queue because of its name. However, you're better off thinking of it as a queue handle. For example, you can open three different MSMQQueue objects on the same physical queue:
Dim qi As MSMQQueueInfo Set qi = new MSMQueueInfo qi.PathName = ".\MyQueue" Dim qSend As MSMQQueue Set qSend = qi.Open(MQ_SEND_ACCESS, MQ_DENY_NONE) Dim qPeek As MSMQQueue Set qPeek = qi.Open(MQ_PEEK_ACCESS, MQ_DENY_NONE) Dim qReceive As MSMQQueue Set qReceive = qi.Open(MQ_RECEIVE_ACCESS, MQ_DENY_NONE)
You can see that an MSMQQueueInfo object represents a physical queue and that an MSMQQueue object actually represents an open handle to the queue. When you call Open, you must specify the type of access you want in the first parameter. You can peek at as well as receive from a queue when you open it with MQ_RECEIVE_ACCESS. However, if you want to send messages while also peeking at or receiving from the same queue, you must open two MSMQQueue objects. Remember to invoke the Close method on an MSMQQueue object as soon as you've finished using it.
You can use the second parameter to Open to specify the share mode for the queue. The default value of this parameter is MQ_DENY_NONE, which means that the queue can be opened by more than one application for receive access at the same time. You must use this setting when you open a queue using MQ_PEEK_ACCESS or MQ_SEND_ACCESS. However, when you open a queue with receive access, you can set the share mode to MQ_DENY_RECEIVE_SHARE to prevent other applications from receiving messages at the same time. When one application opens a queue with both MQ_RECEIVE_ACCESS and MQ_DENY_RECEIVE_SHARE, no other application can open the queue in receive mode. An application using this mode will be the only one that can remove messages from the queue.
When you create a public queue, MSMQ assigns it an identifying GUID and publishes it in the MQIS. This allows other applications to open the queue by assigning the computer name and queue name to the PathName property. This also allows other applications to find the queue by running queries against the MQIS. However, the process of publishing a public queue takes up time and disk space and is sometimes unnecessary.
Imagine an application that consists of hundreds or thousands of independent clients that all require a local response queue. In this situation, it makes sense to use private queues. Private queues must be created locally, and they are not published in the MQIS. They're published only on the computer on which they reside.
As you'll see later in this chapter, you can send the information about a private response queue in the header of a request message. This lets you establish bidirectional communication between a client application and the server. More important, using private queues means that you don't have to publish all those response queues, which saves both time and disk space. You can create a private queue by adding Private$ to the queue's PathName, like this:
Dim qResponseInfo As MSMQQueueInfo Set qResponseInfo = New MSMQQueueInfo qResponseInfo.PathName = ".\Private$\MyResponseQueue" qResponseInfo.Create
MSMQ applications can send messages to private queues on other machines as long as they can find the queues. This isn't as easy as locating public queues because you can't open a private queue using a PathName—it isn't published in the MQIS. Later in this chapter, I'll show you a technique for passing the response queue's information to another application in a request message.
Another way that you can send messages to private queues on another computer is by using the FormatName property. This technique is valuable when you are dealing with private queues on disconnected clients. When a queue is created, MSMQ creates a FormatName for it. Here's an example of two different FormatName properties for a public queue and a private queue:
The FormatName of a public queue includes the GUID that identifies the queue in the MQIS. A private queue doesn't have its own GUID. Instead, its FormatName includes the GUID that identifies the local computer and an extra computer-specific queue identifier. An application can send messages to a private queue across the network by assigning the FormatName before invoking the Open method. Of course, the application must know the FormatName ahead of time.
Let's send our first message. MSMQ makes this task remarkably easy. You can create a new MSMQMessage object and prepare it by setting a few properties. You can then invoke the MSMQMessage object's Send method, and MSMQ will route your message to its destination queue. Here's a simple example:
Dim qi As MSMQQueueInfo Set qi = New MSMQQueueInfo qi.PathName = ".\MyQueue" Dim q As MSMQQueue Set q = qi.Open(MQ_SEND_ACCESS, MQ_DENY_NONE) ' Create a new message. Dim msg As MSMQMessage Set msg = New MSMQMessage ' Prepare the message. msg.Label = "My superficial label" msg.Body = "My parameterized request information" msg.Priority = MQ_MAX_PRIORITY ' Send message to open queue. msg.Send q q.Close
As you can see, MSMQ's ActiveX components make it pretty easy to open a queue and send a message. The message in the last example was prepared by setting three properties. The Caption is a string property of the message header that distinguishes or identifies a particular message. The two other message properties are the message body and the message priority.
In MSMQ, a message body is stored as an array of bytes. The body is typically used to transmit parameterized data between the sender and the receiver. This example demonstrates that you can simply assign a Visual Basic for Applications (VBA) string to a message body. The receiver can read this string from the message body just as easily. However, in many cases you'll use a message body that is more complex. For example, you might need to pass multiple parameters from the sender to the receiver. I'll revisit this topic later in this chapter and discuss how to pack parameterized information into the message body.
The last property used in the example is the message priority. A message has a priority value between 0 and 7; the higher the value, the higher the priority. MSMQ stores messages with higher priority levels at the head of the queue. For example, a message with a priority level of 6 is placed in the queue behind all messages of priority 7 and behind messages of priority 6 that have already been written to the queue. The new message is placed ahead of any message of priority 5 or lower. The MSMQ type library contains the constants MQ_MAX_PRIORITY (7) and MQ_MIN_PRIORITY (0). The default priority for a new message is 3.
You can use the MSMQ Explorer to examine the messages in a queue, as shown in Figure 11-5. You should see a list of all the messages that have been sent to the queue but have not been received. As you can see, messages with the highest priority are placed at the head of the queue. The message at the head is usually the first one to be received.
You must have read permissions for a queue in order to see the messages in it with the MSMQ Explorer. There might be times when your installation of MSMQ doesn't give you these read permissions by default. You can modify the access permissions for a queue by right-clicking on it in the MSMQ Explorer and choosing Properties. If you navigate to the Security tab, you can change both the owner and the permissions for the queue so you can see the messages inside it. It's especially useful to look at the header attributes and bodies of messages when you're beginning to program with MSMQ.
Figure 11-5. You can examine the messages in a queue using the MSMQ Explorer. Messages with the highest priority are at the head of the queue.
Before I move on to the next section, I want to introduce a few other important message properties. The first is the Delivery property, which has two possible settings. The default setting is MQMSG_DELIVERY_EXPRESS, which means that the message is sent in a fast but unreliable fashion. Express messages are retained in memory only while they're being routed across various computers toward their destination queue. If a computer crashes while holding express messages, the messages could be lost.
To ensure that a message isn't lost while being routed to its destination queue, you can set the Delivery property to MQMSG_DELIVERY_RECOVERABLE. The message will be flushed to disk as it is passed from one computer to another. The disk I/O required with recoverable messages results in significant performance degradation, but the message won't be lost in the case of a system failure. When you send nontransactional messages, you must explicitly set the Delivery property if you want recoverable delivery. When you send transactional messages, the Delivery property is automatically set to MQMSG_DELIVERY_RECOVERABLE.
When a message is sent to a queue, MSMQ assigns it an ID property. This property is a 20-byte array that uniquely identifies the message. MSMQ generates the ID by using two different values. The first 16 bytes of the ID are the GUID of the sending computer. (MSMQ assigns an identifying GUID to every computer during installation.) As you can see in Figure 11-5 (shown earlier), the first part of the message ID is always the same for any message sent from the same computer. The last 4 bytes of the ID are a unique integer generated by the sending computer. In most cases, you don't need to worry about what's inside the Byte array. However, if you need to compare two IDs to see whether they represent the same message, you can use VBA's StrComp function with the vbBinaryCompare flag.
Each message also has a CorrelationID property. Like the ID, this property is also stored as a 20-byte array. Let's look at a problem to see why this property is valuable. Let's say that a client application sends request messages to a server. The server processes the requests and sends a response message for each request. How does the client application know which request message is associated with which response message? The CorrelationID property solves this problem.
When the server processes a request, it can assign the ID of the incoming request message to the CorrelationID of the outgoing response message. When the client application receives a response message, it can compare the CorrelationID of the response message with the ID from each request message. This allows the sender to correlate messages. As you can see, the CorrelationID is useful when you create your own response messages. As you'll see later in this chapter, MSMQ also assigns the proper CorrelationID automatically when it prepares certain system-generated messages, such as an acknowledgment message.
To receive a message, you first open an MSMQQueue object with receive access, and then you invoke the Receive method to read and remove the first message in the queue:
Dim qi As MSMQQueueInfo Set qi = New MSMQQueueInfo qi.PathName = ".\MyQueue" Dim q As MSMQQueue Set q = qi.Open(MQ_RECEIVE_ACCESS, MQ_DENY_NONE) Dim msg As MSMQMessage ' Attempt to receive first message in queue. Set msg = q.Receive(ReceiveTimeout:=1000) If Not (msg Is Nothing) Then ' You have removed the first message from the queue. MsgBox msg.Body, vbInformation, msg.Label Else ' You timed out waiting on an empty queue. End If q.close
There's an interesting difference between sending and receiving a message with MSMQ. You invoke the Send method on an MSMQMessage object, but you invoke the Receive method on an MSMQQueue object. (This doesn't really cause problems; it's just a small idiosyncrasy of the MSMQ programming model.) If a message is in the queue, a call to Receive removes it and returns a newly created MSMQMessage object. If there's no message in the queue, a call to Receive behaves differently depending on how the timeout interval is set.
By default, a call to Receive has no timeout value and will block indefinitely if no message is in the queue.
If you don't want the thread that calls Receive to block indefinitely, you can specify a timeout interval. You can use the ReceiveTimeout parameter to specify the number of milliseconds that you want to wait on an empty queue. If you call Receive on an empty queue and the timeout interval expires before a message arrives, the call to Receive returns with a null reference instead of an MSMQMessage object. The code in the last example shows how to set a timeout value of 1000 milliseconds. It also shows how to determine whether a message arrived before the timeout expired. If you don't want to wait at all, you can use a ReceiveTimeout value of 0. A ReceiveTimeout value of ?1 indicates that you want to wait indefinitely. (This is the default if you don't pass a timeout value.)
You can call Receive repeatedly inside a Do loop to synchronously remove every message from a queue. The following example shows how to receive all the messages from a queue and fill a list box with message captions:
Dim qi As MSMQQueueInfo Set qi = New MSMQQueueInfo qi.PathName = ".\MyQueue" Dim q As MSMQQueue Set q = qi.Open(MQ_RECEIVE_ACCESS, MQ_DENY_RECEIVE_SHARE) Dim msg As MSMQMessage Set msg = q.Receive(ReceiveTimeout:=0) Do Until msg Is Nothing lstReceive.AddItem msg.Label Set msg = q.Receive(ReceiveTimeout:=0) Loop q.Close
You can set the share mode for MQ_DENY_RECEIVE_SHARE so that your application won't have to contend with other applications while removing messages from the queue. Use a timeout value of 0 if you want to reach the end of the queue and move on to other business as soon as possible.
Sometimes you'll want to inspect the messages in a queue before removing them. You can use an MSMQQueue object's peek methods in conjunction with an implicit cursor to enumerate through the message in a queue. After opening a queue with either receive access or peek access, you can call Peek, PeekCurrent, or PeekNext.
Peek is similar to Receive in that it reads the first message in the queue. However, Peek doesn't remove the message. If you call Peek repeatedly, you keep getting the same message. Another problem with Peek is that it has no effect on the implicit cursor behind the MSMQQueue object. Therefore, it is more common to work with PeekCurrent and PeekNext.
You can move the implicit cursor to the first message in a queue with a call to PeekCurrent. As with a call to Receive, you should use a timeout interval if you don't want to block on an empty queue. After an initial call to PeekCurrent, you can enumerate through the rest of the messages in a queue by calling PeekNext:
Dim qi As MSMQQueueInfo Set qi = New MSMQQueueInfo qi.PathName = ".\MyQueue" Dim q As MSMQQueue Set q = qi.Open(MQ_PEEK_ACCESS, MQ_DENY_NONE) Dim msg As MSMQMessage Set msg = q.PeekCurrent(ReceiveTimeout:=0) Do Until msg Is Nothing ' Add message captions to a list box. lstPeek.AddItem msg.Label Set msg = q.PeekNext(ReceiveTimeout:=0) Loop q.Close
The ReceiveCurrent method is often used in conjunction with PeekCurrent and PeekNext. For example, you can enumerate through the messages in a queue by peeking at each one and comparing the properties of the current message against criteria of the messages you want to receive and process. For example, after calling PeekCurrent or PeekNext, you can compare the label of the current message with a specific caption that you're looking for. If you come across a message with the caption you're looking for, you can call ReceiveCurrent to remove it from the queue and process it.
The examples I have shown so far of peeking and receiving messages have all used synchronous techniques for examining and removing the messages in a queue. These techniques are easy ways to read or remove all the messages that are currently in a queue. They also let you process future messages as they are sent. The following code doesn't use a timeout interval; it blocks until a message is sent to the queue. It processes all messages until the queue is empty and then blocks until more messages arrive:
' Assume q is an open MSMQQueue object with receive access. Dim msg As MSMQMessage Do While True ' Loop forever. ' Wait indefinitely for each message. Set msg = q.Receive() ' Process message. Loop
While this style of coding allows you to process messages as they arrive, it also holds the calling thread hostage. If you have a single-threaded application, the application can't do anything else. However, you can use MSMQ events as an alternative to this synchronous style of message processing. MSMQ events let your application respond to asynchronous notifications that are raised by MSMQ as messages arrive at a queue. You can therefore respond to a new message without having to dedicate a thread to block on a call to Receive or PeekNext.
Let's look at how MSMQ events work. The MSMQ eventing mechanism is based on the MSMQEvent component. To use events, you must first create an MSMQEvent object and set up an event sink. Next you must associate the MSMQEvent object with an MSMQQueue object that has been opened for either peek access or receive access. You create the association between the two objects by invoking the EnableNotification method on the MSMQQueue object and passing a reference to the MSMQEvent object. After you call EnableNotification, MSMQ notifies your application when a message has arrived by raising an Arrived event.
You learned how to set up an event sink with Visual Basic in Chapter 6. As you'll recall, to create an event sink you must use the WithEvents keyword and declare the source object's reference variable in the declaration section of a form module or a class module. The following code shows how to set up an event sink for a new MSMQEvent object and associate it with an open MSMQQueue object:
Private qPeek As MSMQQueue Private WithEvents qPeekEvents As MSMQEvent Private Sub Form_Load() Dim qi As MSMQQueueInfo Set qi = New MSMQQueueInfo qi.PathName = ".\MyQueue" Set qPeek = qi.Open(MQ_PEEK_ACCESS, MQ_DENY_NONE) Set qPeekEvents = New MSMQEvent qPeek.EnableNotification qPeekEvents End Sub
This example uses peek access, but events work in a similar manner for receiving messages. Once you set up the MSMQEvent object's event sink and call EnableNotification, you will be notified with an Arrived event as soon as MSMQ finds a message in the queue. Here's an implementation of the Arrived event that adds the caption of new messages to a list box as they arrive in the queue:
Sub qPeekEvents_Arrived(ByVal Queue As Object, ByVal Cursor As Long) Dim q As MSMQQueue Set q = Queue ' Cast to type MSMQQueue to avoid IDispatch. Dim msg As MSMQMessage Set msg = q.PeekCurrent(ReceiveTimeOut:=0) If Not (msg Is Nothing) Then lstPeek.AddItem msg.Label End If q.EnableNotification qPeekEvents, MQMSG_NEXT End Sub
Note that this example calls EnableNotification every time an Arrived event is raised. This is required because a call to EnableNotification sets up a notification for only the next message. If you want to receive notifications in an ongoing fashion, you must keep calling EnableNotification in the Arrived event. It is also important to pass the appropriate cursor constant when you call EnableNotification. This example passes the constant MQMSG_NEXT in order to advance the implicit cursor. The next time an Arrived event is raised, a call to PeekCurrent examines the next message in the queue.
You should also note that the code in the example above peeks at every message that was stored in the queue when the MSMQEvent object was set up. In other words, MSMQ raises events for existing messages as well as future messages. If you care only about future messages, you can synchronously advance the implicit cursor to the last existing message before calling EnableNotification.
When you prepare a message, you must often pack several different pieces of parameterized information into the body before sending it to a queue. On the receiving side, you must also be able to unpack these parameters before you start processing the sender's request. Up to this point, I've shown you only how to pass simple VBA strings in a message body. Now we'll look at how to pass more complex data structures.
A message body is a Variant that is stored and transmitted as a Byte array. You can read and write the usual VBA data types to the body, such as Boolean, Byte, Integer, Long, Single, Double, Currency, Date, and String. MSMQ tracks the type you use in the message header. This makes it quite easy to store a single value in a message body. However, it doesn't solve the problem of packing in several pieces of data at once. To pack several pieces of data into a message, you must understand how to use the Byte array behind the message body.
Using an array behind the message body is tricky because it must be an array of bytes. If you assign another type of array to the message body, MSMQ converts it to a Byte array. Unfortunately, once your data has been converted to a Byte array, there's no easy way to convert it back to the original array type on the receiving side. This means that a simple technique such as sending your parameters in a String array won't work as you might hope. A Byte array is flexible because it can hold just about any binary or text-based data. If you don't mind working with a Byte array directly, you can pack the message body using code like this:
Dim msg As MSMQMessage Set msg = New MSMQMessage Dim data(11) As Byte ' Fill the array with parameterized data. data(0) = 65: data(1) = 66 data(2) = 67: data(3) = 68 data(4) = 49: data(5) = 51 data(6) = 53: data(7) = 55 data(8) = 57: data(9) = 97 data(10) = 98: data(11) = 99 msg.Body = data msg.Send q
Figure 11-6 shows the Body tab of the message's Property dialog box, which you can view using the MSMQ Explorer. The message body shown in the figure is the same one that was generated in the last code example. The Body tab shows the contents of the message in both hexadecimal format and ANSI format.
How do you unpack this Byte array from the message body in a receiver application? It's pretty easy. All you have to do is create a dynamic array reference and assign the message body to it, like this:
Dim msg As MSMQMessage Set msg = q.Receive() Dim d() As Byte d = msg.Body ' Now the Byte array is populated. ' For example, to inspect value in position 2 Dim Val As Byte Val = d(2)
While it's important for you to understand that the message body is always stored as a Byte array, the technique I have just shown isn't always the best way to pack and unpack your parameterized information. Writing and reading Byte arrays gives you as much flexibility as MSMQ can offer, but it doesn't offer high levels of productivity.
Figure 11-6. A message body is always stored as a Byte array. The left side shows the hexadecimal value of each byte in the array; the right side displays the ANSI character that represents the value of each byte. The first byte in this body has the decimal value 65 and the hexadecimal value 41, and the letter A is its ANSI character representation.
It can also be tricky and time consuming to write the code for packing and unpacking several pieces of parameterized information into a Byte array. Several other techniques are easier and faster to program. You should work directly with Byte arrays only when the data being packed is fairly straightforward or no other technique can give you the results you need.
OK, let's put your knowledge of Byte arrays to work and pack several parameters into a single message body. Suppose you want to send a request message to submit a sales order. The body of the request message must include a customer name, a product name, and a requested quantity. How do you pack these three pieces of information into a message body? We'll look at three different techniques: using a string parsing technique, using a Visual Basic PropertyBag object, and using a persistent Visual Basic class to read and write an entire object into the message body.
You've already seen that it's easy to write and read a VBA string to and from a message body. As you'll recall from Chapter 6, a VBA string is stored internally using a COM data type known as a basic string (BSTR). A BSTR maintains the actual string data with an array of Unicode characters. Because a BSTR is based on Unicode, it requires 2 bytes per character; ANSI strings require only 1 byte per character.
Packing a VBA string into a message body is easy because MSMQ does the Byte array conversions for you behind the scenes. When you assign a string to a message body, MSMQ simply converts the Unicode characters array to a Byte array. On the receiving side, when you assign the message body to a string variable, MSMQ creates a new BSTR and populates it with the Unicode characters from inside the Byte array. The conversion going on behind the scenes is somewhat complicated, but things couldn't be easier in terms of the Visual Basic code that you must write.
Now let's look at a simple string parsing technique to write the three parameters to a message body. You can simply create a long string by concatenating your parameters and using a character such as a semicolon (;) to delimit each one. This string can be easily written to and read from the message body. The only tricky part is writing the code to pack and unpack the string. Let's begin by packing the string:
Function PackMessage1(ByVal Customer As String, _ ByVal Product As String, _ ByVal Quantity As Long) As String PackMessage1 = Customer & ";" & Product & ";" & CStr(Quantity) End Function
The PackMessage1 method takes three parameters and embeds them in a single VBA string. The embedded semicolons are used by the receiving code to unpack the string. The sending application can now use PackMessage1 to pack up a message and send it on its way:
Dim MsgBody As String MsgBody = PackMessage1("Bob", "Ant", 100) msg.Body = MsgBody msg.Send q
On the receiving side, you must provide the code to unpack the string. The following UnpackMessage1 method walks the string and pulls out the packed parameter values one by one:
Private Sub UnpackMessage1(ByVal MsgBody As String, _ ByRef Customer As String, _ ByRef Product As String, _ ByRef Quantity As Long) Dim StartPosition As Integer, Delimiter As Integer StartPosition = 1 Delimiter = InStr(StartPosition, MsgBody, ";") Customer = Mid(MsgBody, StartPosition, Delimiter - StartPosition) StartPosition = Delimiter + 1 Delimiter = InStr(StartPosition, MsgBody, ";") Product = Mid(MsgBody, StartPosition, Delimiter - StartPosition) StartPosition = Delimiter + 1 Quantity = CLng(Mid(MsgBody, StartPosition, Len(MsgBody) - Delimiter)) End Sub
Now that you have the code to unpack the string, the rest is fairly straightforward. You can receive or peek at a message and extract the request parameters from the body. Here's an example of using the UnpackMessage1 method in the receiving application:
Set msg = q.Receive() Dim PackedMsg As String PackedMsg = msg.Body Dim Customer As String, Product As String, Quantity As Long UnpackMessage1 PackedMsg, Customer, Product, Quantity ' Customer, Product, and Quantity are now populated.
Parsing strings offers much higher productivity than using a Byte array directly. While the code might be tedious to write, it usually isn't very complicated. It's also much easier than working with Byte arrays. However, Visual Basic 6 has a few new options that you should consider before you decide how to pack and unpack your parameters. In the following sections, I'll present two other Visual Basic 6 techniques that offer higher levels of productivity than this parsing technique.
PropertyBag objects aren't new with Visual Basic 6. You might have used them if you programmed ActiveX controls with version 5. However, Visual Basic 6 is the first version that allows you to create PropertyBag objects with the New operator. This means you can create a stand-alone PropertyBag object to pack and unpack your parameters.
A PropertyBag object is useful because it can automate most of the tedious work of packing and unpacking your parameterized information. Each PropertyBag object has a Contents property, which represents a Byte array. You can write named values into this Byte array using the WriteProperty method. Once you write all your parameters into a PropertyBag object, you can use the Contents property to serialize the Byte array into the message body:
Function PackMessage2(ByVal Customer As String, _ ByVal Product As String, _ ByVal Quantity As Long) As Byte() Dim PropBag As PropertyBag Set PropBag = New PropertyBag PropBag.WriteProperty "Customer", Customer PropBag.WriteProperty "Product", Product PropBag.WriteProperty "Quantity", Quantity PackMessage2 = PropBag.Contents End Function
This method takes three parameter values and returns a Byte array. (Note that Visual Basic 6 can use the array type as a method return value.) The PropertyBag object writes your named values into a stream of bytes using its own proprietary algorithm. You can use the PackMessage2 method in the sender application to pack a message body, like this:
Dim msg As MSMQMessage Set msg = New MSMQMessage msg.Body = PackMessage2("Bob", "Ant", 100) msg.Send q
Once you pack up a Byte array in the sender application, you need a second PropertyBag object on the receiving side to unpack it. The UnpackMessage2 method unpacks the message using the ReadProperty method of the PropertyBag object:
Sub UnpackMessage2(ByRef MsgBody() As Byte, _ ByRef Customer As String, _ ByRef Product As String, _ ByRef Quantity As Long) Dim PropBag As PropertyBag Set PropBag = New PropertyBag PropBag.Contents = MsgBody Customer = PropBag.ReadProperty("Customer") Product = PropBag.ReadProperty("Product") Quantity = PropBag.ReadProperty("Quantity") End Sub
Now you can use the UnpackMessage2 method in the receiver application to unpack the message:
Set msg = q.Receive() Dim Customer As String, Product As String, Quantity As Long UnpackMessage2 msg.Body, Customer, Product, Quantity ' Customer, Product, and Quantity are now populated.
As you can see, the PropertyBag object makes your life much easier because it packs and unpacks your parameters for you. This technique does carry some overhead compared to the string parsing technique, however. The PropertyBag object writes proprietary header information into the Byte array in addition to the name of each property. To give you an idea of how much overhead is involved, let's compare the two code examples above. The code for the string parsing technique created a message body 22 bytes long, and the PropertyBag technique created a message body 116 bytes long.
The overhead of the PropertyBag technique depends on the size of the parameters being passed. The overhead becomes less noticeable as your parameter values become larger. Also keep in mind that the header information for each MSMQ message is quite large itself. An MSMQ message header typically contains 136 bytes or more no matter how big the body is. You must weigh the trade-offs between productivity and efficiency.
The last technique for passing parameterized information in a message body is perhaps the most exciting. MSMQ lets you read and write entire objects to the message body. However, the object must belong to a certain category. MSMQ can serialize the properties of an object into and out of a message body if the object implements either IPersistStream or IPersistStorage. These are two standard COM interfaces that derive from IPersist.
The interface definitions for IPersistStream and IPersistStorage contain parameters that are incompatible with Visual Basic. You can't implement these interfaces in a straightforward manner using the Implements keyword. Fortunately, Visual Basic 6 has added persistable classes. When you create a persistable class, Visual Basic automatically implements IPersistStream behind the scenes. Persistable classes let you read and write objects in and out of the message body directly.
Every public class in an ActiveX DLL and ActiveX EXE project has a Persistable property. You must set this property to Persistable at design time to make a persistent class. When you make a class persistent, the Visual Basic IDE lets you add a ReadProperties and a WriteProperties method to the class module. You can add the skeletons for these two methods using the wizard bar. (The wizard bar consists of two combo boxes at the top of the class module window.) You can also add the InitProperties method, although it isn't required when you use MSMQ.
You can use the ReadProperties and WriteProperties methods to read your properties to an internal PropertyBag object. Visual Basic creates this PropertyBag object for you behind the scenes and uses it to implement IPersistStream. Remember, your object must implement IPersistStream in order for MSMQ to write it to a message body. When MSMQ calls the methods in the IPersistStream interface, Visual Basic simply forwards these calls to your implementations of ReadProperties and WriteProperties.
Using persistable classes with MSMQ is a lot easier to use than it sounds. For example, you can create a new persistable class and add the properties you want to pack into the message body. Next you provide an implementation of ReadProperties and WriteProperties. Here's a Visual Basic class module that does this:
' COrderRequest: a persistable class. Public Customer As String Public Product As String Public Quantity As Long Private Sub Class_ReadProperties(PropBag As PropertyBag) Customer = PropBag.ReadProperty("Customer", "") Product = PropBag.ReadProperty("Product", "") Quantity = PropBag.ReadProperty("Quantity", "") End Sub Private Sub Class_WriteProperties(PropBag As PropertyBag) PropBag.WriteProperty "Customer", Customer PropBag.WriteProperty "Product", Product PropBag.WriteProperty "Quantity", Quantity End Sub
As you can see, it's pretty easy. Once you have a persistable class like the one shown above, you can pack it into a message body, like this:
Dim msg As MSMQMessage Set msg = New MSMQMessage ' Create and prepare object. Dim Order As COrderRequest Set Order = New COrderRequest Order.Customer = txtPCS1 Order.Product = txtPCS2 Order.Quantity = txtPCS3 ' Assign the object to the message body. ' Your WriteProperties is called. msg.Body = Order msg.Send q
When you assign an object to the message body, MSMQ performs a QueryInterface on the object to see whether it supports either IPersistStream or IPersistStorage. Since your object supports IPersistStream, MSMQ knows that it can call a method on this interface named Save. Visual Basic forwards the call to Save to your implementation of WriteProperties. You write your parameters into the PropertyBag, and these values are automatically copied into the message body as an array of bytes.
In the receiver applications, things are just as easy. You can rehydrate a persistent object from a message body by creating a new reference and assigning the message body to it:
Set msg = q.Receive(ReceiveTimeOut:=0) Dim Order As COrderRequest Set Order = msg.Body Dim Customer As String, Product As String, Quantity As Long Customer = Order.Customer Product = Order.Product Quantity = Order.Quantity
When you assign a message body to a reference using the Set keyword, MSMQ creates a new instance of the object and calls the Load method of IPersistStream. Visual Basic forwards this call to your implementation of ReadProperties. Once again, you use the PropertyBag object to extract your data.
You should keep a few things in mind when you use persistable classes with MSMQ. First, this parameter-packing technique uses a bit more overhead than the technique using a stand-alone PropertyBag object, and it uses considerably more overhead than the string parsing technique. Second, you should create your persistable classes in ActiveX DLLs so that every application that sends and receives messages can leverage the same code.
One last thing to note is that you can use persistable classes with MSMQ only after you have installed Windows NT Service Pack 4. Earlier versions of MSMQ aren't compatible with Visual Basic's implementation of IPersistStream. In particular, your code will fail when you try to assign an object created from a persistable class to an MSMQ message body. This means you must install Windows NT Service Pack 4 (or later) on all your production machines as well as your development workstations when you start working with persistable classes.