Control WebBrowser en un nuevo hilo

84

Tengo una lista de Uri en los que quiero "hacer clic". Para lograr esto, estoy tratando de crear un nuevo control de navegador web por Uri. Creo un nuevo hilo por Uri. El problema que tengo es el final del hilo antes del documento está completamente cargado, por lo que nunca puedo utilizar el evento DocumentComplete. ¿Cómo puedo superar esto?

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);
}
Arte W
fuente

Respuestas:

152

Tienes que crear un hilo STA que bombee un bucle de mensajes. Ese es el único entorno acogedor para un componente ActiveX como WebBrowser. De lo contrario, no obtendrá el evento DocumentCompleted. Algún código de muestra:

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
fuente
8
¡Si! Simplemente agregue System.Windows.Forms. También me salvó el día. Gracias
zee
4
Estoy tratando de adaptar este código a mi situación. Tengo que mantener WebBrowservivo el objeto (para guardar estado / cookies, etc.) y realizar múltiples Navigate()llamadas a lo largo del tiempo. Pero no estoy seguro de dónde realizar mi Application.Run()llamada, porque bloquea la ejecución de más código. ¿Alguna pista?
dotNET
Puedes llamar Application.Exit();para dejar Application.Run()volver.
Mike de Klerk
26

A continuación, se explica cómo organizar un bucle de mensajes en un hilo que no sea de interfaz de usuario para ejecutar tareas asincrónicas como la WebBrowserautomatización. Se utiliza async/awaitpara proporcionar el flujo de código lineal conveniente y carga un conjunto de páginas web en un bucle. El código es una aplicación de consola lista para ejecutar que se basa parcialmente en esta excelente publicación .

Respuestas 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();
            }
        }
    }
}
ratio nasal
fuente
1
¡Gracias por esa brillante e informativa respuesta! Es exactamente lo que estaba buscando. Sin embargo, parece que (¿intencionalmente?) Extravió la declaración Dispose ().
wodzu
@ Paweł, tienes razón, ese código ni siquiera se compiló :) Creo que pegué una versión incorrecta, ahora arreglada. Gracias por ver esto. Es posible que desee verificar un enfoque más genérico: stackoverflow.com/a/22262976/1768303
noseratio
Intenté ejecutar este código, sin embargo, se atasca task.Wait();. Estoy haciendo algo mal ?
0014
1
Hola, tal vez puedas ayudarme con esto: stackoverflow.com/questions/41533997/… - el método funciona bien, pero si se creó una instancia de Form antes de MessageLoopWorker, deja de funcionar.
Alex Netkachov
3

Según mi experiencia en el pasado, al navegador web no le gusta operar fuera del hilo principal de la aplicación.

Intente usar httpwebrequests en su lugar, puede configurarlos como asíncronos y crear un controlador para que la respuesta sepa cuándo es exitosa:

cómo-utilizar-httpwebrequest-net-asincrónicamente

barc0de
fuente
Mi problema con eso es este. El Uri en el que se hace clic requiere que el sitio esté conectado. No puedo lograr esto con WebRequest. Al usar WebBrowser, ya usa la caché de IE, por lo que los sitios iniciaron sesión. ¿Hay alguna forma de evitar eso? Los enlaces involucran a Facebook. Entonces, ¿puedo iniciar sesión en Facebook y hacer clic en el enlace con webwrequest?
Art W
@ArtW Sé que este es un comentario antiguo, pero la gente probablemente pueda resolverlo configurandowebRequest.Credentials = CredentialsCache.DefaultCredentials;
vapcguy
@vapcguy Si es una API, entonces sí, pero si es un sitio web con elementos HTML para iniciar sesión, entonces necesitará usar cookies de IE o caché; de lo contrario, el cliente no sabe qué hacer con la Credentialspropiedad del objeto y cómo completar el HTML.
ColinM
@ColinM El contexto del que habla toda esta página es el uso del objeto HttpWebRequest y C # .NET, no el HTML simple y los elementos de formulario que se publican, como lo haría con JavaScript / AJAX. Pero independientemente, tienes un receptor. Y para el inicio de sesión, debe utilizar la autenticación de Windows e IIS maneja esto automáticamente, de todos modos. Si necesita probarlos manualmente, puede usarlos WindowsIdentity.GetCurrent().Namedespués de implementar la suplantación y probarlos con una búsqueda de AD, si lo desea. No estoy seguro de cómo se usarían las cookies y el caché para nada de eso.
vapcguy
@vapcguy La pregunta es WebBrowserqué indicaría que se están cargando páginas HTML, OP incluso ha dicho que WebRequestno logrará lo que quiere, por lo tanto, si un sitio web espera una entrada HTML para el inicio de sesión, la configuración del Credentialsobjeto no funcionará. Además, como dice OP, los sitios incluyen Facebook; La autenticación de Windows no funcionará en esto.
ColinM
0

Una solución sencilla en la que se produce el funcionamiento simultáneo de varios WebBrowsers

  1. Cree una nueva aplicación de Windows Forms
  2. Coloque el botón llamado button1
  3. Coloque el cuadro de texto llamado textBox1
  4. Establecer propiedades del campo de texto: Multiline true y ScrollBars Ambos
  5. Escriba el siguiente controlador de clic de 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
fuente