我们有一个 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")
您也可以尝试直接在您的 NUnit
线程上运行 threadStart
lambda,而不实际创建新线程。这样,NUnit 线程将运行 Dispatcher
消息循环。我对 NUnit 不够熟悉,无法预测它是否有效。
如果您不想创建宿主窗口,请考虑使用 WinForms WebBrowser
反而。我发布了一个类似的 self-contained example从控制台应用程序执行此操作。
关于c# - 在 headless 单元测试中运行时,如何使 WebBrowser 完成导航?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21288489/