Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to intercept the onbeforeunload event in a WebBrowser control?

I have a WinForms application in which I have hosted a web page inside a WebBrowser control.

The contents of the web page is the following:

<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
  <title>onbeforeunload test</title>
  <meta charset="utf-8">
</head>
<body>

<a href="#" onclick="window.location.reload();">Test</a>

<script type="text/javascript">
    window.onbeforeunload = function () {
        return 'Are you sure you want to leave this page?';
    };
</script>
</body>
</html>

As you can see I have subscribed to the onbeforeunload event which allows to show a confirmation dialog before navigating away from this page. This works fine when I click on the anchor that reloads the page. The confirmation box is shown and the user can cancel the reload of the page. This works fine inside the WinForms hosted control.

Now, what I am having difficulties with is intercepting and executing this event when the user closes the WinForms application (by clicking on the X button for example).

I am able to fetch the contents of this function in the WinForms application but no matter what I tried I wasn't able to get the contents of the string that this function returns so that I can use it later to fake a MessageBox when the user attempts to close the application:

webBrowser1.Navigated += (sender, e) =>
{
    webBrowser1.Document.Window.Load += (s, ee) =>
    {
        // In order to get the IHTMLWindow2 interface I have referenced
        // the Microsoft HTML Object Library (MSHTML) COM control
        var window = (IHTMLWindow2)webBrowser1.Document.Window.DomWindow;

        // the bu variable contains the script of the onbeforeunload event
        var bu = window.onbeforeunload();

        // How to get the string that is returned by this function
        // so that I can subscribe to the Close event of the WinForms application
        // and show a fake MessageBox with the same text?
    };
};
webBrowser1.Navigate("file:///c:/index.htm");

I have tried the window.execScript method to no available:

// returns null
var script = string.Format("({0})();", bu);
var result = window.execScript(script, "javascript");

I have also tried the following but it also returned null:

var result = window.execScript("(function() { return 'foo'; })();", "javascript");

As a final resort I could use a third party javascript parser to which I can feed the body of this function and it will execute it and give me the return value but that would really be a last resort. I would be happy if there was a more native way to do this using Microsoft's MSHTML library.


UPDATE:

This is now solved thanks to the excellent answer that @Hans provided. For some reason I couldn't make his solution work on my test machine (Win7 x64, .NET 4.0 Client Profile, IE9, en-US locale) and I was always getting hr = -2147024809 after the IDispatch.Invoke call. So I have modified the IDispatch P/Invoke signature to the following (this signature doesn't require a reference to c:\windows\system32\stdole2.tlb to be added to the project):

using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("00020400-0000-0000-C000-000000000046")]
public interface IDispatch
{
    [PreserveSig]
    int GetTypeInfoCount(out int Count);
    [PreserveSig]
    int GetTypeInfo(
        [MarshalAs(UnmanagedType.U4)] int iTInfo,
        [MarshalAs(UnmanagedType.U4)] int lcid, 
        out ITypeInfo typeInfo
    );

    [PreserveSig]
    int GetIDsOfNames(
        ref Guid riid,
        [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr)] string[] rgsNames, 
        int cNames, 
        int lcid, 
        [MarshalAs(UnmanagedType.LPArray)] int[] rgDispId
    );

    [PreserveSig]
    int Invoke(
        int dispIdMember, 
        ref Guid riid, 
        uint lcid, 
        ushort wFlags,
        ref System.Runtime.InteropServices.ComTypes.DISPPARAMS pDispParams, 
        out object pVarResult,
        ref System.Runtime.InteropServices.ComTypes.EXCEPINFO pExcepInfo, 
        IntPtr[] pArgErr
    );
}

and then I have subscribed to the Closing event of the Form and was able to fetch the message returned by the onbeforeunload event and prompt the user:

