- And today, on “Why Smart Documents Are Giving Me Ulcers”: ActiveX controls!
The concept is simple – I want to have an ActiveX control on my Smart Document that can affect the Smart Doc it it initialized from.
Assuming we have the ActiveX linked to an XML element on the document, we can get a handle to it by iterating the SmartTagActions collection on the element, and then attaching an event handler to an event on it, or simply reading a property on it. All would be well.
But unfortunately, as we saw a few posts ago (here), I can’t do that if the ActiveX is linked to the entire document. No way to get a handle, and thus no way to get data back from the ActiveX. Here are some relevant points:
- You can pass data TO the ActiveX using the ActiveXPropBag object from the PopulateActiveXControl event. The problem is that we can only pass strings here.
- There’s no simple way to pass data BACK from the ActiveX to the Smart Document.
In my solution, I have an ActiveX TabStrip that controls what is displayed in the Smart Document. I can pass data TO the ActiveX telling it how many tabs should show and what their captions are, but it’s not simple to get that data back to the Smart Document to refresh the display.
There are two problems involved:
- Having the Smart Document know which tab was chosen, and
- Knowing WHEN the tab was changed so we can refresh the display.
First Try – Remoting
I created a managed WinForms UserControl and wrapped it up as an ActiveX, then added remoting logic in the constructor that exposed the specific instance (using RemotingServices.Marshal()) as a TCP remoting listener. My Smart Document then attempted to get a handle to to object via remoting from the OnPaneUpdateComplete method and call methods such as AddTabs or attach to OnClick events on the remote object.
- The OnPaneUpdateComplete method would run twice, once before the ActiveX had configured itself, one after. I had to add try/catch logic for standard operation, which is bad and slow.
- Work with a remote object is slow. It’s silly that I have to pass through a TCP channel to get to an object in the SAME PROCESS as me, even if it is in a different AppDomain.
- The SDK recommends not to use Managed controls wrapped in ActiveX, and I can see why – I had problems with Events on the control not firing (at all, not just via remoting) and every time I tried to set a property relating to the control’s display – visibility, # of tabs, etc – Word would simply hang on an internal Control.ActiveXImpl.GetAmbientProperty() method, or some such.
In short, this approach failed miserably. Time for a second try:
Second Try – VB6
Ahhh. It’s been years.
So I open a new control in VB6 and put a TabControl in it. I initialize the TabControl with the data from the property bag, so far so good. I still have the two goals I mentioned above – refresh the smart doc and pass data back. The solution for both was similar:
- Send data back by saving in in the document’s CustomProperty collection, and
- Refresh the data by directly calling the documents’s SmartDocument.RefreshPane() method.
Very nice, but from my ActiveX control, I did not have a handle to my document.
Getting a Document Pointer – First Try
“When in COM“, as the saying (loosely) goes, right? How did we get an instance of the currently running Word instance in COM? GetObject!
So I use GetObject(””, “Word.Application”) to get the running instance and all is well, until I try to have two Word instance open at the same time.
It seems that VB’s GetObject() (as well as the CLR’s Marshal.GetActiveObject()) go over the ROT (Running Objects Table) and retrieve the first handle matching the ProgID given. If I had an instance of Winword loaded BEFORE the one I want, I’d get that instead. What to do, what to do?
Fortunately, some nice people on an internal alias had a solution for me:
Getting a Document Pointer – Second Try
When I’m writing my C# Smart Document code, I DO have a reference to the document, right? Right. And this pointer, this Word.Document object, is a COM object and thus implements IUnknown, right? Right.
So we pull up our sleeves to do some interop:
IntPtr docPointer = Marshal.GetIUnknownForObject(m_Document);
m_DocumentPointerForActiveX = docPointer.ToString();
This will give us a string containing a number that’s actually a pointer to the IUnknown interface of the document in question.
This string we can now pass as part of our ActiveXPropBag to the control, where it will be decoded back to a document pointer, and all will be well.
Unfortunately, I couldn’t find any way to do this from VB6. No pointer handlers, and all the COM in VB is under the hood, not for the likes of us to play with. So I wrote a small C# component called WordHelper that uses the reverse function to get a document pointer – and wrapped that up in COM:
public Document GetDocumentByPointer(int Pointer)
IntPtr docPtr = (IntPtr)Pointer;
Now my VB6 code recieves a string containing a document pointer, converts it to Integer, passes it to the WordHelper and receives a Word.Document object, and from there on it’s (relatively) clear sailing.