WebBrowser control in a class library
Asked Answered
S

1

2

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.

Sadiras answered 14/2, 2014 at 6:50 Comment(3)
You'll find a more generic class that supports a browser in this answer.Yvor
@HansPassant: Thanks a ton Hans. I'm giving it a go right now. Appears to be a better candidate.Sadiras
For any future reader, the answer link provided by Hans above is exceptionally good. It also has the added advantage that it doesn't depend on .NET 4.5 features such as async. Thanks Hans.Sadiras
A
3

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://mcmap.net/q/24391/-webbrowser-control-in-a-class-library/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();
        }

    }
}
Azine answered 14/2, 2014 at 9:22 Comment(4)
Hi @Noseratio, Is there a mistake in your 'finally' clause? Maybe could be: _webBrowser.DocumentCompleted -= handler; I would really appreciate if you can share the reason, in case the code line is correctEnfield
@Ayorus, indeed there's a bug, it should be _webBrowser.DocumentCompleted -= handler.Azine
Hi @Noseratio, I have been using this piece of code for the last 2 weeks. However, After implemented the following error has appeared: Appcrash mshtml.dll Fault Module Version: 11.0.9600.18500 Exception Code: c00000fd I looked for some references about it and it turns out it is related to iExplorer. Have you ever face it? Do you have any recomendation?Enfield
@Ayorus, no I haven't seen this but then I haven't ever stress-tested this code. Looks to me like an IE bug, a memory leak perhaps. You may try using multiple MessageLoopApartment instances and recycling them. That should be properly killing WebBrowser instances.Azine

© 2022 - 2024 — McMap. All rights reserved.