Элемент управления WebBrowser в библиотеке классов

Итак, как следует из названия, я пытаюсь использовать элемент управления WebBrowser в библиотеке классов. Я рассмотрел несколько вопросов SO, таких как этот отличный пост, но уникальный Дело в том, что в моей ситуации объект WebBrowser должен оставаться живым на протяжении всего срока службы приложения и сохранять свое состояние/файлы cookie при различных вызовах, которые время от времени будут совершать клиенты библиотеки.

Я подтвердил, что элемент управления WebBrowser не выполняет навигацию, если только поток, в котором он был создан, не содержит насос сообщений. Но как только я ввожу насос сообщений, код блокируется при вызове Application.Run(), и дальнейшие события не генерируются. Любая помощь действительно будет оценена.


person dotNET    schedule 14.02.2014    source источник
comment
Вы найдете более общий класс, поддерживающий браузер, в этом ответе.   -  person Hans Passant    schedule 14.02.2014
comment
@HansPassant: Большое спасибо, Ганс. Я даю ему идти прямо сейчас. Кажется, лучший кандидат.   -  person dotNET    schedule 14.02.2014
comment
Для любого будущего читателя ссылка на ответ, предоставленная Гансом выше, исключительно хороша. У него также есть дополнительное преимущество, заключающееся в том, что он не зависит от функций .NET 4.5, таких как async. Спасибо Ганс.   -  person dotNET    schedule 14.02.2014


Ответы (1)


Если я правильно понял вопрос, вам нужно запустить экземпляр элемента управления WebBrowser на время жизни вашей библиотеки и сохранить его живым и независимым от выделенного потока STA с собственным циклом сообщений WinForms.

В приведенном ниже коде показано, как это можно сделать, используя вспомогательный класс с именем MessageLoopApartment. Обратите внимание, как WebBrowser создается и обрабатывается в отдельном потоке.

Параллельная библиотека задач очень удобна для выполнения задачи синхронизации. Задачи, запланированные в потоке STA с помощью MessageLoopApartment.Run, могут ожидаться синхронно с task.Wait() или асинхронно с await task, результаты и исключения распространяются из потока STA через Task.Result/Task.Execption, исключения повторно выбрасываются в кадр стека вызывающей стороны.

Реализация MessageLoopApartment совместима с NET 4.0 и не использует никаких функций .NET 4.5. Клиентский код (тест навигации WebBrowser) необязательно использует async/await, для чего может потребоваться Microsoft.Bcl.Async для .NET 4.0. TPL и async/await значительно упрощают управление объектами, созданными внутри потока MessageLoopApartment, например _webBrowser.

Навигационный тест выполняется внутри MainForm_Load, но время жизни _webBrowser и _apartment не ограничивается границами этого единственного вызова. Оба уничтожаются внутри MainForm_FormClosed. Тестовое приложение — это приложение WinForms, но оно также может быть консольным приложением или чем-то еще.

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();
        }

    }
}
person noseratio    schedule 14.02.2014
comment
Привет @Noseratio, есть ли ошибка в вашем предложении «наконец»? Может быть: _webBrowser.DocumentCompleted -= handler; Я был бы очень признателен, если бы вы могли поделиться причиной, если строка кода верна. - person Ayorus; 27.10.2016
comment
@Ayorus, действительно есть ошибка, она должна быть _webBrowser.DocumentCompleted -= handler. - person noseratio; 27.10.2016
comment
Привет @Noseratio, я использую этот фрагмент кода последние 2 недели. Однако после реализации появилась следующая ошибка: Appcrash mshtml.dll Fault Module Version: 11.0.9600.18500 Exception Code: c00000fd Я искал некоторые упоминания об этом, и оказалось, что это связано с iExplorer. Вы когда-нибудь сталкивались с этим? Есть ли у вас какие-либо рекомендации? - person Ayorus; 07.11.2016
comment
@Ayorus, нет, я этого не видел, но я никогда не тестировал этот код под нагрузкой. Мне кажется, что это ошибка IE, возможно, утечка памяти. Вы можете попробовать использовать несколько экземпляров MessageLoopApartment и переработать их. Это должно правильно убивать WebBrowser экземпляров. - person noseratio; 08.11.2016