Invoking JavaScript Callbacks Using COM Automation



April 01, 2001
URL:http://www.drdobbs.com/web-development/invoking-javascript-callbacks-using-com/184416311

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 element’s 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 event’s 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:

  1. 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.
  2. 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.
  3. 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 element’s SRC attribute. The other methods require that at least some script be present in the HTML file itself. Thus, it allows better script reuse.
  4. 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 can’t use the FOR ... EVENT method.

The fourth reason is especially important, and indeed MSXML, Microsoft’s 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, doesn’t say much, since all COM Automation values can be passed in VARIANTs. (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 aren’t 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 wasn’t 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 object’s member function being called. Usually, this value is determined using IDispatch::GetIDsOfNames(), which maps a member, specified by name, to the appropriate DISPID. I couldn’t 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 object’s 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 month’s code archive to get the complete source for the component’s 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 component’s 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 article’s 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 haven’t 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 event’s 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

April 2001/Invoking JavaScript Callbacks Using COM Automation/Figure 1

Figure 1: Three styles of JavaScript event handlers

Figure 1a: Event handler as inline attribute value

<HTML>
...
    <IMG SRC="image.gif" ONCLICK="alert('clicked')">
...
</HTML>

Figure 1b: Using FOR. . .EVENT modifiers

<HTML>
...
    <SCRIPT FOR="image" EVENT="onclick">
        alert("clicked");
    </SCRIPT>
    <IMG ID="image" SRC="image.gif">
...
</HTML>

Figure 1c: Setting an event handler by object assignment

<HTML>
...
    <IMG ID="image" SRC="image.gif">
    <SCRIPT>
        function Clicked()
        {
            alert("clicked");
        }
        image.onclick = Clicked;
    </SCRIPT>
...
</HTML>

Figure 2: A test page hosting an ATL control

<HTML>
<HEAD>
    <TITLE>ATL 3.0 test page for object ScriptCallback</TITLE>
    <SCRIPT>
// The callback function
//
function Func()
{
    // Alert on activation
    alert("hello");
}

// Connect the script callback
// Wait until page (and the control) are fully loaded
//
function Loaded()
{
    ScriptCallback.Callback = Func;
}
window.onload = Loaded;
    </SCRIPT>
</HEAD>
<BODY>
    <OBJECT ID="ScriptCallback" 
        CLASSID="CLSID:986FB4FE-75D8-4D1C-9D9C-A755F15C2C8C"
        HEIGHT=1 WIDTH=1>
    </OBJECT>
</BODY>
</HTML>

Figure 3: Code to activate the callback

STDMETHODIMP CScriptTip::put_Callback(VARIANT newVal)
{
    // Verify correct argument type
    if ( newVal.vt != VT_NULL && newVal.vt != VT_DISPATCH )
        return E_INVALIDARG;

    // Copy the reference value
    HRESULT hResult = m_vCallback.Copy(&newVal);
    if ( FAILED(hResult) )
        return hResult;

    // If set to null then we are done
    if ( m_vCallback.vt == VT_NULL )
        return S_FALSE;

    // If callback provided invoke it
    DISPPARAMS params = { NULL, NULL, 0, 0 };
    CComVariant vResult;
    m_vCallback.pdispVal->Invoke(DISPID_VALUE, IID_NULL, 
                                 LOCALE_USER_DEFAULT,
                                 DISPATCH_METHOD,
                                 ¶ms, &vResult,
                                 NULL, NULL);
    return S_OK;
}

Figure 4: Attaching a function to multiple objects

<HTML>
...
<SCRIPT FOR=window EVENT=onload>

function ShowImageSrc()
{
    alert(this.src);
}

// Make all images in the page show their 
// SRC when clicked
//
var images = document.images;
for ( var i = 0 ; i < images.length ; ++i )
    images[i].onclick = ShowImageSrc;

</SCRIPT>
...
</HTML>

Listing 1: scrclbck.cpp — Final version of Callback property

// ScrClbck.cpp : Implementation of CScriptCallback
#include "stdafx.h"
#include "SC.h"
#include "ScrClbck.h"

//////////////////////////////////////////////////////////////////////////
// CScriptCallback

STDMETHODIMP CScriptCallback::get_Callback(VARIANT* pVal)
{
    // Return copy of current value
    return VariantCopy(pVal, &m_vCallback);
}

STDMETHODIMP CScriptCallback::put_Callback(VARIANT newVal)
{
    // Verify correct argument type
    if ( newVal.vt != VT_NULL && newVal.vt != VT_DISPATCH )
        return E_INVALIDARG;

    // Copy the reference value
    HRESULT hResult = m_vCallback.Copy(&newVal);
    if ( FAILED(hResult) )
        return hResult;

    // If set to null then we are done
    if ( m_vCallback.vt == VT_NULL )
        return S_FALSE;


    // If callback provided invoke it
    DISPID dispidThis = DISPID_THIS;
    CComVariant vThis(static_cast<IDispatch*>(this));
    DISPPARAMS params = { &vThis, &dispidThis, 1, 1 };
    CComVariant vResult;
    m_vCallback.pdispVal->Invoke(DISPID_VALUE, IID_NULL, 
                                 LOCALE_USER_DEFAULT, DISPATCH_METHOD,
                                 ¶ms, &vResult,
                                 NULL, NULL);
    return S_OK;
}
//End of File

Listing 2: scrclbck.htm — Test page for final control

<HTML>
<HEAD>
        <TITLE>ATL 3.0 test page for object ScriptCallback</TITLE>
        <SCRIPT>
// The callback function
//
function Func()
{
        // Alert on activation and test context
        alert(this.Callback.toString());
}

// Connect the script callback
// Wait until page (and the control) are fully loaded
//
function Loaded()
{
        ScriptCallback.Callback = Func;
}
window.onload = Loaded;
        </SCRIPT>
</HEAD>
<BODY>
        <OBJECT ID="ScriptCallback" 
                CLASSID="CLSID:986FB4FE-75D8-4D1C-9D9C-A755F15C2C8C"
                HEIGHT=1 WIDTH=1>
        </OBJECT>
</BODY>
</HTML>

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.