在WebBrowser中调用javascript函数并等待javascript事件触发

时间:2014-08-21 05:30:35

标签: c# .net asynchronous webbrowser-control invokescript

我在.NET工作,C#是特定的,创建一个Win Forms UserControl,它包含一个WebBrowser控件。 WebBrowser控件托管一个页面,该页面又使用第三方javascript组件。我遇到的问题是调用javascript函数初始化第三方javascript组件并阻止Windows窗体应用程序中的UI,直到组件初始化为止,组件通过内部javascript事件通知您它有。

部分问题是更改第三方javascript组件的任何配置参数的唯一方法是使用新配置重新初始化它。因此,例如,如果要将其设置为只读,则必须使用只读参数重新初始化它。

我已经完成了所有工作,能够调用Document.InvokeScript,然后在网页中使用window.external调用UserControl方法,但我遇到的问题是如何阻止UserControl代码,用于初始化javascript组件,以便它等待并且不会将控制权返回给用户,直到javascript组件的初始化完成为止。

我需要它以这种方式工作的原因是因为如果我有一个"只读"表单上的复选框,用于更改UserControl的ReadOnly属性,以控制javascript组件是否将数据显示为只读,并且用户非常快速地单击该复选框,您将收到javascript错误或复选框将与...不同步javascript组件的实际只读状态。这似乎是因为控件在其配置发生变化并且您已经尝试再次更改之后尚未重新初始化。

我花费了数小时的时间尝试使用从AutoResetEvent到Application.DoEvents等所有功能,但似乎无法使其正常工作。

我找到的最接近的是Invoke a script in WebBrowser, and wait for it to finish running (synchronized),但它使用了VS2012中引入的功能(我使用的是VS2010)并且我认为它无论如何都不会起作用,因为它&#39 ;有点不同,因为你不等待javascript事件发生。

非常感谢任何帮助。

1 个答案:

答案 0 :(得分:1)

首先,问题是需要“阻止”UI线程,直到某个事件被触发为止。通常可以将应用程序重新分解为使用异步事件处理程序(带或不带async/await),以将执行控制返回到消息循环并避免任何阻塞。

现在让我们说,出于某种原因,你不能重新考虑你的代码。在这种情况下,您需要一个辅助模态消息循环。您还需要在等待事件时禁用主UI,以避免令人讨厌的重入场景。等待本身应该是用户友好的(例如,使用等待光标或进度动画)和非忙碌(避免在DoEvents的紧密循环上烧掉CPU周期。)

执行此操作的一种方法是使用带有用户友好消息的模式对话框,当发生所需的JavaScript事件/回调时,该消息会自动被解除。这是一个完整的例子:

using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace WbTest
{
    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.None)]
    [ComDefaultInterface(typeof(IScripting))]
    public partial class MainForm : Form, IScripting
    {
        WebBrowser _webBrowser;
        Action _onScriptInitialized;

        public MainForm()
        {
            InitializeComponent();

            _webBrowser = new WebBrowser();
            _webBrowser.Dock = DockStyle.Fill;
            _webBrowser.ObjectForScripting = this;
            this.Controls.Add(_webBrowser);

            this.Shown += MainForm_Shown;
        }

        void MainForm_Shown(object sender, EventArgs e)
        {
            var dialog = new Form
            {
                Width = 100,
                Height = 50,
                StartPosition = FormStartPosition.CenterParent,
                ShowIcon = false,
                ShowInTaskbar = false,
                ControlBox = false,
                FormBorderStyle = FormBorderStyle.FixedSingle
            };
            dialog.Controls.Add(new Label { Text = "Please wait..." });

            dialog.Load += (_, __) => _webBrowser.DocumentText = 
                "<script>setTimeout(function() { window.external.OnScriptInitialized}, 2000)</script>";

            var canClose = false;
            dialog.FormClosing += (_, args) =>
                args.Cancel = !canClose;

            _onScriptInitialized = () => { canClose = true; dialog.Close(); };

            Application.UseWaitCursor = true;
            try
            {
                dialog.ShowDialog();
            }
            finally
            {
                Application.UseWaitCursor = false;
            }

            MessageBox.Show("Initialized!");
        }

        // IScripting
        public void OnScriptInitialized()
        {
            _onScriptInitialized();
        }
    }

    [ComVisible(true)]
    [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    public interface IScripting
    {
        void OnScriptInitialized();
    }
}

<小时/> 看起来像这样:


Blocking the UI with a modal dialog


另一种选择(用户不太友好)是使用来自hereWaitOneAndPump之类的内容。您仍然需要注意禁用主UI并向用户显示某种等待反馈。


更新以解决评论。您的WebBrowser实际上是UI的一部分并且对用户可见吗?用户是否应该能够与之互动?如果是这样,则不能使用辅助线程来执行JavaScript。您需要在主线程上执行此操作并继续传送消息,但WaitOne不会抽取大多数Windows消息(仅与{COM 3相关的pumps a small fraction of them)。您可以使用我上面提到的WaitOneAndPump。您仍需要在等待时禁用UI,以避免重新入侵。

无论如何,这仍然是一个混乱。你真的不应该阻止执行只是为了保持线性代码流。如果你不能使用async/await,你总是可以实现一个简单的状态机类,并使用回调从它离开的地方继续。这就是过去async/await之前的情况。