Controle WebBrowser em um novo tópico

84

Eu tenho uma lista de Uri's que desejo "clicar" Para conseguir isso, estou tentando criar um novo controle de navegador da Web por Uri. Crio um novo thread por Uri. O problema que estou tendo é o fim do thread antes do documento está totalmente carregado, então nunca consigo fazer uso do evento DocumentComplete. Como posso superar isso?

var item = new ParameterizedThreadStart(ClicIt.Click); 
var thread = new Thread(item) {Name = "ClickThread"}; 
thread.Start(uriItem);

public static void Click(object o)
{
    var url = ((UriItem)o);
    Console.WriteLine(@"Clicking: " + url.Link);
    var clicker = new WebBrowser { ScriptErrorsSuppressed = true };
    clicker.DocumentCompleted += BrowseComplete;
    if (String.IsNullOrEmpty(url.Link)) return;
    if (url.Link.Equals("about:blank")) return;
    if (!url.Link.StartsWith("http://") && !url.Link.StartsWith("https://"))
        url.Link = "http://" + url.Link;
    clicker.Navigate(url.Link);
}
Art W
fonte

Respostas:

151

Você deve criar um thread STA que bombeia um loop de mensagem. Esse é o único ambiente hospitaleiro para um componente ActiveX como o WebBrowser. Você não obterá o evento DocumentCompleted de outra forma. Alguns exemplos de código:

private void runBrowserThread(Uri url) {
    var th = new Thread(() => {
        var br = new WebBrowser();
        br.DocumentCompleted += browser_DocumentCompleted;
        br.Navigate(url);
        Application.Run();
    });
    th.SetApartmentState(ApartmentState.STA);
    th.Start();
}

void browser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e) {
    var br = sender as WebBrowser;
    if (br.Url == e.Url) {
        Console.WriteLine("Natigated to {0}", e.Url);
        Application.ExitThread();   // Stops the thread
    }
}
Hans Passant
fonte
8
Sim! Basta adicionar System.Windows.Forms. Salvou meu dia também. Obrigado
zee
4
Estou tentando adaptar esse código à minha situação. Tenho que manter o WebBrowserobjeto ativo (para salvar o estado / cookies etc.) e realizar várias Navigate()chamadas ao longo do tempo. Mas não tenho certeza de onde fazer minha Application.Run()chamada, porque ela bloqueia a execução de mais códigos. Alguma pista?
dotNET
Você pode ligar Application.Exit();para deixar Application.Run()voltar.
Mike de Klerk
26

Aqui está como organizar um loop de mensagem em um thread não-UI, para executar tarefas assíncronas como WebBrowserautomação. Ele usa async/awaitpara fornecer o fluxo de código linear conveniente e carrega um conjunto de páginas da web em um loop. O código é um aplicativo de console pronto para rodar parcialmente baseado neste excelente post .

