我们有一个 WPF 应用程序,它在 <WebBrowser/>
控件中加载一些内容,然后根据加载的内容进行一些调用。使用正确的模拟,我们认为我们可以在无显示单元测试(在本例中为 NUnit)中进行测试。但是 WebBrowser
控件不想很好地发挥作用。
问题是我们从未收到 LoadCompleted
或 Navigated
事件。显然这是因为在实际呈现 (see this MSDN thread) 之前,网页永远不会“加载”。我们确实收到了 Navigating
事件,但这对我们的目的来说还为时过早。
那么有没有办法让 WebBrowser
控件“完全”工作,即使它没有输出显示?
这是测试用例的简化版本:
[TestFixture, RequiresSTA]
class TestIsoView
{
[Test] public void PageLoadsAtAll()
{
Console.WriteLine("I'm a lumberjack and I'm OK");
WebBrowser wb = new WebBrowser();
// An AutoResetEvent allows us to synchronously wait for an event to occur.
AutoResetEvent autoResetEvent = new AutoResetEvent(false);
//wb.LoadCompleted += delegate // LoadCompleted is never received
wb.Navigated += delegate // Navigated is never received
//wb.Navigating += delegate // Navigating *is* received
{
// We never get here unless we wait on wb.Navigating
Console.WriteLine("The document loaded!!");
autoResetEvent.Set();
};
Console.WriteLine("Registered signal handler", "Navigating");
wb.NavigateToString("Here be dramas");
Console.WriteLine("Asyncronous Navigations started! Waiting for A.R.E.");
autoResetEvent.WaitOne();
// TEST HANGS BEFORE REACHING HERE.
Console.WriteLine("Got it!");
}
}
最佳答案
为此,您需要使用消息循环来分离 STA 线程。您将在该线程上创建一个 WebBrowser
实例并抑制脚本错误。请注意,WPF WebBrowser
对象需要一个实时主机窗 Eloquent 能运行。这就是它与 WinForms WebBrowser
的不同之处。
以下是如何做到这一点的示例:
static async Task<string> RunWpfWebBrowserAsync(string url)
{
// return the result via Task
var resultTcs = new TaskCompletionSource<string>();
// the main WPF WebBrowser driving logic
// to be executed on an STA thread
Action startup = async () =>
{
try
{
// create host window
var hostWindow = new Window();
hostWindow.ShowActivated = false;
hostWindow.ShowInTaskbar = false;
hostWindow.Visibility = Visibility.Hidden;
hostWindow.Show();
// create a WPF WebBrowser instance
var wb = new WebBrowser();
hostWindow.Content = wb;
// suppress script errors: https://stackoverflow.com/a/18289217
// touching wb.Document makes sure the underlying ActiveX has been created
dynamic document = wb.Document;
dynamic activeX = wb.GetType().InvokeMember("ActiveXInstance",
BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.NonPublic,
null, wb, new object [] { });
activeX.Silent = true;
// navigate and handle LoadCompleted
var navigationTcs = new TaskCompletionSource<bool>();
wb.LoadCompleted += (s, e) =>
navigationTcs.TrySetResult(true);
wb.Navigate(url);
await navigationTcs.Task;
// do the WebBrowser automation
document = wb.Document;
// ...
// return the content (for example)
string content = document.body.outerHTML;
resultTcs.SetResult(content);
}
catch (Exception ex)
{
// propogate exceptions to the caller of RunWpfWebBrowserAsync
resultTcs.SetException(ex);
}
// end the tread: the message loop inside Dispatcher.Run() will exit
Dispatcher.ExitAllFrames();
};
// thread procedure
ThreadStart threadStart = () =>
{
// post the startup callback
// it will be invoked when the message loop pumps
Dispatcher.CurrentDispatcher.BeginInvoke(startup);
// run the WPF Dispatcher message loop
Dispatcher.Run();
Debug.Assert(true);
};
// start and run the STA thread
var thread = new Thread(threadStart);
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
try
{
// use Task.ConfigureAwait(false) to avoid deadlock on a UI thread
// if the caller does a blocking call, i.e.:
// "RunWpfWebBrowserAsync(url).Wait()" or
// "RunWpfWebBrowserAsync(url).Result"
return await resultTcs.Task.ConfigureAwait(false);
}
finally
{
// make sure the thread has fully come to an end
thread.Join();
}
}
用法:
// blocking call
string content = RunWpfWebBrowserAsync("http://www.example.com").Result;
// async call
string content = await RunWpfWebBrowserAsync("http://www.example.org")
您也可以尝试直接在
threadStart
线程上运行 NUnit
lambda,而无需实际创建新线程。这样,NUnit 线程将运行 Dispatcher
消息循环。我对 NUnit 不太熟悉,无法预测它是否有效。如果您不想创建主机窗口,请考虑改用 WinForms
WebBrowser
。我从控制台应用程序发布了一个类似的 self-contained example 。关于c# - 在 headless 单元测试中运行时如何使 WebBrowser 完成导航?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/21288489/