Disable WebBrowser's Context-Menu in NETCF applications

  • A possible approach valid on WM5\6\6.1, no longer using DTM_BROWSERDISPATCH
  • Appendix: sample code to find child windows (EnumChildWindows not available under Windows CE)

Undocumented doesn't mean "not achievable". Undocumented means that on future versions that particular detail may change. So if the application doesn't work on new platforms, that particular thing should be the first one to check. This was for example what happened to the ClassName of NETCF applications... see Daniel Moth's post about this: "#NETCF_AGL_".

Recently one developer I've worked with pointed me to this link, which describes a possible way to disable NETCF's WebBrowser's context-menu, which worked only on Windows Mobile 2003. He needed help on understanding why it no longer worked on WM5\6\6.1 and moreover on finding a suitable way to reach the same goal by other means, if that used in WM2003 really wasn't doable on later platforms.

Result of our analysis was that the aforementioned solution was based on DTM_BROWSERDISPATCH, which proved to work on WM2003 but is no longer working on WM5\6\6.1 devices, because the IBrowser interface was *deprecated* -- please see documentation here, specifically "IBrowser, IBrowser2 and IBrowser3 are deprecated and will not be supported in future versions. Instead use IWebBrowser2 and DWebBrowserEvents2. ".

A solution for current OSs may be found by subclassing the window of the native HTML control that is ultimately wrapped by the NETCF's WebBrowser: we used the same approach I described in Subclassing NETCF Applications. Subclassing means intercepting the Win32 messages sent to a window, by modifying its WndProc's address (which is the function that handles all messages, the so-called "message-pump"): once you intercept a message, you can deal with it or simply pass it to the old WndProc.

However, to be able to intercept messages you must be able to address the window you want to subclass: and here it comes the "undocumented" thing, which is simply the Class Name of the window associated to the native htmlview.dll control: this is not documented anywhere, but proved to be *PIEHTML* on every WM5\6\6.1 platforms I tested Remote Spy with. Furthermore, the undocumented detail has not been retrieved by looking at Microsoft's internal resources: it's publicly available to every developer who can use the "Remote Spy" tool available with VS2008. Here it is a screenshot:

image

Interestingly, we found out that there's an easy OEM-specific solution for Motorola devices, while for other other devices\emulators we had to analyze the message-chain to understand a possible pattern, and found one based on WM_NOTIFY and WM_LBUTTONUP, which should work on every scenario (to be thoroughly tested...).

So, basically we want to avoid the contextmenu to appear: being the WM_CONTEXTMENU the first message sent to the window while pressing&holding the stylus, the 1st test was to use a code like the following:

 private IntPtr NewWndProc(IntPtr hWnd, uint msg, int wParam, int lParam)
{
    switch (msg)
    {
        case WM_CONTEXTMENU:
            return IntPtr.Zero;
            break;
    } 
    return CallWindowProc(oldWndProc, hWnd, msg, wParam, lParam); 
}

As I said, this worked only for Motorola devices: I can't state why since it may depend on whatever customization around the webview.dll native control done by the OEM (?). In any case, that didn't work on WM5\6\6.1 Emulators (so nothing OEM-dependent), and we had  to look at other patterns. An idea was to intercept WM_NOTIFY and pass it to the old WndProc only if the next WM_message was not one of WM_INITMENUPOPUP, WM_ENTERMENULOOP and WM_CONTEXTMENU: by doing so I could prevent the contextmenu to appear, however links on the page could not be clicked. Everything worked apart from this "detail"... Tongue out so, looking at the chain of messages (still by using simply Remote Spy!) I could notice that when user clicks on a link there's a WM_LBUTTONUP followed by a WM_NOTIFY. So I based the NewWndProc on the following and this worked:

 private IntPtr NewWndProc(IntPtr hWnd, uint msg, int wParam, int lParam)
{
    switch (msg)
    {
        //when clicking on a link on the page, WM_NOTIFY is sent AFTER WM_LBUTTONUP
        //however, reset the bool variable if WM_NOTIFY is not received just after WM_LBUTTONUP
        case WM_LBUTTONUP:
            bLButtonUpHandled = true;
            break;
        case WM_NOTIFY:
            if (bLButtonUpHandled)
            {
                bLButtonUpHandled = false;
                break;
            }
            else
            {
                //block WM_NOTIFY
                return IntPtr.Zero;
            }
        case WM_CONTEXTMENU:
            //If you need to do something custom, do it here
            DialogResult dlg = MessageBox.Show("Close?", this.Text, MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1);
            if (dlg == DialogResult.Yes)
            { 
                this.webBrowser1.Navigate(new Uri("https://blogs.msdn.com/raffael"));
                return IntPtr.Zero;
            }
            break;

        //for every other WM_x, if this is coming after WM_LBUTTONUP then reset bLButtonUpHandled
        default:
            if (bLButtonUpHandled)
                bLButtonUpHandled = false;

            break;
        
    }
    return CallWindowProc(oldWndProc, hWnd, msg, wParam, lParam);
}

