Tuesday, February 19, 2008

how to create an activex control that fires events to javascript (without using ATL)

Creating ActiveX Controls that Fire Events (without ATL)

I recently spent a fair bit of time at work figuring out how to write all the COM and OLE goo to make this work. There are lots of articles that tell you how to use ATL, but I am against ATL and I wanted to do it the old fashioned way. Here is a summary of what I learned.

The scenario was this: I wanted to have an ActiveX control that would download some file on a worker thread, then fire events that could be handled by JScript before, during and after the download. The general outline would look like this:

1. Page loads, control instantiated via object tag.
2. JScript calls attachEvent() to setup event handlers for the control.
3. JScript calls a method to start download.
4. Control spins up a worker thread which calls URLDownloadToFile().
5. The worker thread receives progress notifications via IBindStatusCallback(). Control fires events to JScript (from the worker thread) to inform JScript of progress.
6. Download completes, worker thread fires event to JScript.

I will call my control the Downloader and his CLSID is CLSID_DownloaderCtrl.

To make this happen your control must implement (and respond to in IUnknown::QueryInterface()):

1. IUnknown
2. IDispatch
3. IProvideClassInfo, IProvideClassInfo2
4. IObjectWithSite
5. IConnectionPointContainer
6. IDownloader (this is the dual interface for scripts to call methods the control exposes)
7. IObjectSafety
8. IServiceProvider (for URLDownloadToFile() to work properly)
9. IBindStatusCallback (optional -- only if you want download progess)

You will also need to implement IConnectionPoint, but do not repsond to it in QueryInterface. More on this later.

IUnknown

I assume you know how to implement IUnknown. Read Raymond's post on getting it wrong to make sure you know how to implement it.

Type Libraries

Before we can do much more we need a type library. To get this working, you have to create an .idl file that contains definitions. There are four important parts. Each one has its own GUID. They are specified in the .idl and MIDL will generate a header file and c file that defines them.

1. The outgoing event (disp)interface. DIID_DDownloaderEvents
2. The incoming (dual) interface. IID_IDownloader
3. The coclass goo. CLSID_DownloaderCtrl
4. The library. LIBID_Downloader

Your .idl should look something like this:

[
uuid(00000000-0000-0000-0000-000000000000),
version(1.0)
]
library Downloader
{
[
uuid(11111111-1111-1111-1111-111111111111),
hidden
]
dispinterface DDownloaderEvents
{
properties:
methods:
[id(DISPID_PROGRESS)] void Progress();
[id(DISPID_COMPLETE)] void Complete();
}

[
dual,
uuid(22222222-2222-2222-2222-222222222222)
]
interface IDownloader : IDispatch
{
[id(DISPID_DOWNLOAD)] HRESULT download(BSTR bstrFile);
}

[
uuid(33333333-3333-3333-3333-333333333333)
]
coclass DownloaderCtrl
{
[default] interface IDownloader;
[source, default] dispinterface DDownloaderEvents;
}
}



MIDL will generate a .tlb file from this. You must include this type library as a resource in your .dll. To do that, add a line like the following:

1 TYPELIB "downloader.tlb"

Now you can call LoadTypeLib() using your module's path to get the type library, which you will need to use in your IDispatch implementation.

IDispatch

I am going to assume you know mostly know how to implement this as well. It is well documented. The important thing is to make sure you expose your typelib correctly.

IProvideClassInfo, IProvideClassInfo2

These are pretty straight-forward. In GetGUID() return your outgoing event interface, DIID_DDownloaderEvents.

The only tricky part is in GetClassInfo. You should call LoadTypeLib() then call ITypeLib->GetTypeInfoOfGuid(). The question is, which GUID do you use? The correct answer is CLSID_DownloaderCtrl. This gets the type info of your coclass, which Internet Explorer can use to figure out what your outgoing event interface is.

IConnectionPointContainer

When you call attachEvent() in JScript, IE will ask for this interface to try to find a connection point for your outgoing event interface. You have to implement FindConnectionPoint(). I found EnumConnectionPoints() and to not be called by Internet Explorer. However, you may experience different results here. Set breakpoints and/or use asserts() to make sure anything you E_NOTIMPL isn't called, otherwise you may find yourself debugging into the wee hours.

