11

I am trying to call my BHO method from the javascript. The problem is same as stated in the the following posts:

  1. Call BHO from Javascript function
  2. http://social.msdn.microsoft.com/Forums/en-US/ieextensiondevelopment/thread/91d4076e-4795-4d9e-9b07-5b9c9eca62fb/
  3. Calling C++ function from JavaScript script running in a web browser control

Third link is another SO post talking about it, but I did not understand the need and code. Also the shared working sample keeps crashing on windows 7 with ie 8 and windows vista with ie 7.

If it helps my BHO is written in C++ using ATL.

What I have tried:

I have written a very basic BHO and tried the approach as mentioned here by Igor Tandetnik. There is no exception generated but when I open the following html file in IE then it says object undefined.

<html>
    <head>
        <script language='javascript'>
            function call_external(){
                try{
                alert(window.external.TestScript);
                //JQueryTest.HelloJquery('a');
                }catch(err){
                    alert(err.description );
                }
            }
        </script>
    </head>
    <body id='bodyid' onload="call_external();">
        <center><div><span>Hello jQuery!!</span></div></center>
    </boay>
</html>

Question:

  1. Please clarify whether is it possible to expose and call BHO method from javascript or do i have to expose it using an activex (as answered by jeffdav in [2] )? If yes then how to do it.
  2. Basically i want to extend the window.external but the way shown in the above link [2] uses var x = new ActiveXObject("MySampleATL.MyClass");; Is both the calling conventions are same or different?

Note:

  1. There is a related post on SO which gives hint that it is possible through inserting this [id(1), helpstring("method DoSomething")] HRESULT DoSomething(); in the BHO IDL file. I am not sure how it was done and couldn't find any supporting resource through google.
  2. I am aware of this post calling-into-your-bho-from-a-client-script, but haven't tried it as it is solving the problem using ActiveX.
  3. My reason to avoid ActiveX is primarily due to the security restrictions.

Edit 1


It seems there is a way to extend the window.external. Check this. Specially the section titled IDocHostUIHandler::GetExternal: Extending the DOM.Now assuming our IDispatch interface is on the same object that implements IDocHostUIHandler. Then we can do something like this:
HRESULT CBrowserHost::GetExternal(IDispatch **ppDispatch) 
{
    *ppDispatch = this;
    return S_OK;
}

The problem with this approach is that it won't append to the existing windows methods, but rather replace them. Please tell if I am wrong.

Edit 2


The BHO Class:
class ATL_NO_VTABLE CTestScript :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CTestScript, &CLSID_TestScript>,
    public IObjectWithSiteImpl<CTestScript>,
    public IDispatchImpl<ITestScript, &IID_ITestScript, &LIBID_TestBHOLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
    public IDispEventImpl<1, CTestScript, &DIID_DWebBrowserEvents2, &LIBID_SHDocVw, 1, 1>
{
public:
    CTestScript()
    {
    }

DECLARE_REGISTRY_RESOURCEID(IDR_TESTSCRIPT)

DECLARE_NOT_AGGREGATABLE(CTestScript)

BEGIN_COM_MAP(CTestScript)
    COM_INTERFACE_ENTRY(ITestScript)
    COM_INTERFACE_ENTRY(IDispatch)
    COM_INTERFACE_ENTRY(IObjectWithSite)
END_COM_MAP()



    DECLARE_PROTECT_FINAL_CONSTRUCT()

    HRESULT FinalConstruct()
    {
        return S_OK;
    }

    void FinalRelease()
    {
    }

public:
    BEGIN_SINK_MAP(CTestScript)
        SINK_ENTRY_EX(1, DIID_DWebBrowserEvents2, DISPID_DOCUMENTCOMPLETE, OnDocumentComplete)
        //SINK_ENTRY_EX(1, DIID_DWebBrowserEvents2, DISPID_NAVIGATECOMPLETE2, OnNavigationComplete)
    END_SINK_MAP()

    void STDMETHODCALLTYPE OnDocumentComplete(IDispatch *pDisp, VARIANT *pvarURL);
    //void STDMETHODCALLTYPE OnNavigationComplete(IDispatch *pDisp, VARIANT *pvarURL);

    STDMETHOD(SetSite)(IUnknown *pUnkSite);

    HRESULT STDMETHODCALLTYPE DoSomething(){
        ::MessageBox(NULL, L"Hello", L"World", MB_OK);
        return S_OK;
    }
public:

//private:
    // InstallBHOMethod();

private:
    CComPtr<IWebBrowser2>  m_spWebBrowser;
    BOOL m_fAdvised;
};

