Controllo WebBrowser in un nuovo thread


84

Ho un elenco di Uri che desidero "fare clic" Per ottenere ciò, sto cercando di creare un nuovo controllo del browser Web per Uri. Creo un nuovo thread per Uri. Il problema che ho è la fine del thread prima del documento è completamente caricato, quindi non riesco mai a utilizzare l'evento DocumentComplete, come posso superarlo?

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

Risposte:


152

Devi creare un thread STA che pompa un loop di messaggi. Questo è l'unico ambiente ospitale per un componente ActiveX come WebBrowser. Altrimenti non otterrai l'evento DocumentCompleted. Alcuni esempi di codice:

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
    }
}

8
Sì! Basta aggiungere System.Windows.Forms. Ha salvato anche la mia giornata. Grazie
zee

4
Sto cercando di adattare questo codice alla mia situazione. Devo mantenere WebBrowservivo l' oggetto (per salvare stato / cookie ecc.) Ed eseguire più Navigate()chiamate nel tempo. Ma non sono sicuro di dove effettuare la mia Application.Run()chiamata, perché blocca l'esecuzione di ulteriore codice. Qualche indizio?
dotNET

Puoi chiamare Application.Exit();per far Application.Run()tornare.
Mike de Klerk

26

Ecco come organizzare un loop di messaggi su un thread non UI, per eseguire attività asincrone come l' WebBrowserautomazione. Viene utilizzato async/awaitper fornire il flusso di codice lineare conveniente e carica un set di pagine Web in un ciclo. Il codice è un'app per console pronta per essere eseguita parzialmente basata su questo eccellente post .

Risposte correlate:

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

1
Grazie per quella risposta brillante e istruttiva! È esattamente quello che stavo cercando. Tuttavia sembra che tu abbia (intenzionalmente?) Smarrito l'istruzione Dispose ().
wodzu

@ Paweł, hai ragione, quel codice non è stato nemmeno compilato :) Penso che abbia incollato una versione sbagliata, ora corretta. Grazie per aver notato questo. Si consiglia di controllare un approccio più generico: stackoverflow.com/a/22262976/1768303
noseratio

Ho provato a eseguire questo codice, tuttavia si blocca task.Wait();. Sto facendo qualcosa di sbagliato?
0014

1
Ciao, forse potresti aiutarmi con questo: stackoverflow.com/questions/41533997/… - il metodo funziona bene, ma se Form è stato istanziato prima di MessageLoopWorker, smette di funzionare.
Alex Netkachov

3

Dalla mia esperienza in passato al browser web non piace operare al di fuori del thread principale dell'applicazione.

Prova invece a utilizzare httpwebrequests, puoi impostarli come asincroni e creare un gestore per la risposta per sapere quando ha successo:

come-usare-httpwebrequest-net-in modo asincrono


Il mio problema è questo. L'URI su cui si è fatto clic richiedeva l'accesso al sito. Non posso farlo con WebRequest. Utilizzando il browser web utilizza già la cache di IE, quindi i siti hanno effettuato l'accesso. C'è un modo per aggirare questo? I collegamenti coinvolgono Facebook. Quindi posso accedere a Facebook e fare clic sul collegamento con webwrequest?
Art W

@ArtW So che questo è un vecchio commento, ma le persone possono probabilmente risolverlo impostandowebRequest.Credentials = CredentialsCache.DefaultCredentials;
vapcguy

@vapcguy Se è un'API, allora sì, ma se è un sito web con elementi HTML per il login, allora dovrà usare i cookie di IE o la cache, altrimenti il ​​client non sa cosa fare con la Credentialsproprietà dell'oggetto e come riempire l'HTML.
ColinM

@ColinM Il contesto di cui parla l'intera pagina è l'utilizzo dell'oggetto HttpWebRequest e C # .NET, non semplici elementi HTML e form che vengono pubblicati, come si potrebbe fare con JavaScript / AJAX. Ma a prescindere, hai un ricevitore. E per l'accesso dovresti usare l'autenticazione di Windows e IIS lo gestisce automaticamente, comunque. Se è necessario testarli manualmente, è possibile utilizzarli WindowsIdentity.GetCurrent().Namedopo aver implementato la rappresentazione e testarli con una ricerca AD, se lo si desidera. Non sono sicuro di come verranno utilizzati i cookie e la cache per tutto ciò.
vapcguy

@vapcguy La domanda di cui si sta parlando WebBrowserindicherebbe che le pagine HTML vengono caricate, OP ha persino detto che WebRequestnon otterrà ciò che vuole, quindi se un sito Web si aspetta l'input HTML per il login, l'impostazione Credentialsdell'oggetto non funzionerà. Inoltre, come dice OP, i siti includono Facebook; L'autenticazione di Windows non funzionerà su questo.
ColinM

0

Una soluzione semplice in cui si verifica il funzionamento simultaneo di più WebBrowser

  1. Crea una nuova applicazione Windows Form
  2. Posiziona il pulsante denominato button1
  3. Posiziona la casella di testo denominata textBox1
  4. Imposta le proprietà del campo di testo: Multiline true e ScrollBars Both
  5. Scrivi il seguente gestore di clic 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);
    
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.