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, ¶ms, &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("https://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
Anonymous
May 14, 2007
The comment has been removedAnonymous
May 14, 2007
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. NicolasAnonymous
May 14, 2007
The comment has been removedAnonymous
May 15, 2007
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
Anonymous
May 15, 2007
Very good news Kian! Regards NicolasAnonymous
August 29, 2007
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. ThanksAnonymous
March 03, 2010
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? ThanksAnonymous
July 01, 2010
The comment has been removedAnonymous
November 07, 2010
Does anyone have a working example of this in C# using SpiceIE ?