// TestScript.cpp : Implementation of CTestScript

#include "stdafx.h"
#include "TestScript.h"


// CTestScript

STDMETHODIMP CTestScript::SetSite(IUnknown* pUnkSite)
{
    if (pUnkSite != NULL)
    {
        HRESULT hr = pUnkSite->QueryInterface(IID_IWebBrowser2, (void **)&m_spWebBrowser);
        if (SUCCEEDED(hr))
        {
            hr = DispEventAdvise(m_spWebBrowser);
            if (SUCCEEDED(hr))
            {
                m_fAdvised = TRUE;              
            }
        }
    }else
    {
        if (m_fAdvised)
        {
            DispEventUnadvise(m_spWebBrowser);
            m_fAdvised = FALSE;
        }
        m_spWebBrowser.Release();
    }
    return IObjectWithSiteImpl<CTestScript>::SetSite(pUnkSite);
}

void STDMETHODCALLTYPE CTestScript::OnDocumentComplete(IDispatch *pDisp, VARIANT *pvarURL)
{
        CComPtr<IDispatch> dispDoc;
        CComPtr<IHTMLDocument2> ifDoc;
        CComPtr<IHTMLWindow2> ifWnd;
        CComPtr<IDispatchEx> dispxWnd;

        HRESULT hr = m_spWebBrowser->get_Document( &dispDoc );
        hr = dispDoc.QueryInterface( &ifDoc );      
        hr = ifDoc->get_parentWindow( &ifWnd );
        hr = ifWnd.QueryInterface( &dispxWnd );

        // now ... be careful. Do exactly as described here. Very easy to make mistakes
        CComBSTR propName( L"myBho" );
        DISPID dispid;
        hr = dispxWnd->GetDispID( propName, fdexNameEnsure, &dispid );

        CComVariant varMyBho( (IDispatch*)this );
        DISPPARAMS params;
        params.cArgs = 1;
        params.cNamedArgs = 0;
        params.rgvarg = &varMyBho;            
        params.rgdispidNamedArgs = NULL;
        hr = dispxWnd->Invoke( dispid, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_PROPERTYPUT,
            &params, NULL, NULL, NULL );

}

The Javascript:

<script language='javascript'>
            function call_external(){
                try{
                alert(window.ITestScript);
                }catch(err){
                    alert(err.description );
                }
            }
        </script>

Edit 3


After spending three days on this, I think I should take the ActiveX path. Writing a basic activex is just way to easy, written one and tested on all major IE releases. I am leaving this question as open, please see comments in Uri's answer (many thanks to him). I have tried most of his suggestions (except for 4 and 5). I will also suggest you to see the MSDN IDispatcEx sample. If you find a solution then please post, if I find a solution then I will definitely update here.

Edit 4


See my last comment in URI's post. Issue Resolved.
Favonius
  • 13,959
  • 3
  • 55
  • 95

1 Answers1

7

Igor Tandetnik's method is the correct approach. The problem with the post is that the sample code (at least on the few pages I spotted it) is that it wasn't complete. I had many trials and errors until I got it working. Here is a good chunk of my code that does the trick:

Say you have a class CMyBho, and you want to expose IMyBho automation object for Java scripts

Class definition:
You derive from the standard CComObjectRootEx, and CComCoClass to make it 'co creatble'. You have IObjectWithSiteImpl (reuse the m_spUnkSite implemented by this base class). IDispatchImpl implements your automation object, and IDispatchEventImpl is the sink to get notifications from the browser:

class ATL_NO_VTABLE CMyBho
    : public CComObjectRootEx<CComSingleThreadModel>
    , public CComCoClass<CMyBho, &CLSID_MyBho>
    , public IObjectWithSiteImpl<CMyBho>
    , public IDispatchImpl<IMyBho, &IID_IMyBho, &LIBID_MyBhoLib, 1, 0>
    , IDispatchEventImpl<1, CMyBho, &DIID_DWebBrowserEvents2, &LIBID_SHDocVw, 1, 1>
{
    ...

public:
    BEGIN_COM_MAP(CMyBho)
        COM_INTERFACE_ENTRY(IMyBho)
        COM_INTERFACE_ENTRY(IDispatch)
        COM_INTERFACE_ENTRY(IObjectWithSite)
    END_COM_MAP()

    ...

    BEGIN_SINK_MAP(CMyBho)
        SINK_ENTRY_EX( 1, DIID_DWebBrowserEvents2, DISPID_DOCUMENTCOMPLETE, OnDocComplete )
    END_SINK_MAP()

    ...

private:
    CComPtr<IWebBrowser2> m_ifbrz;          // pointer to the hosting browser

}

