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

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

我遇到的问题是,当从主线程调用我的WebBrowser的委托时,调用正在主线程上进行 。 我希望这会发生在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.Navigate()之后才会加载Document WebBrowser.DocumentComplete事件触发。 基本上,在DocumentComplete触发之前,我无法调用InvokeScript() …我想等待加载文档(阻止我的调用者),这样我就可以调用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; } 

这是设计的。 控件的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。

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

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

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

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

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

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

 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(); 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 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; } } /// MessageLoopApartment constructor public MessageLoopApartment() { var tcs = new TaskCompletionSource(); // 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; } /// shutdown the STA thread 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; } } /// Task.Factory.StartNew wrappers public void Invoke(Action action) { Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.None, _taskScheduler).Wait(); } public TResult Invoke(Func 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 Run(Func action, CancellationToken token) { return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler); } public Task Run(Func action, CancellationToken token) { return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler).Unwrap(); } public Task Run(Func> action, CancellationToken token) { return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler).Unwrap(); } } } }