2

I'm working in .NET, C# to be specific, creating a Win Forms UserControl, which contains a WebBrowser control. The WebBrowser control hosts a page, which in turn uses a third-party javascript component. The problem I'm having is with invoking a javascript function to initialize the third-party javascript component and block the UI in the Windows Forms application until the component has been initialized, which the component notifies you of through an internal javascript event that it has.

Part of the problem is that the only way to change any configuration parameter of the third-party javascript component is to re-initialize it with the new configuration. So for example, if you want to make it read-only you have to re-initialize it with the read-only parameter.

I've got everything working in terms of being able to call the Document.InvokeScript and then in the web page call the UserControl method using window.external but the problem I'm having is how to block the UserControl code that makes the call to initialize the javascript component so that it waits and doesn't return control to the user until the initialization of the javascript component has been completed.

The reason I need it to work this way is because if I have a "Read-Only" checkbox on the form that changes the the ReadOnly property of the UserControl to control whether the javascript component shows the data as read-only and the user clicks that checkbox really quickly you will either get a javascript error or the checkbox will get out of sync with the actual read-only state of the javascript component. This seems to happen because the control hasn't re-initialized yet after it's configuration has changed and you're already trying to change it again.

I've spent hours and hours trying work out a way to make it work using everything from AutoResetEvent to Application.DoEvents and so on, but don't seem to be able to get it working.

The closest I've found is Invoke a script in WebBrowser, and wait for it to finish running (synchronized) but that uses features introduced in VS2012 (and I'm using VS2010) and I don't think it would work anyway as it's a bit different in that you're not waiting for a javascript event to fire.

Any help would be greatly appreciated.

Community
  • 1
  • 1
Sheldmandu
  • 1,700
  • 16
  • 10

1 Answers1

1

The problem in the first place is the requirement to "block" the UI thread until some event has been fired. It's usually possible to re-factor the application to use asynchronous event handlers (with or without async/await), to yield execution control back to the message loop and avoid any blocking.

Now let's say, for some reason you cannot re-factor your code. In this case, you'd need a secondary modal message loop. You'd also need to disable the main UI while you're waiting for the event, to avoid nasty re-entrancy scenarios. The waiting itself should to be user-friendly (e.g., use the wait cursor or progress animation) and non-busy (avoid burning CPU cycles on a tight loop with DoEvents).

One way to do this is to use a modal dialog with a user-friendly message, which gets automatically dismissed when the desired JavaScript event/callback has occured. Here's a complete example:

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();
    }
}


Which looks like this:

Blocking the UI with a modal dialog


Another option (a less user-friendly one) is to use something like WaitOneAndPump from here. You'd still need to take care about disabling the main UI and showing some kind of waiting feedback to the user.


Updated to address the comment. Is your WebBrowser actually a part of the UI and visible to the user? Should the user be able to interact with it? If so, you cannot use a secondary thread to execute JavaScript. You need to do it on the main thread and keep pumping messages, but WaitOne doesn't pump most of Windows messages (it only pumps a small fraction of them, related to COM). You might be able to use WaitOneAndPump which I mentioned above. You'd still need to disable the UI while waiting, to avoid re-entrancy.

Anyhow, that'd still be a kludge. You really shouldn't be blocking the execution just to keep the linear code flow. If you can't use async/await, you can always implement a simple state machine class and use callbacks to continue from where it was left. That's how it used to be before async/await.

Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • Thanks for the comprehensive answer. Ultimately what I'm trying to do is avoid re-entrance scenarios more so than block the UI as such. I was thinking it may be possible to call the external javascript and have the externally exposed function in a separate thread perhaps and then use AutoResetEvent with a WaitOne in the main thread and calling AutoResetEvent.Set() in the other thread. Would that be a better solution you think? Ultimately I don't really want to show any modal windows as it's not really necessary as the javascript executes in less than a second, but fast clicks cause issues. – Sheldmandu Aug 25 '14 at 00:06
  • Yes, the WebBrowser is part of the UI and the user should be able to interact with it, but not while the javascript component being shown within it is loading. I'll try the options you've suggested above. – Sheldmandu Aug 26 '14 at 05:27