1

I've created a new WebBrowser() control in a new Thread().

The problem I'm having, is that when invoking a delegate for my WebBrowser from the Main Thread, the call is occurring on the Main Thread. I would expect this to happen on browserThread.

private static WebBrowser defaultApiClient = null;

delegate void DocumentNavigator(string url);


private WebApi() {

    // Create a new thread responsible 
    // for making API calls.
    Thread browserThread = new Thread(() => {

        defaultApiClient = new WebBrowser();

        // Setup our delegates
        documentNavigatorDelegate = new DocumentNavigator(defaultApiClient.Navigate);

        // Anonymous event handler
        defaultApiClient.DocumentCompleted += (object sender, WebBrowserDocumentCompletedEventArgs e) => {
            // Do misc. things
        };

        Application.Run();
    });
    browserThread.SetApartmentState(ApartmentState.STA);
    browserThread.Start();

}

DocumentNavigator documentNavigatorDelegate = null;
private void EnsureInitialized() {

    // This always returns "false" for some reason
    if (defaultApiClient.InvokeRequired) {

        // If I jump ahead to this call
        // and put a break point on System.Windows.Forms.dll!System.Windows.Forms.WebBrowser.Navigate(string urlString, string targetFrameName, byte[] postData, string additionalHeaders)
        // I find that my call is being done in the "Main Thread".. I would expect this to be done in "browserThread" instead
        object result = defaultApiClient.Invoke(documentNavigatorDelegate, WebApiUrl);

    }

}

I've tried invoking the method a myriad of ways:

// Calls on Main Thread (as expected)
defaultApiClient.Navigate(WebApiUrl);

// Calls on Main Thread
defaultApiClient.Invoke(documentNavigatorDelegate, WebApiUrl); 

// Calls on Main Thread
defaultApiClient.BeginInvoke(documentNavigatorDelegate, WebApiUrl); 

// Calls on Main Thread
documentNavigatorDelegate.Invoke(WebApiUrl);

// Calls on random Worker Thread
documentNavigatorDelegate.BeginInvoke(WebApiUrl, new AsyncCallback((IAsyncResult result) => { .... }), null);

Update

Let me break down my end-goal a little bit to make things more clear: I have to make calls using WebBrowser.Document.InvokeScript(), however Document is not loaded until after I call WebBrowser.Navigate() and THEN the WebBrowser.DocumentComplete event fires. Essentially, I cannot make my intended call to InvokeScript() until after DocumentComplete fires... I would like to WAIT for the document to load (blocking my caller) so I can call InvokeScript and return my result in a synchronous fashion.

Basically I need to wait for my document to complete and the way I would like to do that is with a AutoResetEvent() class which I will trigger upon DocumentComplete being fired... and I need all this stuff to happen in a separate thread.

The other option I see is doing something like this:

private bool initialized = false;
private void EnsureInitialized(){
    defaultApiClient.Navigate(WebApiUrl);
    while(!initialized){
        Thread.Sleep(1000); // This blocks so technically wouldn't work
    }
}

private void defaultApiClient_DocumentComplete(object sender, WebBrowserDocumentCompletedEventArgs e){
    initialized = true;
}
noseratio
  • 59,932
  • 34
  • 208
  • 486
Ryan Griffith
  • 1,591
  • 17
  • 41
  • 4
    `I've created a new WebBrowser() control in a new Thread()` Well there's your problem. Don't do that. Create UI components on the UI thread, not in non-UI threads. Also don't create multiple UI threads. Use a single UI thread. When you fix those problems the rest should sort itself out. – Servy Feb 14 '14 at 21:51
  • I've done this to prevent blocking. I do not wany this UI to be seen either, this is hidden. I need strictly the facilities provided by the control to invoke Javascript functionality. I've implemented basic concept per the suggested methodology listed here: http://stackoverflow.com/questions/4269800/webbrowser-control-in-a-new-thread The only difference is that I would like to invoke the Navigate() event from outside of the the new thread's anonymous function. – Ryan Griffith Feb 14 '14 at 21:57
  • In that context the user wasn't in *any* UI environment. He was creating *the one, single UI thread* in that case, because there was none without it. In your case you already have another UI thread, so just pump the messages through there, rather than creating another UI thread. – Servy Feb 14 '14 at 21:59
  • @RyanGriffith, based upon your update: are you creating a `WebBrowser` on a secondary thread with the only goal of making *synchronous*, blocking calls from the main thread and waiting until the `WebBrowser` has done loading? Are you OK with the fact the main thread's UI will be frozen while waiting? – noseratio Feb 15 '14 at 04:49
  • I am aware that the main UI will be frozen. This will happen only once during the lifetime of the application (upon initialization). I'm struggling to find another way to do what I'm looking to accomplish. – Ryan Griffith Feb 15 '14 at 19:31