Next, the SetSite method, where you register to get the notification. Don't forget to call the base class.

STDMETHODIMP CMyBho::SetSite( IUnknown* unkSite )
{
    ...
    hr = IObjectWithSiteImpl::SetSite( unkSite );
    if( unkSite ) {
        ...
        // advise to browser event.
        CComPtr<IServiceProvider> ifsp;
        hr = m_spUnkSite.QueryInterface( &ifsp );
        hr = ifsp->QueryService( SID_SwebBrowserApp, IID_IWebBrowser2, &m_ifbrz );
        hr = DispEventAdvise( m_ifbrz );
    }
    else {
        // release various resources (m_ifbrz will be released automatically by its dtor)
        ...
    }
...
}

When document load is complete, this function will be called:

void STDMETHODCALLTYPE CMyBho::onDocComplete( IDispatch* dispBrz, VARIANT* pvarUrl )
{
    CComPtr<IDispatch> dispDoc;
    CComPtr<IHTMLDocument2> ifDoc;
    CComPtr<IHTMLWindow2> ifWnd;
    CComPtr<IDispatchEx> dispxWnd;

    hr = m_ifbrz->get_Document( &dispDoc );
    hr = dispDoc.QueryInterface( &ifDoc );      
    hr = ifDoc->get_parentWindow( &ifWnd );
    hr = ifWnd.QueryInterface( &dispxWnd );

    // now ... be careful. Do exactly as described here. Very easy to make mistakes
    CComBSTR propName( L"myBho" );
    DISPID dispid;
    hr = dispxWnd->GetDispID( propName, fdexNameEnsure, &dispid );

    CComVariant varMyBho( (IDispatch*)this );
    DISPPARAMS params;
    params.cArgs = 1;
    params.cNamedArgs = 0;
    params.rgvarg = &varMyBho;            
    params.rgdispidNamedArgs = NULL;
    hr = dispxWnd->Invoke( dispid, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_PROPERTYPUTREF,
                           &params, NULL, NULL, NULL );
}

As for your other questions:

  • Evidently, my answer implies that you can make an automation object available for scripting from your BHO. It is also possible your object will be instantiated with the new ActiveXObject. In that case don't forget to tell IE your object is safe for scripting (side note: make your BHO safe for scripting. make sure malicious web site won't be able to exploit your BHO).

  • I think that window.myBho is a better place than window.external.myBho. Semantically, 'external' is when the mshtml browser control is hosted within another application.

Hope this helped.

Piotr Niewinski
  • 1,298
  • 2
  • 15
  • 27