protected override void OnFormClosing(FormClosingEventArgs e)
{
    var window = (IHTMLWindow2)webBrowser1.Document.Window.DomWindow;
    var args = new System.Runtime.InteropServices.ComTypes.DISPPARAMS();
    var result = new object();
    var except = new System.Runtime.InteropServices.ComTypes.EXCEPINFO();
    var idisp = window.onbeforeunload as IDispatch;
    if (idisp != null)
    {
        var iid = Guid.Empty;
        var lcid = (uint)CultureInfo.CurrentCulture.LCID;
        int hr = idisp.Invoke(0, ref iid, lcid, 1, ref args, out result, ref except, null);
        if (hr == 0)
        {
            var msgBox = MessageBox.Show(
                this,
                (string)result,
                "Confirm",
                MessageBoxButtons.OKCancel
            );
            e.Cancel = msgBox == DialogResult.Cancel;
        }
    }
    base.OnFormClosing(e);
}
like image 339
Darin Dimitrov Avatar asked Jan 16 '12 16:01

Darin Dimitrov


3 Answers

I just dealt with a similar problem. This is a late answer, but hopefully it may help someone. The solution is based on this MSKB article. It also works for cases when the web page handles onbeforeunload event via attachEvent or addEventListener.

void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    // this code depends on SHDocVw.dll COM interop assembly,
    // generate SHDocVw.dll: "tlbimp.exe ieframe.dll",
    // and add as a reference to the project

    var activeX = this.webBrowser.ActiveXInstance;
    object arg1 = Type.Missing;
    object arg2 = true;
    ((SHDocVw.WebBrowser)activeX).ExecWB(SHDocVw.OLECMDID.OLECMDID_ONUNLOAD, SHDocVw.OLECMDEXECOPT.OLECMDEXECOPT_DODEFAULT, ref arg1, ref arg2);
    if (!(bool)arg2)
    {
        e.Cancel = true;
    }
}

The above code is for WinForms version of WebBrowser control. For WPF version, ActiveXInstance should be first obtained via reflection:

 var activeX = this.WB.GetType().InvokeMember("ActiveXInstance",
                    BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.NonPublic,
                    null, this.WB, new object[] { }) as SHDocVw.WebBrowser;
like image 159
noseratio Avatar answered Nov 11 '22 19:11

noseratio


IHtmlWindow2.onbeforeunload does not itself display the dialog. It merely returns a string which the host must then use in a message box. Since your Winforms app is the host it must use MessageBox.Show(). Calling onbeforeunload is difficult, it is an IDispatch pointer whose default member (dispid 0) returns the string. Add a reference to c:\windows\system32\stdole2.tlb and paste this code:

using System.Runtime.InteropServices;
...
        [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("00020400-0000-0000-C000-000000000046")]
        public interface IDispatch {
            int dummy1();
            int dummy2();
            int dummy3();
            [PreserveSig]
            int Invoke(int dispIdMember, ref Guid riid, int lcid, int dwFlags, 
                [In, Out] stdole.DISPPARAMS pDispParams, 
                [Out, MarshalAs(UnmanagedType.LPArray)] object[] pVarResult, 
                [In, Out] stdole.EXCEPINFO pExcepInfo, 
                [Out, MarshalAs(UnmanagedType.LPArray)] IntPtr[] pArgErr);
        }

You'll use it like this:

    protected override void OnFormClosing(FormClosingEventArgs e) {
        var window = (IHTMLWindow2)webBrowser1.Document.Window.DomWindow;
        var args = new stdole.DISPPARAMS();
        var result = new object[1];
        var except = new stdole.EXCEPINFO();
        var idisp = (IDispatch)window.onbeforeunload;
        var iid = Guid.Empty;
        int hr = idisp.Invoke(0, ref iid, 1033, 1, args, result, except, null);
        if (hr == 0) {
            if (MessageBox.Show(this, (string)result[0], "Confirm",
                MessageBoxButtons.OKCancel) == DialogResult.Cancel) e.Cancel = true;
        }
        base.OnFormClosing(e);                                                       
    }
like image 28
Hans Passant Avatar answered Nov 11 '22 19:11

Hans Passant


If your happy to prematurely execute the event code (which could be anything) the following captures the string for me in your Window.Load;

Object[] args = { @"(" + bu + ")();" };
string result = webBrowser1.Document.InvokeScript("eval", args).ToString();
like image 1
Alex K. Avatar answered Nov 11 '22 18:11

Alex K.