IConnectionPoint

FindConnectionPoint() gives out a pointer to an IConnectionPoint, which should really be a different object than your IConnectionPointContainer. See the documentation. We don't respond to IConnectionPoint in QueryInterface, since the only allowed way of getting it is via FindConnectionPoint().

You have to implement Advise() and Unadvise(). The remaining methods were never called for my implementation. However, as above, your milage may vary. Everytime IE calls Advise(), it will pass you an IUnknown. QueryInterface() for IID_IDispatch and remember that pointer. Make sure you associate it with the cookie you give back (std::map is one option, if you go in for that sort of thing).

When you want to fire your event, simply call the Invoke() member of all the IDispatch pointers you are holding on to, passing whatever parameters you want and the DISPID of whatever event you want to send.

Important: Before you use this pointer, read the bit about marshalling below.

IObjectWithSite

This is simple as well. Make sure you respond to SetSite(NULL) by releasing all the pointers you acquired from your site. Also, this is your queue that your control is going away soon.

IServiceProvider

All you have to do here is QueryInterface your site for IServiceProvider and thunk the call to QueryService() through to your site's implementation. If you're using the Vista SDK, you can use IUnknown_QueryService().

If you don't implement this, URLDownloadToFile() may not be able to get access to certain security information and your life will be harder.

IObjectSafety

You should implement this to make instantiating your control easier and safer. Refer to the documentation and plentiful on-line examples.

A Word on Marshalling

My object operates on two threads--the IE Tab thread that it is created on, and a worker thread that does the heavy lifting. The point of using a worker thread is to not hang the UI while the download happens. This gives a nice experience, but makes implementation harder.

The one thing to remember, is all of IE's interaction with your object will happen on it's thread. You will receive IDispatch pointers on this thread. You cannot use these pointers in a different apartment (which means, you cannot use them on the worker thread).

In order to fire events from the worker thread, you must marshal the IDispatch pointers. You have two options for doing this:

1) Call CoMarshalInterThreadInterfaceInStream() on the IE thread, then CoGetInterfaceAndReleaseStream() on the worker thread. Do this for every IDispatch pointer and use the pointer returned on the worker thread.
2) Use the GIT (Global Interface Table). You're on your own with that one -- see the documentation.

If you try to use the pointer on the wrong thread, your Invoke() call will fail silently and the event will not be fired.

Setting up the JScript

1. Create your object using the object tag. You cannot use new ActiveXObject() because IE will not hook-up the events for you.
2. Give your object tag and ID, such as ID="downloader".
3. In the onLoad handler for the body element, call a function that uses the ID to attach the event handlers to events. E.g., downloader.attachEvent('Progress', onProgressEvent). Then simply implement the onProgressEvent() in JScript in the script section of your HTML.

Conclusion

Well, I hope that helps someone. If you have anything to add, please leave a comment.

6 comments:

Unknown said...

Thanks for the help by posting this link in msdn to answer my question.The point is that I have always worked on c# and its my first time on vc++ so I am just a beginner hence if you can provide the code (just an add function with calling sleep in it to delay its execution)and the javascript file I would be really greatful

Unknown said...

I have tried following your instructions but have failed - my javascript event doesn't get called :-(

If you have the source code for this sample, can you please make that available?

Many Thanks,
Burhan

Unknown said...

Nice article. Just a quick question for you - do you know of any way to enumerate/manipulate the individual event handlers of a given handler? (e.g. do you know mshtml stores the IDispatch pointers in the IConnectionPoint for a given event, such as onclick?)

jeffdav said...

Unfortunately the code I wrote is proprietary and owned by my company, so I cannot post it. Sorry.

jeffdav said...

Another thing I should have said in the original post: You can't marshal NULL. If you have to pass NULL for a parameter to a method that is being marshalled, you have to create a VT_EMPTY VARIANT or pass an empty BSTR or something.

jeffdav said...

lol. "queue" should be "cue" in the bit about SetSite(NULL).