我在新的主题()中创建了一个新的 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;
}
答案 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();
}
}
}
}