Hybrid Native/Browser-Based Windows Mobile App

So you're a Windows Mobile 6 developer working on a C++ application, and you're using the WebBrowser control (which, I guess, is an instance of Pocket IE, the mobile version of Internet Explorer circa IE 6). Your application is hosting a web page, and on that page you want to have a button, and when that button is clicked, you want to run some native code, and then give the result back to the page so that JavaScript can act on it.

Okay, so I'm not sure that this particular "you" exists.  But that was me, recently, and I spent a lot of time researching how to do this.  The answer isn't straightforward because Pocket IE isn't the same as Desktop IE and the documentation is hard to find.

On the desktop the answer is fairly straightforward:  You can use IHTMLDocumentEvents to add an onclick handler and after processing the click, use IHTMLDocument::Script to get the script engine and ask it to run a JavaScript function.

But in Pocket IE, there is no IHTMLDocumentEvents, and no access to the script engine (at least, not that I've found).  In fact, the whole process is different.  Hence this page.

The general strategy I used is:

  • In the HTML, add a button that the user is going to click to call your native function and an input field to receive the result.  (The input field can be invisible).
  • In the dialog’s DocumentComplete (your notification from the browser control that it’s done loading the document):
    • Find buttons on the page (by looking for PIEHTMLInputButtonElement objects) and add click listeners.
    • Also find the input field that will be receiving the result.
  • Pass the result back to the JavaScript by calling put_value on the result field.

I'm going to assume you've already got a dialog that's hosting the browser control and you have an IPIEHTMLDocument2.  If not, you need to get it from the DocumentComplete event.

The includes for the symbols you'll need are:

[cc lang="C++"]
#include
#include
[/cc]

Here's the class I'm using as the listener.  This class implements, through IDispEventSimpleImpl, the dispatch map for the IPIEHTMLInputButtonElementEvents interface.

[cc lang="c++"]
class ButtonEventListener :
      public CObject,
      public IDispEventSimpleImpl
{
      DECLARE_DYNAMIC(ButtonEventListener)
 
      BEGIN_SINK_MAP(ButtonEventListener)
            SINK_ENTRY_EX(ID_BUTTON,
                                DIID_PIEHTMLInputButtonElementEvents,
                                DISPID_HTMLINPUTELEMENTEVENTS_ONCLICK,
                                &ButtonEventListener::OnInputButtonClick)
      END_SINK_MAP()
 
protected:
      HRESULT GetFuncInfoFromId(const IID& iid, DISPID dispidMember,
            LCID lcid, _ATL_FUNC_INFO& info);
 
      CMyBrowserDlg *m_pOwner;
 
      CComPtr m_spButton;
 
public:
      ButtonEventListener(CLCBrowserDlg *pOwner);
      virtual ~ButtonEventListener() {};
 
      void ListenTo(IPIEHTMLInputButtonElement *pButton);
       void __stdcall OnInputButtonClick(IDispatch *pDisp, VARIANT_BOOL *Cancel);
};
[/cc]

A few comments on this class:

  • GetFuncInfoFromId is called because we’re using IDispEventSimpleImpl which doesn’t require a type library.  GetFuncInfoFromId eturns the information that would normally come from the type library.
  • You can embed this class in your dialog class (for example), and pass its constructor the dialog.  This is how I pass the click message from the event sink class back to the dialog.  There are lots of ways to do this; pick one you like.
  • ID_BUTTON is just an arbitrary number (I used ‘1’).

The implementation:

[cc lang="c++]
IMPLEMENT_DYNAMIC(ButtonEventListener, CCmdTarget)

// Constructor: Just store the owner dialog so we can call it later
ButtonEventListener::ButtonEventListener(CMyBrowserDlg *pOwner)
      : m_pOwner(pOwner)
{
}

// Called by the dialog to start listening on a particular button
void ButtonEventListener::ListenTo(IPIEHTMLInputButtonElement *pButton)
{
      m_spButton.Attach(pButton);
      HRESULT hr = IDispEventSimpleImpl::DispEventAdvise(pButton, &DIID_PIEHTMLInputButtonElementEvents);
}

// Called by IE when the button is clicked.
void __stdcall ButtonEventListener::OnInputButtonClick(IDispatch *pDisp, VARIANT_BOOL *Cancel)
{
      m_pOwner->OnButtonClick(m_spButton);
      return;
}

// Required to tell the caller about the function's calling convention (__stdcall)
HRESULT ButtonEventListener::GetFuncInfoFromId(const IID& iid, DISPID dispidMember,
        LCID lcid, _ATL_FUNC_INFO& info)
{
      if (InlineIsEqualGUID(iid, DIID_PIEHTMLInputButtonElementEvents))
      {
            info.cc = CC_STDCALL;
            switch(dispidMember)
            {
                  case DISPID_HTMLINPUTELEMENTEVENTS_ONCLICK:
                        info.vtReturn = VT_I4;
                        info.nParams = 0;
                 return S_OK;
 
                  default:
                 return E_FAIL;
            }
      }

       return E_FAIL;
}
[/cc]

The next step is finding the buttons you want to handle and the control where you’re going to write the result.  What I did for this is I called a function in my DocumentComplete handler which walks the list of all the elements on the page looking for buttons to handle and the input field to use for the response text. 

[cc lang="c++"]
// Set up for native handling of button clicks.
void CLCBrowserDlg::RegisterButtonListener()
{
      // Find the IPIEHTMLDocument2
      CComPtr pDoc2;
      HRESULT hr = E_FAIL;
      CComPtr spDisp;
      if (FAILED(m_spIWebBrowser2->get_Document(&spDisp)))
            return;
 
      pDoc2 = spDisp;
 
      // Get all the elements on the page
      CComPtr spAllElements;
      if (SUCCEEDED(pDoc2->get_all(&spAllElements)))
      {
            LONG lLen = 0;
            if (SUCCEEDED(spAllElements->get_length(&lLen)))
            {
                  // For each item on the page..
                  for (LONG i = 0; i spDispItem;
                        _variant_t index = i;
                        if (SUCCEEDED(spAllElements->item(index, index, &spDispItem)))
                        {
                              // Is it a button?
                              CComPtr spButton;
                              if (SUCCEEDED(spDispItem->QueryInterface(IID_IPIEHTMLInputButtonElement, (void **)&spButton)))
                              {
                                    // Yes .. attach the listener
                                    m_ButtonListener.ListenTo(spButton);
                              }
 
                              // Is it a text field with the specific id we're looking for?
                              CComPtr spTextInput;
                              if (SUCCEEDED(spDispItem->QueryInterface(IID_IPIEHTMLInputTextElement, (void **)&spTextInput)))
                              {
                                    CComBSTR id;
                                    spTextInput->get_id(&id);
                                    if (CString(L"__ipc_result__").Compare(id) == 0)
                                    {
                                          // It is; save it for later
                                          m_spRPC = spTextInput;
                                    }
                              }
                        }
                  }
            }
      }
}
[/cc]

Call RegisterButtonListener from DocumentComplete and you should be getting clicks from the browser hitting breakpoints in your native code.  Getting a result back to the HTML is simple, assuming it's a string:

[cc lang="C++"]
void CMyBrowserDlg::OnButtonClick(IPIEHTMLInputButtonElement *pButton)
{
m_spRPC->put_value(L"Some Value from Native Code");
}
[/cc]

If you need to pass something complex there’s always XML. 

With this snippet of HTML:

[cc lang="html"]



[/cc]

Assuming you’ve got everything set up correctly, a click on the “Click Me” button will cause the alert to show up (‘yo yo’), followed by the native code running and the result field’s value changing to the value we wrote to it from the C++ code. 

It'd be nice if the onclick handler ran after the C++ code instead of before, but you could use this behaviour to set a timer to run after the click to read the result.

The code above has not been extensively tested.  It worked when I tried it, but it’s not perfect. Use at at your own risk.  One missing piece is unsubscribing from the events when you no longer need to listen for them.

But hopefully someone will find this useful.  Let me know if you do.