The ability to connect script functions to events is one of the most powerful features of modern browsers. This feature makes it easy and intuitive for a web developer to create dynamic web pages that respond to user interactions. For example, to create an image roll-over effect, you simply connect the IMG elements mouse-over event to a function that changes its SRC attribute to an alternate URL and the mouse-out event to a function that restores the original value. Thus, very little programming is required to achieve this effect.
COM (formally ActiveX) controls that are embedded in a web page can also fire events that invoke script code. The documented method for accomplishing this is to use connection points, the standard COM Automation technique for a component to notify its container. Connection points, however, are only compatible with one out of the three possible methods for defining JavaScript event handlers. In this article, I will describe a method for invoking JavaScript event handlers as callbacks, which has several notable advantages over connection points.
Defining JavaScript Event Handlers
The Microsoft Internet Explorer browser provides three methods for defining JavaScript event handlers: you can inline the script code into the HTML as an attribute value (see Figure 1a); you can define a script block that uses the FOR ... EVENT modifiers (see Figure 1b); and you can programmatically assign a script handler to the event (see Figure 1c).
The second method, using the FOR ... EVENT syntax, uses connection points. Connection points are a standard, well-documented feature of COM Automation: a control defines a set of outbound dispinterfaces, assigning an implementation of IConnectionPoint
to each such interface. The control container, Internet Explorer in this case, connects an IDispatch
implementation to the appropriate point. To signal an event, the control calls IDispatch::Invoke()
through that connection. Internet Explorer then routes the call to the script handler block, based on the events source and name. You can generally ignore these details because most COM frameworks, such as VB, MFC, and ATL, provide intrinsic support for connection points and firing events through them.
For the controls I developed, I also wanted to support the other methods for defining JavaScript event handlers, but could find no documentation on how to accomplish this. I was especially interested in supporting the third method, programmatic connection, because it has several notable advantages over the other two:
- It allows dynamic attachment, detachment, and replacement of the script code associated with an event. This is possible because you can attach any JavaScript function to the event handler at any time in the script execution, even within the JavaScript callback itself. Detaching a callback is accomplished by setting its value to
null
. - It supports chaining: the ability to connect several handlers to a single event. When connecting a handler, you save the previous handler in a member. The new handler can then activate the original one before finishing.
- It facilitates the separation of script code from the HTML because both the callback and the code that attaches it can reside in a JavaScript file that is referenced from the HTML using the SCRIPT elements SRC attribute. The other methods require that at least some script be present in the HTML file itself. Thus, it allows better script reuse.
- Only objects that are embedded in the HTML using the OBJECT tag can invoke events using connection points. Objects that are created using the JavaScript new ActiveXObject syntax cannot do this, nor can a browser container that exposes services through the
window.external
property. As a result, such objects cant use the FOR ... EVENT method.
The fourth reason is especially important, and indeed MSXML, Microsofts XML parser, which is instantiated using new ActiveXObject
, also supports this method for event generation.
Obtaining a JavaScript Function Reference
Having found no relevant documentation, I set out to reverse engineer the method by which controls invoke JavaScript functions as callback. I began by examining the interfaces of objects that provide this functionality. The IHTMLElement
interface, which describes the functionality common to all HTML element objects, includes the following properties: onclick
, ondblclick
, onkeypress
, and other properties that match the events such an object can generate. The onclick
property, for example, is defined as follows:
HRESULT IHTMLElement::get_onclick(VARIANT *p); HRESULT IHTMLElement::put_onclick(VARIANT v);
This means that when a JavaScript function is assigned to the event, its reference is passed in as a VARIANT
. This, unfortunately, doesnt say much, since all COM Automation values can be passed in VARIANT
s. (Indeed, this is how dispinterfaces work.) What is important is the type of the value passed in the VARIANT
, as determined by the value stored in its vt
member. To determine this value, I created a simple ATL control with a dual interface that defined a single property:
[ object, uuid(E1835AAB-2FAC-4B7F-B01A-8FF1E019A409), dual, helpstring("IScriptCallback Interface"), pointer_default(unique) ] interface IScriptCallback : IDispatch { [propget, id(1), helpstring("Script callback reference")] HRESULT Callback([out, retval] VARIANT *pVal); [propput, id(1), helpstring("Script callback reference")] HRESULT Callback([in] VARIANT newVal); };
The ATL wizard was kind enough to create an HTML page that hosts this control.
I modified that page to assign a JavaScript function to the Callback
property (see Figure 2). I ran this project in
Visual Studio, with the browser as the process being debugged, placing a breakpoint
in the implementation of IScriptCallback::put_Callback()
, and then
looked at the value of its parameter. The type of the parameter turned out
to be VT_DISPATCH
, which was what I had expected.
The reason I expected this value is that in JavaScript everything (even a function) is an object. And, in the world of COM Automation, an object is represented by an IDispatch
. You may wonder why the properties that accept function references arent defined as taking an IDispatch
parameter explicitly. The reason is that event handlers are detached by assigning null
to the appropriate property. The Microsoft implementation of JavaScript uses a VARIANT
with a type of VT_NULL
to represent the null value.
Invoking the JavaScript Callback
Having determined that a JavaScript callback is represented by an IDispatch
, it still wasnt clear how to activate it. The key was obviously IDispatch::Invoke()
, whose purpose is to call code in the object that implements the interface. This method requires a DISPID, which is a numeric value that identifies the objects member function being called. Usually, this value is determined using IDispatch::GetIDsOfNames()
, which maps a member, specified by name, to the appropriate DISPID. I couldnt think of what name to use in this context, however. This led me to think that a fixed DISPID value is being used.
The COM and Windows header files define several fixed DISPID values. These are DISPIDs that, when supported, provide standard functionality. Assigning standard functions a fixed value makes the IDispatch
implementation simpler and more efficient. Fixed DISPIDs have negative values while the custom DISPIDs that are mapped to named methods have positive values. I looked at the various fixed DISPIDs and tested the likely candidates. It turned out that DISPID_VALUE
was the answer.
DISPID_VALUE
, which has the value of zero, identifies the default
member for the dispinterface, which is the method invoked if the object is
specified by itself without a property or method in the controller script.
The code that activates the callback appears in Figure
3. Note that the activation occurs right after the callback is assigned.
This is probably not a common scenario, but it demonstrates the method.
Providing the Appropriate Context
At first glance, it appears that I had found the solution I had sought. A
bit more testing showed that something was still missing. When JavaScript
code is activated to handle an event for an HTML element, it executes in the
context of the object that invoked it. That is, the JavaScript function is
activated as if it was a method of the object to which it is connected, and
that objects members can be accessed directly through this
reference.
This is more than just convenience for the script writer. It allows a single
script function to be attached to several objects simultaneously and execute
in the appropriate context for each one. See Figure 4 for an example of this feature.
When a control fires an event via a connection code, Internet Explorer automatically provides the appropriate context for the script block. This, unfortunately, is not the case for JavaScript callbacks invoked via the callback mechanism described above. Instead, the JavaScript code runs in the global context, which in the browser maps to the window
object. I wanted to execute the script code in the correct context in order to improve ease of use and to achieve constancy with the intrinsic browser objects.
While inspecting the various fixed DISPIDs
, I had noticed DISPID_THIS
, which has the value -613
. This DISPID
seemed appropriate, but the MSDN did not have anything to say about it. The only clue for its use came from a comment in dispex.idl
, which defines IDispatchEx
, an extension of the IDispatch
interface. The comment was for IDispatchEx::InvokeEx()
, an enhanced version of IDispatch::Invoke()
. Here is its text:
When DISPATCH_METHOD
is set in wFlags
, there may be a named parameter for the this value. The dispID will be DISPID_THIS
and it must be the first named parameter.
Named parameters are a special feature of both Invoke()
and InvokeEx()
. This feature provides support for programming languages, such as Visual Basic, that allow associating a value with a specific parameter explicitly by name. In the DISPPARAMS
structure, used by these methods to pass parameters, you specify the number of named parameters and the DISPID
values that identify them. The documentation states that no order is imposed on named parameters, but apparently if the first one is identified as DISPID_THIS
, its value provides the context.
Since the context describes the object that invokes the script callback,
I guessed that its value should be the IDispatch
for that object. I
modified the C++ code appropriately and also changed the JavaScript code to
verify that the correct context is indeed being set. This modification did
indeed work as desired; see scrclbck.cpp
(Listing
1) for the final version. scrclbck.htm
(Listing
2) shows the HTML page I used to test the control. You can download this
months code archive to get the complete source for the components
project.
Addendum
Unlike connection points, programmatic handler attachment requires script
access to the control. If you plan to use such a control in an unsafe environment,
such as the browser, you will get a pesky security warning each time the page
is loaded unless you mark the control as safe for scripting. You
can designate a control as safe for scripting by implementing IObjectSafety
or simply by adding the appropriate component category setting to the components
entry in the registry. This is what I did with the sample object, as shown
in scrclbck.rgs
(Listing 3). Please remember,
however, that before marking a control as safe you must verify that this is
indeed the case.
As this articles title suggests, when I researched this technique and began writing this article, I thought that programmatic activation technique was only relevant for JavaScript. During the writing I discovered, however, that it is also applicable to VBScript, version 5.0 and higher. So if you want to use that language for some reason, you can use GetRef()
to obtain a reference to a VBScript Sub
or Function
and pass it to the Callback
property.
Conclusion
While the method described above for invoking JavaScript event handlers as callbacks is not documented, I have tested it and found that it works on all versions of Internet Explorer since version 4.0. I, therefore, think that it is safe to assume that it will continue to work in future versions of the browser, at least until .NET comes along and makes everything irrelevant.
I have since used this method not only in controls hosted by the browser or in browser containers, but also by applications that host the script engine directly. I find that it is much more flexible than invoking a script function by name. Another potential use, albeit one that I havent tested yet, is to connect hosted COM controls to events generated by elements in an HTML page. A control can implement an IDispatch
that supports invocation using DISPID_VALUE
with no parameters and then pass that interface to the events property put method. The element would then call the component as if it were a script callback.
Dan Shappir holds a M.Sc. in Computer Science and has been a programmer for nine years. He is currently working for an Israeli computer firm developing Internet applications. Dan can be reached at [email protected] or www.math.tau.ac.il/~shappir.
Click here to download the zip folder containing shappir.zip