在新的Thread()中创建控件时在正确的线程上调用方法

时间:2014-02-14 21:50:26

标签: c# .net multithreading webbrowser-control

我在新的主题()中创建了一个新的 WebBrowser()控件。

我遇到的问题是,当从主线程调用 WebBrowser 的委托时,调用正在主线程< / em>的。我希望这会发生在 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);

    }

}

我尝试过无数种方式调用该方法:

// 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);

更新

让我分解我的最终目标以使事情变得更清楚:我必须使用WebBrowser.Document.InvokeScript()拨打电话,但文件不是加载到我调用WebBrowser.Navigate()之后,然后WebBrowser.DocumentComplete事件触发。基本上,在InvokeScript()发生之后,我无法拨打DocumentComplete的预期电话...我想等待加载文件(阻止我的来电者),这样我就可以拨打InvokeScript和以同步的方式返回我的结果。

基本上我需要等待我的文档完成,我希望这样做的方式是AutoResetEvent()类,我会在DocumentComplete被触发时触发......我需要所有这些事情发生在另一个线程中。

我看到的另一个选项是喜欢这个:

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

2 个答案:

答案 0 :(得分:3)

这是设计的。控件的InvokeRequired / BeginInvoke / Invoke成员需要创建控件的 Handle 属性。这是它可以找出要调用的特定线程的主要方式。

但是在您的代码中没有发生这种情况,Handle通常只在您向父级的Controls集合添加控件并且使用Show()显示父级时创建。换句话说,实际上为浏览器创建了主机窗口。这些都不会发生在您的代码中,因此Handle仍然是IntPtr.Zero,而InvokeRequired返回 false

实际上是一个问题。 WebBrowser类很特别,它是一个引擎盖下的COM服务器。 COM处理线程细节本身而不是将其留给程序员,这与.NET的工作方式截然不同。它会自动封送对其Navigate()方法的调用。这完全是自动的,不需要任何帮助。 COM服务器的热情好客的家就是所需要的,你通过创建一个STA线程并使用Application.Run()抽取一个消息循环来创建一个。它是COM用于执行自动编组的消息循环。

所以你可以简单地在主线程上调用Navigate(),没有任何问题。 DocumentCompleted事件仍然会在帮助程序线程上触发,您可以快乐地修改该线程上的Document。

不确定为什么这是一个问题,它应该可以正常工作。也许你只是对它的行为感到困惑。如果没有,那么this answer可以帮助您获得更通用的解决方案。不要太害怕nay-sayers太多btw,在工作线程上显示UI充满了陷阱,但你实际上从未在这里显示任何UI而且从不创建窗口。

答案 1 :(得分:3)

此答案基于更新的问题和评论:

  

基本上我需要等待我的文件完成和我的方式   我想这样做是使用AutoResetEvent()类   触发DocumentComplete被触发......我需要所有这些东西   发生在一个单独的线程中。

     

...

     

我知道主UI会被冻结。这只会发生一次   在应用程序的生命周期内(初始化时)。我   努力寻找另一种方法去做我想要完成的事情。

我认为你不应该为此使用单独的线程。您可以禁用UI(例如,使用模式“请稍候...”对话框)并在主UI线程上执行与WebBrowser相关的工作。

无论如何,下面的代码显示了如何在单独的STA线程上驱动WebBrowser对象。它基于我最近发布的the related answer,但与.NET 4.0兼容。使用.NET 4+,您不再需要使用AutoResetEvent等低级同步原语。使用TaskCompletionSource代替,它允许将结果和可能的异常传播到操作的消费者端。

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