[ Home | FAQ | OCX Book | Sites ]  

OLE Controls Frequently Asked Questions
OLE Automation Questions

Last Update: May 13, 1996

How do I pass a structure or a pointer to a structure using Automation?

Here's an example of the problem:

struct MyType
{
   short sSize;
   char  szName[30];
   double dBalance;
};

void DoSomething( struct MyType* pMyType )
{
   pMyType->sSize = 100;
   strcpy( pMyType->szName, "Steve Levine" );
   pMyType->bBalance = 1455.12;
}

How can I pass such a structure between a client and server using OLE Automation? There are basically five ways to solve this problem using automation:

1) if your data is composed of a series of automation supported types, you can use a safe array. This requires a VARIANT parameter and that both sides understand how to work with safe arrays. This techniques is described in the Returning Multiple Items question. Also, see the Microsoft SAFEARAY.EXE example. In our case above, we don't have a homogeneous group of types so we must do something else.

2) The general solution for handling data types that are not supported by automation is to "wrap" the object in an automation wrapper. In other words, create an automation enabled C++ object that houses the data. Then expose methods and properties that manipulate that contents of the structure (within the C++ object) as automation intrinsic types. Interaction with the data will then be through an IDispatch pointer to the encapsulated object. The main drawback to this approach is that you must make an automation call to retrieve and set the values of the structure, which can be slow. Here's an example of how it would look in Visual Basic.

'VB Client
Dim objServer as Object
Dim objMyType as Object
' Create an instance of the C++ server
set objServer = CreateObject( "The.Server")

' Create an instance of the automation wrapped structure
set objMyType = objServer.NewMyType

objMyType.Size = 100;
objMyType.Name = "Steve Levine"
objMyType.Balance = 1455.12

' Now tell the server to do something with the structure
objServer.DoSomething( objMyType )

The structure instance will actually reside within the server housing. This may be either an in-process (DLL) or local-server (EXE). Using this method you get all of the features of automation. Cross process marshaling, run-time resolution of methods, etc. Typically, your instance data will be organized in a hierarchical manner, which makes it easier for the client to manipulate.

3) If both sides "understand" the structure, you can serialize the data into a SafeArray as a VT_UI type. This will typically require C++ or some other non-VB development. The problem with this approach is that you're losing the real benefit of OLE, cross-language support. If you're going to lose this, you might as well choose option 5 below.

4) Write a custom marshaller for your data types. This provides the ability to pass non-automation types across process boundaries. This apparently isn't that hard, though I've not done it. Also, it still does not provide cross-language (e.g., VC++ to VB) support.

5) If both the client and server are executing in the same process space, a kludge solution, similar to 3 above, is to pass a pointer to the structure by casting it to a known automation type (e.g., IUnknown). The receiver, then, must cast it back into the "known" type. This requires that both the client and server cooperate and that they both "understand" the data types within the structure, typically only a C/C++ solution. This solution basically provides you with nothing but reduced performance. If it only works with "like" partners (i.e., they both have to be C++), then just use C++bindings.

A related solution to this is to use the CCmdTarget::FromIDispatch method. If both client and server are VC++ applications. The FromIDispatch method allows the client to retrieve the C++ object from its associated dispatch.

Update - The June, 1996 issue of Microsoft Systems Journal has a good article, Don Box's OLE Q&A column, on this subject. It describes the techniques in good detail, and he also discusses the issues in the context of using dual interfaces.

Back to top


How do I expose a complex object, like a collection, from my automation server?

