Calling into your BHO from a client script


BHO means “Browser Helper Object”, that’s an IE plugin that interacts with browser & user events.
Basically this is a COM component that implements IObjectWithSite and is registered under
“HKLM\Software\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects”


A typical BHO could be a pop-up blocker, customizing at client-side an HTML document.
Some of them are also doing some UI stuffs, like adding menu features or acting as a toolbar
Note however those toolbars & co. don’t always require to be a registered as BHOs.


For a step by step BHO go to Building Browser Helper Objects with Visual Studio 2005.
For a sample step by step toolbar, go to
that tutorial .


That being said, we might get a great user experience from IE by reacting to its application…
That could make inside IE a kind of “rich intranet application”, interactions between client side scripts, and extensions.


We can imagine different kind of interaction with that schema (non exhaustive list…):


·                  Calling a method exposed by your browser extension from JavaScript


·                  Calling JavaScript method/event from your browser extension


·                  Modify the HTML / JS code from your browser extension


This post will for now only speak about calling a BHO method from your scripting code, maybe in future posts I’ll describe other possible things with some samples.


To get an IE extensions that changes some user UI (menu items, mouse gesture, etc…), we can easily implement in our BHO IDocHostUIHandler and attach it to current document using ICustomDoc::SetUIHandler()


That trick also allows to “extend” the HTML external object by implementing some custom window.external.foo() methods.


When making that window.external call, the webbrowser control will call into IDocHostUIHandler::GetExternal() to get the UI handler IDispatch interface to invoke
This means you just need to implement it to send back your own interface to make it callable.


The problem I wanted to discuss in that post is that there is no existing design for chaining UIHandlers… Indeed UIHandlers exist to allow extending user interface within a custom application that hosts a WebBrowser control, but not for Internet Explorer itself that already exposes a UI…


IOW from a BHO you might not be able to add your own menu items, but only to replace the all menu.
That’s basically the same for HTML external object, IE also relies on that external object to implement some UI features through
IShellUIHelper & IShellUIHelper2 interfaces (like Favorites & find dialogs, or runonce and search providers with IE7).


That kind of problem you might have is described by Q330441 (PRB: ICustomDoc::SetUIHandler Causes Changes in Save As Dialog)


If you replace the UI Handler within IE by your BHO instance (in order to answer window.external() calls), you will break thereby several UI related features, that is a highly non desirable behavior.


In order to workaround that design issue, some people have implemented within their BHO a dispatch gateway towards builtin interfaces IShellUIHelper & IShellUIHelper2.
That was quite easy to do, just chaining within our BHO IDispatch methods GetIDsOfNames() & Invoke() to standard IDispatch shell interfaces and was working pretty well.
Unfortunately for some reasons I would need to discuss longer in a next post, that might not be working anymore as expected on IE7, getting instead an E_FAIL result.


The best way I could recommend to call your BHO from a script is to come back to a supported way…One of the easiest method is to create a simple ActiveX that is called from your script, and fires BHO methods. To do that, we basically only have to get the BHO instance from the ActiveX and to call its methods.


A good method is to rely on IE session to store a variant we’ll know about on each side (using IWebBrowser2::PutProperty() / IWebBrowser2::GetProperty()).


In the code bellow I register my BHO instance in session variable called “MyBHO_IDisp”. That allows my BHO to “register” for later use from ActiveX.


 


STDMETHODIMP CObjectBHO::SetSite(IUnknown* pUnkSite)


{


      


 


       // Registers the IE session to allow ActiveX to call into it


       hr = ReferenceMe(false);


       if (FAILED(hr))


             return S_FALSE;


      


}


 


 


HRESULT CObjectBHO::ReferenceMe(bool bRemove)


{


BSTR bstrThisKey = SysAllocString(L“MyBHO_IDisp”);


VARIANT vThis;


HRESULT hr = S_FALSE;


 


       if (!bRemove)


       {


             if (!m_spWebBrowserApp)


                goto Cleanup;


 


             // Save this to a variant that will be referenced in IE Session


             VariantInit(&vThis);


             vThis.vt = VT_DISPATCH;


             vThis.pdispVal = static_cast<IDispatch*>(this);


 


             // Add our this pointer to IE session by adding a named property


             if (FAILED( m_spWebBrowserApp->PutProperty(bstrThisKey, vThis) ))


                goto Cleanup;


       }


       else


       {


             VariantInit(&vThis);


             vThis.vt = VT_NULL;


 


             // Time to release, remove our reference


             if (FAILED( m_spWebBrowserApp->PutProperty(bstrThisKey, vThis) ))


                goto Cleanup;


       }


 


       hr = S_OK;


 


Cleanup:


VariantClear(&vThis);


SysFreeString(bstrThisKey);


 


       return hr;


}


 


 