2 Answers2

4

This is by design. The InvokeRequired/BeginInvoke/Invoke members of a control require the Handle property of the control to be created. That is the primary way by which it can figure out to what specific thread to invoke to.

But that did not happen in your code, the Handle is normally only created when you add a control to a parent's Controls collection and the parent was displayed with Show(). In other words, actually created the host window for the browser. None of this happened in your code so Handle is still IntPtr.Zero and InvokeRequired returns false.

This is not actually a problem. The WebBrowser class is special, it is a COM server under the hood. COM handles threading details itself instead of leaving it up to the programmer, very different from the way .NET works. And it will automatically marshal a call to its Navigate() method. This is entirely automatic and doesn't require any help. A hospitable home for the COM server is all that's needed, you made one by creating an STA thread and pumping a message loop with Application.Run(). It is the message loop that COM uses to do the automatic marshaling.

So you can simply call Navigate() on your main thread and nothing goes wrong. The DocumentCompleted event still fires on the helper thread and you can take your merry time tinkering with the Document on that thread.

Not sure why any of this is a problem, it should work all just fine. Maybe you were just mystified about its behavior. If not then this answer could help you with a more universal solution. Don't fear the nay-sayers too much btw, displaying UI on a worker thread is filled with traps but you never actually display any UI here and never create a window.