Uri London
  • 10,631
  • 5
  • 51
  • 81
  • Thanks for your reply. I tried your suggestion but when I am accessing the bho object in my javascript then it is giving me `object undefined`. Please see my updated answer, I have included the BHO code and the corresponding javascript. – Favonius Jan 11 '12 at 14:33
  • I have changed these two statements `params.rgvarg = &varTanduBar; params.rgdispidNamedArgs = null;` to this `params.rgvarg = &varMyBho;params.rgdispidNamedArgs = NULL;`. Otherwise I have used the same code. – Favonius Jan 11 '12 at 14:36
  • Also, I have tried `DISPATCH_PROPERTYPUTREF` and `DISPATCH_PROPERTYPUT`. But in this case both are not working. – Favonius Jan 11 '12 at 14:44
  • Yeah, 'TanduBar' is taken from my own code. Sorry. I think you have a timing issue. I suspect that when the JavaScript tries to access the window.myBho, the entry is not there yet. Please try the following. Change your HTML to react to button click, way after the page (document) is loaded. Add the following button: If that works for you, then we need to set up the window OM sooner (if you need the object there sooner). Let me know if the button works, and I'll look for a better event for you. – Uri London Jan 11 '12 at 14:44
  • Thanks. I tried the button approach but still I am getting `window.ITestScript is null or not an object`. I hope I am referring the BHO correctly in my JS. – Favonius Jan 11 '12 at 14:52
  • This is very weird, because exactly same code was working for me. Don't do DISPATCH_PROPERTYPUT - that won't work when the property is an automation object. Here are few things to try: 1) Open the F12 developer console, and try to enumerate all the properties of window. 2) Let's put a dummy params.rgdispidNamedArgs (eventhough cNamedArgs remains 0, just define DISPID dummy, and params.rgdispidNamedArgs=&dummy; 3) If still doesn't work, write some code after the dispxExternal->Invoke to try to read the same argument. See if it is working. – Uri London Jan 11 '12 at 15:02
  • 4) Still doesn't work??? do some assembly level debugging. Download and install WinDBG. Debug iexplore.exe. Get the symbols of iexplore.exe from the Microsoft web site. Put a break point at: MSHTML!CBase::varInvoke. Make sure same object is used ('this' point at ebp+8). – Uri London Jan 11 '12 at 15:17
  • Thanks uri. I tried to enumerate the windows properties in the my test js itself. Though i am not sure how to get the name of the `[object]`. Also my development environment is IE7, so i can't use ie developer tools. I am not sure about the 3) could you please explain it. I have tried the 2) but that is not working. i will next try the option 4). – Favonius Jan 11 '12 at 15:34
  • One more update: I have added the following code `if(FAILED(hr)){::MessageBox(NULL, L"Failed", L"Message", MB_OK);}` as the last statement. It seems the `dispxWnd->Invoke(...)` is failing on IE7. – Favonius Jan 11 '12 at 15:45
  • **But** it is failing only with `DISPATCH_PROPERTYPUTREF` and not with `DISPATCH_PROPERTYPUT`. This is kind of weird, i am not sure why it is happening. – Favonius Jan 11 '12 at 15:51
  • On further analysis I found that with `DISPATCH_PROPERTYPUTREF ` the value of hr is `DISP_E_MEMBERNOTFOUND`, which as per MSDN: `The requested member does not exist, or the call to InvokeEx tried to set the value of a read-only property.`Any suggestions. Thanks. – Favonius Jan 11 '12 at 16:01
  • Definitely, you must always check all the return code (the code above is simplified). Few more suggestions: 5) Let's see if you can add a simple property, say an Integer. For an integer, use DISPATCH_PROPERTYPUT, and varMyBho variant would be LONG in the constructor. See if that work. 6) For automation object (IDispatch) it has to be DIPATCH_PROPERTYPUTREF. So this call fails. If you can step through the assembly of Invoke mshtml.dll. Put a break point on the AddRef and QueryInterface (the Invoke should call these functions) and try to understand what's going on. – Uri London Jan 11 '12 at 17:11
  • Thanks Uri for your help. I am still struggling with the problem. I will try the WinDBG approach as the final step. Anyway thanks again. +1 your answer. – Favonius Jan 12 '12 at 07:12
  • I was unable to extend my bho as you and igor suggested, but the activex implementation is working. I will try to explore the WinDBG approach in my spare time. Thanks again. – Favonius Jan 12 '12 at 12:58
  • Well, the problem with the ActiveX approach is that you'll get a different instance of your object. The would be one object created as a BHO and is available throughout the life of the browser (that is, the IE tab), and there would be another instance created by the page ActiveX. If you are stateless, that shouldn't be a problem, but if you have state in the BHO that's a problem. If you don't mind sharing your code, and your code is small enough, I wouldn't mind taking a quick look. – Uri London Jan 12 '12 at 13:30
  • 2
    The scope of my application is very limited. It is inserting the jquery and jqueryui libraries on any given page. Which is then used for augmenting the look and feel of the existing application. The interaction with js and bho is required for saving the html text at a fixed location and always as text. The problem you stated are very true and also there are security angle associated with activex. that is why i wanted to use bho. I have created simple bare bone bho application to test this. Please find it at http://code.google.com/p/simple-atl-bho/downloads/list. Thanks. – Favonius Jan 12 '12 at 18:36
  • Hey @Favonius would you mind providing a test framework that outlines the use of the activex approach? I've found some articles (you probably have seen http://blogs.msdn.com/b/nicd/archive/2007/04/18/calling-into-your-bho-from-a-client-script.aspx as well), but some solid examples would be a bit nicer for us C# guys who rarely leave the managed code :) – Mike Jan 24 '12 at 08:11
  • 2
    @uri: Thanks. Finally got it working i.e. calling BHO from JS and that without ActiveX. Atleast working for IE8 and IE7. Though not sure about ie9 and above. It was just a silly mistake from my side!! Forgot to expose the method in the idl. Though `DISPATCH_PROPERTYPUTREF` is not working for me, instead i have tried `DISPATCH_PROPERTYPUT` and it worked. Thanks for your time. – Favonius Jan 31 '12 at 09:55