Respostas relacionadas:

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace ConsoleApplicationWebBrowser
{
    // by Noseratio - https://stackoverflow.com/users/1768303/noseratio
    class Program
    {
        // Entry Point of the console app
        static void Main(string[] args)
        {
            try
            {
                // download each page and dump the content
                var task = MessageLoopWorker.Run(DoWorkAsync,
                    "http://www.example.com", "http://www.example.net", "http://www.example.org");
                task.Wait();
                Console.WriteLine("DoWorkAsync completed.");
            }
            catch (Exception ex)
            {
                Console.WriteLine("DoWorkAsync failed: " + ex.Message);
            }

            Console.WriteLine("Press Enter to exit.");
            Console.ReadLine();
        }

        // navigate WebBrowser to the list of urls in a loop
        static async Task<object> DoWorkAsync(object[] args)
        {
            Console.WriteLine("Start working.");

            using (var wb = new WebBrowser())
            {
                wb.ScriptErrorsSuppressed = true;

                TaskCompletionSource<bool> tcs = null;
                WebBrowserDocumentCompletedEventHandler documentCompletedHandler = (s, e) =>
                    tcs.TrySetResult(true);

                // navigate to each URL in the list
                foreach (var url in args)
                {
                    tcs = new TaskCompletionSource<bool>();
                    wb.DocumentCompleted += documentCompletedHandler;
                    try
                    {
                        wb.Navigate(url.ToString());
                        // await for DocumentCompleted
                        await tcs.Task;
                    }
                    finally
                    {
                        wb.DocumentCompleted -= documentCompletedHandler;
                    }
                    // the DOM is ready
                    Console.WriteLine(url.ToString());
                    Console.WriteLine(wb.Document.Body.OuterHtml);
                }
            }

            Console.WriteLine("End working.");
            return null;
        }

    }

    // a helper class to start the message loop and execute an asynchronous task
    public static class MessageLoopWorker
    {
        public static async Task<object> Run(Func<object[], Task<object>> worker, params object[] args)
        {
            var tcs = new TaskCompletionSource<object>();

            var thread = new Thread(() =>
            {
                EventHandler idleHandler = null;

                idleHandler = async (s, e) =>
                {
                    // handle Application.Idle just once
                    Application.Idle -= idleHandler;

                    // return to the message loop
                    await Task.Yield();

                    // and continue asynchronously
                    // propogate the result or exception
                    try
                    {
                        var result = await worker(args);
                        tcs.SetResult(result);
                    }
                    catch (Exception ex)
                    {
                        tcs.SetException(ex);
                    }

                    // signal to exit the message loop
                    // Application.Run will exit at this point
                    Application.ExitThread();
                };

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

            // set STA model for the new thread
            thread.SetApartmentState(ApartmentState.STA);

            // start the thread and await for the task
            thread.Start();
            try
            {
                return await tcs.Task;
            }
            finally
            {
                thread.Join();
            }
        }
    }
}
noseratio
fonte
1
Obrigado por essa resposta brilhante e informativa! É exatamente o que eu estava procurando. No entanto, parece que você (intencionalmente?) Perdeu a instrução Dispose ().
wodzu
@ Paweł, você está certo, aquele código nem compilou :) Acho que colei uma versão errada, agora corrigida. Obrigado por perceber isso. Você pode querer verificar uma abordagem mais genérica: stackoverflow.com/a/22262976/1768303
noseratio
Tentei executar este código, mas ele travou task.Wait();. Eu estou fazendo algo errado ?
0014
1
Olá, talvez você possa me ajudar com este: stackoverflow.com/questions/41533997/… - o método funciona bem, mas se o Form foi instanciado antes do MessageLoopWorker, ele para de funcionar.
Alex Netkachov
3

Pela minha experiência anterior, o navegador da web não gosta de operar fora do thread principal do aplicativo.

Tente usar httpwebrequests, você pode defini-los como assíncronos e criar um manipulador para a resposta para saber quando ela é bem-sucedida:

how-to-use-httpwebrequest-net-asynchronously

barc0de
fonte
Meu problema com isso é este. O Uri clicado exigia que o site estivesse logado. Não consigo fazer isso com WebRequest. Ao usar o WebBrowser, ele já usa o cache do IE, então os sites se logam. Existe uma maneira de contornar isso? Os links envolvem o Facebook. Posso entrar no Facebook e clicar no link com webwrequest?
Art W
@ArtW Eu sei que este é um comentário antigo, mas as pessoas provavelmente podem resolver isso definindowebRequest.Credentials = CredentialsCache.DefaultCredentials;
vapcguy
@vapcguy Se for uma API, sim, mas se for um site com elementos HTML para fazer o login, ele precisará usar cookies ou cache do IE, caso contrário o cliente não sabe o que fazer com a Credentialspropriedade do objeto e como preencher o HTML.
ColinM
@ColinM O contexto sobre o qual toda esta página está falando está usando o objeto HttpWebRequest e C # .NET, não HTML simples e elementos de formulário sendo postados, como você faria com JavaScript / AJAX. Mas independentemente, você tem um receptor. E para fazer logon, você deve usar a Autenticação do Windows e o IIS trata disso automaticamente, de qualquer maneira. Se você precisar testá-los manualmente, poderá usá-los WindowsIdentity.GetCurrent().Nameapós implementar a representação e testá-los em uma pesquisa do AD, se desejar. Não tenho certeza de como os cookies e o cache seriam usados ​​para isso.
vapcguy
@vapcguy A questão é falar sobre o WebBrowserque indicaria que as páginas HTML estão sendo carregadas, OP até disse que WebRequestnão vai conseguir o que deseja, portanto, se um site espera entrada de HTML para o login, a configuração do Credentialsobjeto não funcionará. Além disso, como diz OP, os sites incluem Facebook; A autenticação do Windows não funcionará nisso.
ColinM
0

Uma solução simples em que ocorre o funcionamento simultâneo de vários WebBrowsers

  1. Crie um novo aplicativo Windows Forms
  2. Coloque o botão denominado button1
  3. Coloque a caixa de texto chamada textBox1
  4. Definir propriedades do campo de texto: Multiline true e ScrollBars Ambos
  5. Escreva o seguinte manipulador de clique em button1:

    textBox1.Clear();
    textBox1.AppendText(DateTime.Now.ToString() + Environment.NewLine);
    int completed_count = 0;
    int count = 10;
    for (int i = 0; i < count; i++)
    {
        int tmp = i;
        this.BeginInvoke(new Action(() =>
        {
            var wb = new WebBrowser();
            wb.ScriptErrorsSuppressed = true;
            wb.DocumentCompleted += (cur_sender, cur_e) =>
            {
                var cur_wb = cur_sender as WebBrowser;
                if (cur_wb.Url == cur_e.Url)
                {
                    textBox1.AppendText("Task " + tmp + ", navigated to " + cur_e.Url + Environment.NewLine);
                    completed_count++;
                }
            };
            wb.Navigate("/programming/4269800/webbrowser-control-in-a-new-thread");
        }
        ));
    }
    
    while (completed_count != count)
    {
        Application.DoEvents();
        Thread.Sleep(10);
    }
    textBox1.AppendText("All completed" + Environment.NewLine);
    
Ramil Shavaleev
fonte