2

我正在使用 .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 等等,但似乎无法让它工作。

我发现的最接近的是在 WebBrowser 中调用一个脚本,并等待它完成运行(同步),但它使用了 VS2012 中引入的功能(我正在使用 VS2010),我认为它无论如何都不会工作有点不同,因为您不需要等待 javascript 事件触发。

任何帮助将不胜感激。

4

1 回答 1

1

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

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

一种方法是使用带有用户友好消息的模式对话框,当所需的 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();
    }
}


看起来像这样:


使用模态对话框阻止 UI


另一种选择(用户不太友好的选择)是使用类似WaitOneAndPumpfrom here的东西。您仍然需要注意禁用主 UI 并向用户显示某种等待反馈。


更新以解决评论。您的 WebBrowser 实际上是 UI 的一部分并且对用户可见吗?用户应该能够与之交互吗?如果是这样,您就不能使用辅助线程来执行 JavaScript。您需要在主线程上执行此操作并继续发送消息,但WaitOne不会发送大部分 Windows 消息(它只发送一小部分与 COM 相关的消息)。您也许可以使用WaitOneAndPump我上面提到的。您仍然需要在等待时禁用 UI,以避免重新进入。

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

于 2014-08-21T08:14:15.330 回答