Original Question:
How would I implement something like this in an OLE Control in C++?

   Dim obj as Object
   Set obj = MyControl.MyCollection.Add( "Item1", Item1 Value" )
   Text1 = obj.Value
   ' Iterate over the collection
   Dim objItem as Object
   For each objItem in MyControl.MyCollection
        List1.AddItem objItem.Value
   Next

I've recently done something very similar to what you want to do, so I should be able to help. Implement your collection object as you typically would, with the associated Enum class, etc. Add a property to your control that returns a dispatch. Name it appropriately, say something like 'MyCollection'. When your control is created, you will create an instance and assign it to a member of your control's class. E.g.

  m_pCollection = new CMyCollection;
  ...
  LPDISPATCH CMyControl::GetMyCollection()
  {
     return m_pCollection->GetIDispatch( FALSE );
  }

This will allow the Visual Basic syntax shown below

  Dim obj as Object
  Set obj = MyControl.MyCollection

Now we need to 'Add' items to the collection. Here's some code for your 'Add' method. Of course, it is implemented within the CMyCollection class.

  LPDISPATCH CMyCollection::Add( short sIndex, LPCSTR szName,... )
  {
    // Create an initialize an Item instance
    CMyItem* pItem = new CMyItem( szName,... );

    // Add it to the collection using your favorite method
    m_pCollection->m_lstItems->AddTail( pItem )

    // We are managing the memory, so make sure that we
    // AddRef it before giving it to VB
    return pItem->GetIDispatch( TRUE );   
  }

You are actually creating the storage for the item, and you will be responsible for its management. The 'Add' method will allow Visual Basic syntax like this:

  Dim objItem as Object
  Set objItem = MyControl.MyCollection.Add( 1, "Testing", "Hey" )

You can get real creative and allow various parameter types, etc. on the Add method by accepting them as VARIANTs. This way you could do something like this instead:

  Dim objItem as Object
  Set objItem = MyControl.MyCollection.New()
  objItem.Name = "Testing" 
  objItem.Value = "Hey" 
  MyControl.MyCollection.Add( objItem )

The same Add method shown above can support this overloading. The actual code would check the variant type and then add the item to the collection with the appropriate technique.

You could also return the dispatch of the collection instead of the item from the Add method. Once you get the hang of this stuff, you realize just how cool it is. The more difficult aspect of this, at least when I first started working with them, is the creation of the OLE Collection classes.

Back to top


I'm using automation with 16-bit VC++, but I forgot how to build the wrapper class?

Here's the original question:
I added a CCmdTarget-derived class to an app using the VC++1.52b class wizard. I can automate it fine from a simple VB app. How in the hell do I get the client application's class wrappers from the newly automated application? I obviously have the header file, but how do I get those magic InvokeHelper offset things in my implementation file (.cpp)?

Answer:
When you add automation to a project in VC++ 1.52, it doesn't automatically build the .ODL file for you. And you must have an TLB file to create the "helper" class that you need, i.e. the InvokeHelper wrappers, etc.

So, you need to create a ODL file. I do it quickly by using VC++ 4.x using "Add Class" from ClassWizard. I build the initial CPP and H files and take the ODL file over and use it in the 16-bit environ. You just need the skeleton of the ODL. There is an undocumented feature of VC++ 1.52. If there is an ODL file in the directory with the name of the project, ClassWizard will write the property and method definitions out for you. You just have to create the file initially. Sure, sometimes it screws up, but hey, it's undocumented.

Anyway, once you get the ODL file build, use the MkTypLib command from the tools menu and you're all set. From your other C++ project use the "Import TypLibrary" button on ClassWizard. This will create your wrapper class. From that point, everything should be pretty easy.

Back to top


How do I create an automation server that allows multiple client connections?

Here's the original question:
I have the following problem: I created a VC++ OLE Automation server that exposes a certain foo class. I would like that each time an OLE Automation client creates a foo object, that this object be instantiated in the SAME instance of the OLE Automation server.

In practice, I would like to start an application (my OLE Automation server) and that each time a client creates a foo object, the object is "provided" by this running instance of the server. This would allow the running server to (for example) keep a list of all foo objects and would allow some "interprocess" communication between all OLE clients with the server being the central point.

Answer:
What you're describing isn't difficult as long as the automation server is implemented as a local server (EXE). If this is the case, then flag the server as multi-use and all clients will use the same instance of the server. If, on the other hand, your server is in-process, then you've got a much more difficult problem. In-process servers are DLLs and operate in the clients address space (Win32) so you will always get another instance of the server. If you're using MFC, the multiuse flag is set during the m_server.ConnectTemplate call in InitInstance of the CWinApp derived class. The default is for MDI apps to be multi-instance and for SDI apps to be single-instance. The terms single and multi-instance are confusing, but they described the behavior from the server's perspective. So in your case you want a multi-instance server.

Response:
OK, but what if his server isn't document-based, and therefore he doesn't personally call ConnectTemplate? I'm looking into this one myself, and haven't yet come to the definitive answer. Sounds like a darned interesting discussion topic though.

Answer:
If your class isn't document-based, which I imagine is the typical case, the COleObjectFactory class handles the multi-instance stuff. The default behavior is to create the class so that it allows multiple instances of the object within the housing (EXE). If you don't want this behavior, you must create a new IMPLEMENT_OLECREATE macro, with a different name of course, and set the third parameter to the COleObjectFactory constructor to TRUE instead of FALSE.

For additional details check out the above macro definition and the members of COleObjectFactory, particularly the constructor.

Back to top


Can I define an OCX property of type "object" and have VB reference its properties? or
How do I implement multiple dot (".") operators with OLE Automation objects?

Here's the original question:
Question: Can I define an OCX property of type "object" and have VB reference its properties?

Desired code in VB:

Ocx1.Obj1.Property1 = "foo"
Ocx1.Obj2.Property1 = "bar"
Ocx1.Obj2.PropertyArray(1,2) = 42

The OCX property is an object which has its own properties that VB can reference.

Answer:
Sure you can do this. The ability to build a hierarchy of objects is one of the powerful features of OLE Automation. What you have to do to provides this "multiple-dot operator" functionality, is to build independent automation objects. Your control starts out with a main automation interface and from there you add "stacked" objects as you describe above. The trick is to add a property that returns an LPDISPATCH, which is a pointer to the instance of an object. An indirect pointer, of course, through OLE's IDispatch interface. Here's a simple example.

Say we want to add an "Range" object to our control's Dispatch. From Visual Basic we would use it like this:

OCX1.Range.Low = 100
OCX1.Range.High = 200

First we need to create a "Range" OLE automation object. This is easy to do using ClassWizard's "Add Class" button. It will derive from CCmdTarget and will support "Automation." It does not, however, need to be "OLE Creatable." We'll create the instance ourselves, by containing it within our control class. Anyway, assume we've made the above class and called it CRange. Using ClassWizard we also need to add our Low and High properties to our new CRange class.

Next, we need to add a property to our control called "Range" that is of type LPDISPATCH. Use the Get/Set technique and blank out the "Set" method. We should only "Get" the value of the property.

Next, add a member to the control class, something like m_Range. This is simply an embedded instance of our CRange class that we built above. The example control that demonstrates this technique declares it like this:

class CDotCtrl : public COleControl
{
   DECLARE_DYNCREATE(CDotCtrl)

protected:
   CRange   m_Range;

// Constructor
public:
   CDotCtrl();
...
};

If you want the behavior to change based on some value during creation of the control, you can also dynamically create this instance. Methods that return objects dynamically create instances of automation-enabled objects and return a pointer to their dispatches, but this is another topic. One other important point is to ensure that the constructor and destructor for the CRange class are public. ClassWizard will make the destructor protected by default and you will need to change this.

Anyway, after completing the above, the code for the GetRange method would look something like this:

LPDISPATCH CDotCtrl::GetRange()
{
   return m_Range.GetIDispatch( TRUE );
}

The parameter tells the method whether or not to AddRef the IDispatch being returned. Since we're controlling the lifetime of the object, it doesn't really matter. We call it with TRUE though, to make sure that when VB releases it, it won't be automatically destructed.

That's all there is to it. Easy huh? Things do get more complicated when working with large hierarchies and when receiving and returning IDispatch pointers through VB method calls, etc.

I've made a simple control that demonstrates this technique. Download DOT.ZIP.

Back to top


Typical Disclaimer: This FAQ is copyright (c) 1996 by Tom Armstrong (toma@sky.net). You have the right to copy and distribute this file in any way you see fit as long as this paragraph is included with the distribution. No warranties or claims are made as to the validity of the information contained herein. So use it at your own risk.


Send mail to Tom - toma@sky.net