Community
  • 1
  • 1
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • I'd like to keep all of my events and method calls in the separate (non-ui) thread that I've created. so I can do my tasks in parallel. How can I keep my method invocation on the thread that created the WebBrowser? – Ryan Griffith Feb 15 '14 at 19:39
  • Additionally, you are correct that I can call `Navigate()` on my main thread and it will fire the event in my helper thread, however I want to call `WaitOne()` on an `AutoResetEvent` on my main thread and have my helper thread call `Set()` to let the program continue... but calling `Navigate()` and then `WaitOne()` on my main thread stops everything and my helper thread never has its event fired (which makes sense because `Navigate()` is being called on the very thread I'm also calling `WaitOne()` on). I imagine I need a refactor of my thought process here... there must be a better way. – Ryan Griffith Feb 15 '14 at 20:46
  • Hans, thank you for your response this link helped: http://stackoverflow.com/questions/21680738/how-to-post-messages-to-an-sta-thread-running-a-message-pump/21684059#21684059 – Ryan Griffith Feb 18 '14 at 18:56
4

This answer is based on the updated question and the comments:

Basically I need to wait for my document to complete and the way I would like to do that is with a AutoResetEvent() class which I will trigger upon DocumentComplete being fired... and I need all this stuff to happen in a separate thread.

...

I am aware that the main UI will be frozen. This will happen only once during the lifetime of the application (upon initialization). I'm struggling to find another way to do what I'm looking to accomplish.

I don't think you should be using a separate thread for this. You could disable the UI (e.g. with a modal "Please wait..." dialog) and do the WebBrowser-related work on the main UI thread.

Anyhow, the code below shows how to drive a WebBrowser object on a separate STA thread. It's based on the related answer I recently posted, but is compatible with .NET 4.0. With .NET 4+, you no longer need to use low-level synchronization primitives like AutoResetEvent. Use TaskCompletionSource instead, it allows to propagate the result and possible exceptions to the consumer side of the operation.

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WinFroms_21790151
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();

            this.Load += MainForm_Load;
        }

        void MainForm_Load(object senderLoad, EventArgs eLoad)
        {
            using (var apartment = new MessageLoopApartment())
            {
                // create WebBrowser on a seprate thread with its own message loop
                var webBrowser = apartment.Invoke(() => new WebBrowser());

                // navigate and wait for the result 

                var bodyHtml = apartment.Invoke(() =>
                {
                    WebBrowserDocumentCompletedEventHandler handler = null;
                    var pageLoadedTcs = new TaskCompletionSource<string>();
                    handler = (s, e) =>
                    {
                        try
                        {
                            webBrowser.DocumentCompleted -= handler;
                            pageLoadedTcs.SetResult(webBrowser.Document.Body.InnerHtml);
                        }
                        catch (Exception ex)
                        {
                            pageLoadedTcs.SetException(ex);
                        }
                    };

                    webBrowser.DocumentCompleted += handler;
                    webBrowser.Navigate("http://example.com");

                    // return Task<string>
                    return pageLoadedTcs.Task;
                }).Result;

                MessageBox.Show("body content:\n" + bodyHtml);

                // execute some JavaScript

                var documentHtml = apartment.Invoke(() =>
                {
                    // at least one script element must be present for eval to work
                    var scriptElement = webBrowser.Document.CreateElement("script");
                    webBrowser.Document.Body.AppendChild(scriptElement);

                    // inject and run some script
                    var scriptResult = webBrowser.Document.InvokeScript("eval", new[] { 
                        "(function(){ return document.documentElement.outerHTML; })();" 
                    });

                    return scriptResult.ToString();
                });

                MessageBox.Show("document content:\n" + documentHtml);

                // dispose of webBrowser
                apartment.Invoke(() => webBrowser.Dispose());
                webBrowser = null;
            }
        }

        // MessageLoopApartment
        public class MessageLoopApartment : IDisposable
        {
            Thread _thread; // the STA thread

            TaskScheduler _taskScheduler; // the STA thread's task scheduler

            public TaskScheduler TaskScheduler { get { return _taskScheduler; } }

            /// <summary>MessageLoopApartment constructor</summary>
            public MessageLoopApartment()
            {
                var tcs = new TaskCompletionSource<TaskScheduler>();

                // start an STA thread and gets a task scheduler
                _thread = new Thread(startArg =>
                {
                    EventHandler idleHandler = null;

                    idleHandler = (s, e) =>
                    {
                        // handle Application.Idle just once
                        Application.Idle -= idleHandler;
                        // return the task scheduler
                        tcs.SetResult(TaskScheduler.FromCurrentSynchronizationContext());
                    };

                    // handle Application.Idle just once
                    // to make sure we're inside the message loop
                    // and SynchronizationContext has been correctly installed
                    Application.Idle += idleHandler;
                    Application.Run();
                });

                _thread.SetApartmentState(ApartmentState.STA);
                _thread.IsBackground = true;
                _thread.Start();
                _taskScheduler = tcs.Task.Result;
            }

            /// <summary>shutdown the STA thread</summary>
            public void Dispose()
            {
                if (_taskScheduler != null)
                {
                    var taskScheduler = _taskScheduler;
                    _taskScheduler = null;

                    // execute Application.ExitThread() on the STA thread
                    Task.Factory.StartNew(
                        () => Application.ExitThread(),
                        CancellationToken.None,
                        TaskCreationOptions.None,
                        taskScheduler).Wait();

                    _thread.Join();
                    _thread = null;
                }
            }

            /// <summary>Task.Factory.StartNew wrappers</summary>
            public void Invoke(Action action)
            {
                Task.Factory.StartNew(action, 
                    CancellationToken.None, TaskCreationOptions.None, _taskScheduler).Wait();
            }

            public TResult Invoke<TResult>(Func<TResult> action)
            {
                return Task.Factory.StartNew(action,
                    CancellationToken.None, TaskCreationOptions.None, _taskScheduler).Result;
            }

            public Task Run(Action action, CancellationToken token)
            {
                return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler);
            }

            public Task<TResult> Run<TResult>(Func<TResult> action, CancellationToken token)
            {
                return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler);
            }

            public Task Run(Func<Task> action, CancellationToken token)
            {
                return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler).Unwrap();
            }

            public Task<TResult> Run<TResult>(Func<Task<TResult>> action, CancellationToken token)
            {
                return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler).Unwrap();
            }
        }
    }
}
Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486