My ActiveX object now only has to implement IObjectWithSiteImpl::SetSite() to save the client site instance, and to grab the BHO instance from its session properties (GrabBHOInstance())


 


// CMyClass


 


class ATL_NO_VTABLE CMyClass :


       public CComObjectRootEx<CComSingleThreadModel>,


       public CComControl<CMyClass>,


       public CComCoClass<CMyClass, &CLSID_MyClass>,


       public IObjectWithSiteImpl <CMyClass>,


       public IDispatchImpl<IMyClass, &IID_IMyClass, &LIBID_MySampleATLLib, /*wMajor =*/ 1, /*wMinor =*/ 0>


{


private:


       IDispatch *m_pBHODisp;


       CComPtr<IUnknown> _spUnkSite;


 


       HRESULT(GrabBHOInstance)(void);


 


public:


       CMyClass()


       {


       }


 


 


 


STDMETHODIMP CMyClass::SetSite(IUnknown* pUnkSite)


{


       HRESULT hr = S_FALSE;


 


       // Save our client site instance


       if (pUnkSite)


       {


             _spUnkSite = pUnkSite;


             hr = S_OK;


       }


 


       // Try to grab BHO


       GrabBHOInstance();


 


       return hr;


}


 


 


Here is the implementation for GrabBHOInstance, just getting property from IWebBrowser2::GetProperty()


 


 


HRESULT CMyClass::GrabBHOInstance()


{


IServiceProvider* pISP = NULL;


IWebBrowser2* pBrowser = NULL;


BSTR bstrBHOKey = SysAllocString(L“MyBHO_IDisp”);


VARIANT vBHO;


HRESULT hr = S_FALSE;


 


       VariantInit(&vBHO);


            


       // Get the IWebBrowser2 interface


       if (!_spUnkSite)


             goto Cleanup;


 


       if (FAILED(_spUnkSite->QueryInterface(IID_IServiceProvider, (void **)&pISP) ))


          goto Cleanup;


 


       if (FAILED( pISP->QueryService(IID_IWebBrowserApp,


   IID_IWebBrowser2, (void **)&pBrowser)))


             goto Cleanup;


 


       // Get the BHO instance pointer


       if (FAILED( pBrowser->GetProperty(bstrBHOKey, &vBHO) ))


          goto Cleanup;


 


       // Ensure it’s valid and reference count it


       if (vBHO.vt == VT_DISPATCH && vBHO.pdispVal != NULL)


       {


          m_pBHODisp = vBHO.pdispVal;


          m_pBHODisp->AddRef();


       }


 


       hr = S_OK;


 


Cleanup:


VariantClear(&vBHO);


SysFreeString(bstrBHOKey);


 


if (pBrowser != NULL)


{


pBrowser->Release();


pBrowser = NULL;


}


if (pISP != NULL)


{


pISP->Release();


pISP = NULL;


}


 


       return hr;


}


 


 


STDMETHODIMP CMyClass::get_IsBHOInstalled(VARIANT_BOOL* pVal)


{


       // Check if our BHO instance could be get from the SetSite() call


       if (m_pBHODisp)


             *pVal = true;


       else


             *pVal = false;


 


       return S_OK;


}


 


 


 


A simple ActiveX method now can call into our BHO


 


STDMETHODIMP CMyClass::SayHelloFromBHO(BSTR* pVal)


{


       CComVariant varResult;


       HRESULT hr = S_FALSE;


       DISPPARAMS params = { NULL, NULL, 0, 0 };


 


       // Ensure we have the BHO instance propertly got


       if (!m_pBHODisp)


             if (FAILED(GrabBHOInstance()))


                    return hr;


 


       // Forward to BHO instance if valid


       if (m_pBHODisp)


       {


             hr = m_pBHODisp->Invoke(DISP_MYBHOMETHOD, IID_NULL, LOCALE_USER_DEFAULT,


                    DISPATCH_METHOD, &params, &varResult, NULL, NULL);


                   


             // Return the BSTR we got.


             *pVal = SysAllocStringLen(varResult.bstrVal, SysStringLen(varResult.bstrVal));


       }


 


       return hr;


}


 


 


Bellow a sample of the Jscript needed to call your ActiveX, that will fire a method from your browser extension.


 


 


var myATL = new ActiveXObject(“MySampleATL.MyClass”);


 


if (myATL.IsBHOInstalled)


       alert (myATL. SayHelloFromBHO());


 


else


       alert (“BHO isn’t installed now !”);


 


window.external.AddFavorite(“http://blogs.msdn.com/nicd”, “Nico’s Blog”)


 


 


That’s here a quick & modest sample of what can be done in order to integrate your BHO within your client web application.


Maybe in later posts I’ll discuss other possible things with some samples if some people are interested.
I’m now going to holidays for some days, so apologizes in advance if my 3rd post takes some times
J



Nico


 

Comments (9)

  1. kirants says:

    I have followed the steps you mention, however, in my ActiveX class’ SetSite, the following line fails with E_NOINTERFACE

    pISP->QueryService(IID_IWebBrowserApp,   IID_IWebBrowser2, (void **)&pBrowser)

    Any ideas ?

    One thing I want to mention is that, the BHO class and the Activex class are implemented in the same DLL. Would that matter ?

    Also, I am using IE7 on XP

    Thanks,

    Kiran

  2. nicd says:

    Do you correctly derive from IObjectWithSiteImpl? Do _spUnkSite & pISP look valid?

    Are you using that ActiveX from a simple html page or within an important html site (with frames, etc..)

    You can send me offline your ActiveX code and i’ll take a look.

    Nicolas

  3. kirants says:

    Hi Nico,

    Thanks a lot for following up.

    This is something interesting I found.

    See below:

    1. I implemented an activex control class and a BHO class in the same DLL.

    2. I implemented IObjectWithSite SetSite and the Service provider query to get the IWebBrwoser2 interface in both BHO and Activex control class. The same code is used in both places like below:

    CComQIPtr<IServiceProvider> isp = pUnkSite;

    CComQIPtr<IWebBrowser2> iwb2;

    isp->QueryService(IID_IWebBrowserApp,IID_IWebBrowser2, (void**)&iwb2);

    3. I register this dll.

    4. I wrote a sample HTML with the following code:

    <HTML>

    <HEAD>

    </HEAD>

    <SCRIPT LANGUAGE="JavaScript">

    var oShell = new ActiveXObject("TestJavaActivex.JavaActivexClass");

    </SCRIPT>

    <BODY>

    </BODY>

    </HTML>

    5. I launch IE. The BHO is loaded and iwb2 is valid for the BHO class’ SetSite

    6. I browse and open the HTML file mentioned above. The activex control class object is instantiated and the SetSite for the control shows iwb2 as valid.

    All is fine. Now, I want to customize the right mouse menu for IE and so I add the following registry entries:

    [HKEY_CURRENT_USERSoftwareMicrosoftInternet ExplorerMenuExtCustomMenu]

    @="C:\tmp\MenuScript.html"

    "Contexts"=dword:00000001

    where menuscript.html is like below:

    <SCRIPT LANGUAGE="JavaScript">

    var oShell = new ActiveXObject("TestJavaActivex.JavaActivexClass");

    </SCRIPT>

    Note: This is no different from the HTML code earlier.

    7. I launch IE again. The BHO is loaded and iwb2 is valid for the BHO class’ SetSite

    8. I right click and choose ‘CustomMenu’ item. The activex control class object is instantiated and the SetSite for the control is called, however, now iwb2 is NULL.

    I can send you the code, but it is pretty much bare bones ATL project. If it helps in me sending the code your way, please let me know how I can send it offline.

    Thanks,

    Kiran

  4. kirants says:

    Nevermind, Nico.

    I figured it out. I read somewhere that when a custom menu script is invoked, a new instance of the MSHTML is used and hence the IWebBrowser comes as NULL. I tweaked the script to pass in the external.menuArguments to the ActiveX object and get the IWebBrowser2 interface from there and it is all fine.

    Thanks for your interest in this matter.

    – Kiran

  5. nicd says:

    Very good news Kian!

    Regards

    Nicolas

  6. kovacs says:

    Hello! I have a question. only this 2 class and the application run, or has other classes? if possible i want to see the source code.

    Thanks

  7. markvs says:

    Hello!

    I’m having trouble trying to implement the BHO part, it does register, but when I close a tab or window the IE throws an exception:

    Access violation reading location 0xdddddde5

    If I don’t call ReferenceMe this doesn’t happen. I’m using IE 8. Any ideas?

    Thanks

  8. Michael McCord says:

    I do not think you actually need to call VariantClear in ReferenceMe. The documentation for VariantClear seems to indicate that it takes appropriate action to clear the contents of memory pointed to by the variant or to release the COM object pointed to by the variant. IE appears to call VariantClear for each variant in it's session property bag when it kills off the session property bag. This means if you're calling VariantClear in reference me then IE is going to kill it too….probably the source of the last poster's access violation…

  9. c# using SpiceIE says:

    Does anyone have a working example of this in C# using SpiceIE ?