Remember that the code provided is for didactic purposes only: it shows how you can reach the goal, however doesn't contain error-handling. Obviously I do recommend including try\catch and exception-handling!!

APPENDIX: FindWindow API can be used only for top-level windows. If you need the handle of a child window knowing its ClassName and having the handle of the parent, then you can use for example the following code:

 private IntPtr FindChildWindowByParent(string strChildClassName, IntPtr hWndTopLevel)
{
    bool bFound = false;
    IntPtr hwndCur = IntPtr.Zero;
    IntPtr hwndCopyOfCur = IntPtr.Zero;
    char[] chArWindowClass = new char[32];

    do
    {
        // Is the current child null?
        if (IntPtr.Zero == hwndCur)
        {
            // get the first child
            hwndCur = GetWindow(hWndTopLevel, (uint)GetWindowFlags.GW_CHILD);
        }
        else
        {
            hwndCopyOfCur = hwndCur;
            // at this point hwndcur may be a parent of other windows
            hwndCur = GetWindow(hwndCur, (uint)GetWindowFlags.GW_CHILD);

            // in case it's not a parent, does it have "brothers"?
            if (IntPtr.Zero == hwndCur)
                hwndCur = GetWindow(hwndCopyOfCur, (uint)GetWindowFlags.GW_HWNDNEXT);
        }

        //if we found a window (child or "brother"), let's see if it's the one we were looking for
        if (IntPtr.Zero != hwndCur)
        {
            GetClassName(hwndCur, chArWindowClass, 256);
            string strWndClass = new string(chArWindowClass);
            strWndClass = strWndClass.Substring(0, strWndClass.IndexOf('\0'));

            bFound = (strWndClass.ToLower() == strChildClassName.ToLower());
        }
        else
            break;
    }
    while (!bFound);

    //found!
    return hwndCur;
}

If you don't have the handle of the parent or even don't know which top-level window it is, since we don't have EnumChildWindows under Windows CE, you need to use EnumWindows and basically invoke the function above for each of them, to be run only in case the child window hasn't been found yet -- I mean the following:

 private void FindChildWindow(string strChildClassName, string strChildWindowName)
{
    //Enum all top-level windows
    //for each window, see if it has childs and if among them there's the window we're looking for
    EnumWindows(new EnumWindowsDelegate(EnumWindowsProc), 0);
    return;
}

private int EnumWindowsProc(IntPtr hWndParent, int lParam)
{
    IntPtr hwndCur = IntPtr.Zero;
    IntPtr hwndCopyOfCur = IntPtr.Zero;
    char[] chArWindowClass = new char[32];
    
    //for each window, see if it has childs and if among them there's the window we're looking for
    //if already found, don't search again
    if (!bFound)
    {
        do
        {
            // Is the current child null?
            if (IntPtr.Zero == hwndCur)
            {
                // get the first child
                hwndCur = GetWindow(hWndParent, (uint)GetWindowFlags.GW_CHILD);
            }
            else
            {
                hwndCopyOfCur = hwndCur;
                // at this point hwndcur may be a parent of other windows
                hwndCur = GetWindow(hwndCur, (uint)GetWindowFlags.GW_CHILD);

                // in case it's not a parent, does it have "brothers"?
                if (IntPtr.Zero == hwndCur)
                    hwndCur = GetWindow(hwndCopyOfCur, (uint)GetWindowFlags.GW_HWNDNEXT);
            }

            //if we found a window (child or "brother"), let's see if it's the one we were looking for
            if (IntPtr.Zero != hwndCur)
            {
                GetClassName(hwndCur, chArWindowClass, 256);
                string strWndClass = new string(chArWindowClass);
                strWndClass = strWndClass.Substring(0, strWndClass.IndexOf('\0'));

                bFound = (strWndClass.ToLower() == strChildClassName.ToLower());
            }
            else
                break;
        }
        while (!bFound);

        //found!
        hWndTarget = hwndCur;
    }
    return 1;
}

[DllImport("coredll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool EnumWindows(
    [MarshalAs(UnmanagedType.FunctionPtr)]EnumWindowsDelegate lpEnumFunc,
    int lParam);

internal delegate int EnumWindowsDelegate(IntPtr hwnd, int lParam); //NOTE THE RETURN VALUE!!!

Last hint: when invoking delegates of NATIVE functions, NETCF has a limitation about the type returned: it must be an integer, or in any case a blittable and integer-based datatype. I was using managed Boolean (bool in C#), but this is not blittable to a native BOOL because they are 1 Byte in managed code and 4 bytes in native code!! This limitation was a design decision on V2 because the registers for return values are 4 Byte integer registers on most platforms... and made me waste some time… it sufficed to use private int EnumWindowsProc() instead of private bool EnumWindowsProc() and Win32 and .NET started talking each other. Nerd

Cheers,

~raffaele