IE add-on development: communicating with tabs across threads

As you are no doubt aware, Internet Explorer 7 uses the tab-based browser interface that was popularized by Opera. Each tab runs in its own thread, and additionally, the browser chrome (areas outside the tabs such as the address bar and search bar) runs on yet another thread.

This means that if you need to communicate across tabs, you are in fact communicating across threads and thus you need to take care. In Find As You Type this situation occurs because I want to capture the CTRL-F (or other, if changed by the user) shortcut key even when the input focus is outside the tabs, in the chrome. When the user presses CTRL-F in the chrome, I want to show the search bar in the currently active tab. This means I need to communicate between the chrome thread and the active tab's thread.

In the previous article, the method I described for doing this depended on sending window messages between the tabs. I was not satisfied with this solution as it depends on the window layout used by IE7 (which is not documented so may change in a future version of IE) and it required SetFocus which you're not supposed to use across threads.

Preferably, we'd want to talk to the active tab's IWebBrowser2 object directly. But IE is apartment-threaded and none of its API objects are thread-safe, which means you cannot just use the interfaces across threads. The normal method of dealing with this is the CoMarshalInterThreadInterfaceInStream method (named after a town in Wales, I'm sure). I could in theory call this method whenever the active tab changes. However, the chrome thread would need to call CoGetInterfaceAndReleaseStream to get the original interface, and that releases the stream. Since the user might press CTRL-F in the chrome multiple times without switching tabs, the chrome thread would need to cache the marshalled interface somehow, and that leads to all kinds of problems when the active tab changes later.

Fortunately, there is a simple solution available, called the global interface table. This object, which is exposed through the IGlobalInterfaceTable interface, allows you to register any interface, which can then be acessed from the table by any other apartment (thread).

To use this, I keep a global instance of the StdGlobalInterfaceTable COM object (this is the system-provided implementation of IGlobalInterfaceTable) available as a static member of the Browser Helper Object (BHO) class, which is called _tabInterfaceTable. In the IObjectWithSite::SetSite method for the BHO, I store the IWebBrowser2 interface in the global interface table, using the IGlobalInterfaceTable::RegisterInterfaceInGlobal method. This method returns a cookie which is stored in the _interfaceCookie member (which is initialized to 0) of the BHO class. Since SetSite is also called when the BHO detaches, we can use it to remove the interface from the table if it was previously registered as well. This is shown below.

STDMETHODIMP ExampleBrowserHelper::SetSite(IUnknown *punkSite)
{
    // If this isn't the first time SetSite is called, revoke the
    // interface from the previous call from the global interface
    // table.
    if( _interfaceCookie != 0 )
    {
        _tabInterfaceTable->RevokeInterfaceFromGlobal(_interfaceCookie);
        _interfaceCookie = 0;
    }

    // SetSite is called with NULL when the BHO is detaching.
    if( punkSite != NULL )
    {
        // Get a web browser object from the service provider for the site.
        IServiceProvider *serviceProvider;
        if( SUCCEEDED(punkSite->QueryInterface(IID_IServiceProvider, reinterpret_cast<void**>(&serviceProvider))) )
        {
            IWebBrowser2 *browser;
            if( SUCCEEDED(serviceProvider->QueryService(IID_IWebBrowserApp, IIS_IWebBrowser2, reinterpret_cast<void**>(&browser))) )
            {
                // Store the web browser interface in the global table.
                _tabInterfaceTable->RegisterInterfaceInGlobal(browser, IID_IWebBrowser2, &_interfaceCookie);
                browser->Release();
            }
            serviceProvider->Release();
        }
    }
}

Now we can access the web browser object for any tab from any thread. In our case however, we are interested in the active tab, so besides just registering the interface we also have to keep track of the active tab. To do this, we can use the DWebBrowserEvents2::WindowStateChanged event which was added to IE7 for just that purpose. To connect to this event, the BHO class registers an event sink object which implements IDispatch. This event sink object listens for the WindowStateChanged event and if it fires (with appropriate parameters to indicate the tab has been activated) it calls a method in the ExampleBrowserHelper class that updates some variable that indicates what the active tab is.

We need to take some care at this point. A single IE7 process may hold multiple windows, each with a number of tabs. If we just store the active tab in a global variable this information will be wrong when the user switches to another window in the same process. So instead of storing one active tab, we store the active tab per window, for which we use a static member _activeTabs of the type std::map<HWND, DWORD> which stores the interface cookie of the active tab for a given window. Because all this is multi-threaded we also need to protect accesses to this variable with a CRITICAL_SECTION which is called _tabLock. The method that stores the active tab then looks like this:

void ExampleBrowserHelper::SetActiveTab()
{
    EnterCriticalSection(&_tabLock);

    // Set the interface cookie for this tab as the active one for the top-level window.
    HWND hwnd;
    if( SUCCEEDED(_browser->get_HWND(reinterpret_cast<SHANDLE_PTR*>(&hwnd))) )
    {
        _activeTabs[hwnd] = _interfaceCookie;
    }

    LeaveCriticalSection(&_tabLock);
}

Now we can use this map in the global keyboard hook that was described in the previous article to find the interface cookie for the active tab (don't forget to use the critical section there too!) and retrieve the IWebBrowser2 interface from the global interface table and use that to communicate with the active tab instead of the cumbersome window messages.

As always, you can find a working implementation of all this in the Find As You Type source code available on this site. The code there looks a bit different than the samples in this article because it does a lot of other things as well and uses some utility classes. If you have trouble making sense of the Find As You Type source code, don't hesitate to leave a message here.

Categories: Programming
Posted on: 2007-10-17 06:50 UTC.

Comments

John Schroedl

2007-10-17 16:54 UTC

Excellent points to make -- I've enjoyed looking at the FAYT source as well. Thanks for sharing!

John

EricLaw [MSFT]

2007-11-09 20:48 UTC

Cool article, thanks for sharing!

Sandeep

2008-02-05 09:55 UTC

The FindAsYouType is an cool project which extensively spans multiple ideas. Kudos to you on that!!
I have made a simple toolbar (ATL project)which has a button on it.
It works fine with IE 6. Im having a problem coz of tabs in IE 7, in trying to find out which is the active tab.

You have spoken about "WindowStateChanged" event with DISPID DISPID_WINDOWSTATECHANGED.
I was nt able to implement this. Could you please tell me as to how I can add this event to my application and handle it?
I ve also downloaded Boost and MSXML6.
I'm still unable to debug the FindAsYouType sample project. I'm faced with errors like
OLECMDIDF_WINDOWSTATE_USERVISIBLE, DISPID_WINDOWSTATECHANGED... undefined....
Thank you very much...

Mudassar

2008-04-01 06:23 UTC

Currently, I am developing an add-on for IE 7. I have two problems:
1. I want to detect Tab Change (There is no event available). But After reading this article, I think it is possible.
2. second problem is that I want a single instance of my addon shared by each tab instead of having one instance for each tab. This is possible in Firefox but don't know how to do in IE 7

pragatheesh

2008-09-23 11:47 UTC

Hi,

It is very nice Article. It would be helpful if you can send me the same code in C#.

Thanks in Advance.

thirdpath

2009-08-07 04:03 UTC

Great article!
Have you research IE8? It seems many HWNDs, tabs in different process.
:)

utkal

2009-10-26 11:54 UTC

Gr8 article. But unfortunately the plugins developed for IE6/7 are not working with IE8 as IE8 runs in different processes for each browser tab. Do you have crack for IE8 too ?

Add comment

Comments are closed for this post. Sorry.

Latest posts

Categories

Archive

Syndication

RSS Subscribe