So as the title suggests, I'm trying to use WebBrowser control in a class library. I've gone through several SO questions like this excellent post, but the unique thing in my situation is that the WebBrowser object must remain alive for the life of application and keep its state/cookies across different calls that the library clients will make from time to time.
I have confirmed that WebBrowser control does not do navigation unless the thread it was created on contains a message pump. But as soon as I introduce a message pump, the code blocks at Application.Run()
call and no further events are generated. Any help will really be appricated.
If I understood the question correctly, you need to run an instance of WebBrowser
control for the lifetime of your library, and keep it alive and independent on a dedicated STA thread with its own WinForms message loop.
The code below shows how it can possibly be done, using a helper class called MessageLoopApartment
. Note how the WebBrowser
gets created and manipulated on a separate thread.
The Task Parallel Library is very handy in getting the synchronization job done. The tasks scheduled on the STA thread with MessageLoopApartment.Run
can be waited synchronously with task.Wait()
or asynchronously with await task
, results and exceptions are propagated from the STA thread via Task.Result
/Task.Execption
, exceptions are re-thrown on the caller's stack frame.
The implementation of MessageLoopApartment
is compatible with NET 4.0, it doesn't use any .NET 4.5 features. The client code (the WebBrowser
navigation test) optionally uses async/await
, which may require Microsoft.Bcl.Async
to target .NET 4.0. TPL and async/await
greatly simplify manipulating objects created inside the MessageLoopApartment
's thread, like _webBrowser
.
The navigation test is performed inside MainForm_Load
, but the lifetime of _webBrowser
and _apartment
is not limited by the boundaries of that single call. Both gets destroyed inside MainForm_FormClosed
. The test app is a WinForms app, but it may as well be a console app or anything else.
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WinForms_21772632
{
// https://stackoverflow.com/q/21772632/1768303
public partial class MainForm : Form
{
MessageLoopApartment _apartment;
// _webBrowser is created on a separate thread,
// with MessageLoopApartment.Run
WebBrowser _webBrowser;
// MainForm
public MainForm()
{
InitializeComponent();
// create an independent STA thread
_apartment = new MessageLoopApartment();
// create a WebBrowser on that STA thread
_webBrowser = _apartment.Run(
() => new WebBrowser(),
CancellationToken.None).Result;
this.Load += MainForm_Load;
this.FormClosed += MainForm_FormClosed;
}
// navigation test
async void MainForm_Load(object senderLoad, EventArgs eLoad)
{
// navigate
var cts = new CancellationTokenSource(10000); // cancel in 10s
var url = "http://example.com";
var html = await _apartment.Run(async () =>
{
WebBrowserDocumentCompletedEventHandler handler = null;
var navigateTcs = new TaskCompletionSource<bool>();
handler = (s, e) =>
navigateTcs.TrySetResult(true);
_webBrowser.DocumentCompleted += handler;
try
{
using (cts.Token.Register(() => navigateTcs.TrySetCanceled()))
{
_webBrowser.Navigate(url);
await navigateTcs.Task;
return _webBrowser.Document.Body.OuterHtml;
}
}
finally
{
_webBrowser.DocumentCompleted -= handler;
}
},
cts.Token);
// show the HTML of the downloaded page
MessageBox.Show(html);
}
void MainForm_FormClosed(object sender, FormClosedEventArgs e)
{
// destroy the WebBrowser
_apartment.Run(
() => _webBrowser.Dispose(),
CancellationToken.None).Wait();
// shut down the appartment
_apartment.Dispose();
}
}
/// <summary>MessageLoopApartment</summary>
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>A wrapper around Task.Factory.StartNew</summary>
public Task Run(Action action, CancellationToken token)
{
return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler);
}
/// <summary>A wrapper around Task.Factory.StartNew to run lambdas with a result</summary>
public Task<TResult> Run<TResult>(Func<TResult> action, CancellationToken token)
{
return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler);
}
/// <summary>A wrapper around Task.Factory.StartNew to run async lambdas</summary>
public Task Run(Func<Task> action, CancellationToken token)
{
return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler).Unwrap();
}
/// <summary>A wrapper around Task.Factory.StartNew to run async lambdas with a result</summary>
public Task<TResult> Run<TResult>(Func<Task<TResult>> action, CancellationToken token)
{
return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler).Unwrap();
}
}
}