Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
348 views
in Technique[技术] by (71.8m points)

c# - 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:windowssystem32stdole2.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);
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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:windowssystem32stdole2.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);                                                       
    }

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...