Calling method on correct thread when control is created in new Thread()

ぃ、小莉子 提交于 2019-11-28 11:20:20

问题


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

回答1:


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.




回答2:


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


来源:https://stackoverflow.com/questions/21790151/calling-method-on-correct-thread-when-control-is-created-in-new-thread

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!