Location transparency is a wonderful thing. Visual Basic and the universal marshaler work together to move your data magically across the network, and all you have to do is design your method signatures correctly. Without this help, you would have to write the code to move the data between processes. Luckily, the details of moving the data aren't your problem. You can think of every method signature as a slingshot that you design in order to fling data from the client to the object and back.
There's one frustrating limitation that neither Visual Basic nor the universal marshaler can overcome: The universal marshaler can't move an object across the network. This means that you can pass an object only by reference, not by value. When you declare an object parameter using ByVal, the reference is marshaled from the client to the object. When you declare an object parameter using ByRef, the reference is marshaled in both directions. However, the COM object (and all the precious data that's inside) never leaves the process in which it was created.
Figure 6-4 illustrates the dilemma. When you pass an object reference to a remote client, the data doesn't move. It remains on the machine where the object was created. The only thing that passes across the network is a COM-savvy pointer that lets the recipient establish a connection back to the object being referenced. When the recipient wants to access a property or a method through the object reference, it must go back across the proxy/stub layer to where the object lives.
Figure 6-4. Visual Basic and the universal marshaler don't support pass-by-value parameters with object data types. You can pass an object only by reference.
What does this mean to the Visual Basic programmer? It means that many straightforward programming techniques that you might use with in-process objects don't work well for out-of-process objects. For example, what if you are using a Visual Basic collection object to manage a set of CCustomer objects in a server-side process? How can you get the data for these customer objects over to a client application to fill a grid? One simple technique is to pass a reference to the collection over to the client. The client can then enumerate through the collection with a For Each loop and access the customer objects individually. However, this approach creates a big problem. Every loop in the For Each construct creates a new connection to an out-of-process customer object, and this connection requires a round-trip to the server to access any single property or method. If each customer object has five properties and the collection has hundreds of objects, the round-trips really start adding up.
It would be wonderful if you could simply marshal an object by value in the same way that you marshal a primitive data type, but this isn't possible. At times, you will want to move all the data associated with an object from one process to another. This is a common undertaking, but the universal marshaler can't help. If you want to simulate pass-by-value semantics, you must write your own code to intelligently move things across the network.
Programmers using languages other than Visual Basic, such as C++, occasionally resort to writing custom proxy code by hand by implementing a special interface IMarshal using a technique known as custom marshaling. Custom marshaling doesn't rely on the universal marshaler (Oleaut32.dll). Instead a custom proxy DLL is distributed to client desktops. Custom marshaling can give you complete control over how things are transmitted between the object and the client. It also lets you optimize the connection by simulating pass-by-value at the object level. However, custom marshaling is complicated. It requires in-depth knowledge of the RPC layer under COM, and few programmers are proficient in programming at this level. Another problem with custom marshaling it that it isn't supported by environments such as Microsoft Transaction Server.
A Visual Basic programmer can't participate in "true" custom marshaling but can accomplish something similar by writing intelligent code that optimizes how data is sent across the network. Pseudo-custom marshaling code written in Visual Basic simply leverages the universal marshaler instead of replacing it.
So how can you simulate pass-by-value with an object? One common technique for moving the data for an object uses a long-winded parameter list. If an object has 10 properties, you can create a set method and a get method with 10 parameters apiece. This is a bit tedious for both the class author and the client that calls these methods, but it's a quick-and-dirty way to get the data where it's needed in a single round-trip.
You can use a UDT instead of a long list of parameters to keep a method signature a little saner. This is especially helpful when your classes define a lot of properties. After you define a UDT in a public class module, you can use it to send a complex set of data between the client and the object. You can even create multiple UDTs for each class. For example, if your CCustomer class contains 20 properties but you typically need to marshal only 5 of them, you can create two UDTs, such as CustomerShort and CustomerLong. One UDT can define a common subset of customer properties, and the other can define the complete state of a customer object. UDTs let you dial in what you want to send across the network. They aren't supported in many older versions of COM, however. Before you design with UDTs, you must be sure that all of the computers in your production environment can handle them.
The two techniques described above are used to marshal the data for a single object. However, you will often need to move the data associated with multiple objects in a single round-trip. You can use any combination of variants, arrays, and UDTs to create data structures that are as complex as you like. The only limitation is your imagination. The example below moves the data for multiple customer objects across the network using a single method.
Sub GetCustomerData(ByRef arrData As Variant) ReDim arrData(1 To 2, 1 To 3) As String arrData(1, 1) = "Freddie" arrData(1, 2) = "Washington" arrData(1, 3) = "(310)594-4929" arrData(2, 1) = "Mary" arrData(2, 2) = "Contrary" arrData(2, 3) = "(310)493-9080" End Sub
Many development teams have discovered that the variant data type is very flexible for marshaling complex data structures. You can change the two-dimensional string arrays in the example above to an array of a different type or a different number of dimensions without changing the method signature. This means you don't have to worry about breaking binary compatibility when you redefine what you're marshaling. However, both the object and the client must agree on how the data structure is built. If you change the way that the object stores the data, you must make sure that the client knows how to use it. Some developers even add metadata to their structures to convey such information as a version number or a schema for the set of columns being marshaled.
One problem with marshaling a complex data structure is that it requires two complementary pieces of code. One piece constructs the data structure for transmission, and the other piece disassembles the data to use it in a meaningful way. If you are marshaling a complex data structure across the network from an object to a client application, you should probably hide this complexity from other Visual Basic programmers who are creating form-based desktop applications.
You can simplify things by creating a complementary proxy object in the client's process that encapsulates the complexity of your data structure. Client application programmers can simply create an instance of the proxy object and use it by calling a simple set of methods and properties. For example, you can create a client-side CCustomersProxy class for caching customer data returned from the GetCustomerData method. Examine the following class module:
' CCustomersProxy class Private RemoteCustomers As CCustomers Public LocalCustomers As Collection Private Sub Class_Initialize() Set RemoteCustomers = New CCustomers Set LocalCustomers = New Collection Dim Temp As Variant RemoteCustomers.GetCustomerData Temp ' Create a local collection of CCustomer objects. Dim Customer As CCustomer, Position As Long For Position = LBound(Temp, 1) To UBound(Temp, 1) Set Customer = New CCustomer Customer.FirstName = Temp(Position, 1) Customer.LastName = Temp(Position, 2) Customer.Phone = Temp(Position, 3) LocalCustomers.Add Customer Next Position End Sub
The diagram in Figure 6-5 shows how things are set up. The proxy object's Class_Initialize method takes the two-dimensional string array returned by GetCustomerData and uses it to create a set of CCustomer objects in the client process. The CCustomer objects are added to a local Visual Basic collection object for convenience.
Figure 6-5. You can use a smart proxy object on the client side to hide the details of marshaling complex data structures. The proxy object can download data and create local objects and collections for fast, convenient client-side access.
From a high-level design perspective, it makes sense to distribute the CCustomersProxy class and the CCustomer class in a client-side ActiveX DLL. Visual Basic programmers who create form-based applications on the desktop can simply reference the DLL and use your proxy object. Here is the required code:
Private Proxy As CCustomersProxy Private Sub Form_Load() Set Proxy = New CCustomersProxy End Sub Private Sub cmdFillListbox_Click() Dim Customer As CCustomer For Each Customer In Proxy.LocalCustomers lstCustomer.AddItem Customer.LastName Next Customer End Sub
As you can see, this technique gives you the best of both worlds. It gives you the efficiency of marshaling a complete set of data in a single round-trip, and it allows other programmers to write code in a style that is more natural to the average Visual Basic programmer. You can assume that the proxy object will never be accessed out of process, so you can expose properties and use collections more freely.