WebBrowser-Steuerelement in einem neuen Thread

84

Ich habe eine Liste von Uris, auf die ich "klicken" möchte. Um dies zu erreichen, versuche ich, ein neues Webbrowser-Steuerelement pro Uri zu erstellen. Ich erstelle einen neuen Thread pro Uri. Das Problem, das ich habe, ist das Thread-Ende vor dem Dokument ist vollständig geladen, sodass ich das DocumentComplete-Ereignis nie nutzen kann. Wie kann ich das überwinden?

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.
quelle

Antworten:

151

Sie müssen einen STA-Thread erstellen, der eine Nachrichtenschleife pumpt. Dies ist die einzige gastfreundliche Umgebung für eine ActiveX-Komponente wie WebBrowser. Andernfalls wird das DocumentCompleted-Ereignis nicht angezeigt. Einige Beispielcodes:

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
quelle
8
Ja! Fügen Sie einfach System.Windows.Forms hinzu. Hat auch meinen Tag gerettet. Danke
zee
4
Ich versuche, diesen Code an meine Situation anzupassen. Ich muss das WebBrowserObjekt am Leben erhalten (um Status / Cookies usw. zu speichern) und im Navigate()Laufe der Zeit mehrere Anrufe tätigen. Ich bin mir jedoch nicht sicher, wo ich meinen Application.Run()Anruf tätigen soll, da dadurch die Ausführung von weiterem Code blockiert wird. Irgendwelche Hinweise?
DotNET
Sie können anrufen Application.Exit();, um Application.Run()zurückkehren zu lassen .
Mike de Klerk
26

Hier erfahren Sie, wie Sie eine Nachrichtenschleife in einem Nicht-UI-Thread organisieren, um asynchrone Aufgaben wie die WebBrowserAutomatisierung auszuführen . Es dient async/awaitzur Bereitstellung des bequemen linearen Codeflusses und lädt eine Reihe von Webseiten in einer Schleife. Der Code ist eine sofort einsatzbereite Konsolen-App, die teilweise auf diesem hervorragenden Beitrag basiert .

Verwandte Antworten:

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
quelle
1
Vielen Dank für diese brillante und informative Antwort! Es ist genau das, wonach ich gesucht habe. Sie scheinen jedoch (absichtlich?) Die Dispose () - Anweisung falsch platziert zu haben.
Wodzu
@ Paweł, du hast recht, dieser Code wurde nicht einmal kompiliert :) Ich denke, eine falsche Version eingefügt, jetzt behoben. Danke, dass du das entdeckt hast. Vielleicht möchten Sie einen allgemeineren Ansatz überprüfen: stackoverflow.com/a/22262976/1768303
noseratio
Ich habe versucht, diesen Code auszuführen, aber er bleibt hängen task.Wait();. Ich mache etwas falsch
0014
1
Hallo, vielleicht können Sie mir dabei helfen: stackoverflow.com/questions/41533997/… - Die Methode funktioniert gut, aber wenn Form vor dem MessageLoopWorker instanziiert wurde, funktioniert sie nicht mehr.
Alex Netkachov
3

Nach meiner Erfahrung in der Vergangenheit arbeitet der Webbrowser nicht gerne außerhalb des Hauptanwendungsthreads.

Versuchen Sie stattdessen, httpwebrequests zu verwenden. Sie können sie als asynchron festlegen und einen Handler erstellen, damit die Antwort weiß, wann sie erfolgreich ist:

How-to-Use-httpwebrequest-net-asynchron

barc0de
quelle
Mein Problem damit ist das. Für das Klicken auf den Uri musste die Site angemeldet sein. Dies kann mit WebRequest nicht erreicht werden. Bei Verwendung des WebBrowser wird bereits der IE-Cache verwendet, sodass die Websites angemeldet sind. Gibt es einen Weg, dies zu umgehen? Die Links betreffen Facebook. Kann ich mich also bei Facebook anmelden und auf den Link mit webwrequest klicken?
Art W
@ArtW Ich weiß, dass dies ein alter Kommentar ist, aber die Leute können das wahrscheinlich lösen, indem siewebRequest.Credentials = CredentialsCache.DefaultCredentials;
vapcguy
@vapcguy Wenn es sich um eine API handelt, dann ja, aber wenn es sich um eine Website mit HTML-Elementen zum Anmelden handelt, müssen IE-Cookies oder Cache verwendet werden. Andernfalls weiß der Client nicht, was mit der CredentialsObjekteigenschaft zu tun ist und wie sie zu füllen ist das HTML.
ColinM
@ColinM Der Kontext, über den diese ganze Seite spricht, verwendet das HttpWebRequest-Objekt und C # .NET, nicht einfache HTML- und Formularelemente, die wie bei JavaScript / AJAX veröffentlicht werden. Aber egal, Sie haben einen Empfänger. Und für die Anmeldung sollten Sie die Windows-Authentifizierung verwenden, und IIS übernimmt dies ohnehin automatisch. Wenn Sie sie manuell testen müssen, können Sie sie WindowsIdentity.GetCurrent().Namenach der Implementierung des Identitätswechsels verwenden und bei Bedarf anhand einer AD-Suche testen. Ich bin mir nicht sicher, wie Cookies und Cache dafür verwendet werden sollen.
Vapcguy
@vapcguy Es geht um WebBrowserdie Frage, welche darauf hinweisen würde, dass HTML-Seiten geladen werden. OP hat sogar gesagt, dass dies WebRequestnicht das erreicht, was er will. Wenn eine Website HTML-Eingaben für die Anmeldung erwartet, Credentialsfunktioniert das Festlegen des Objekts nicht. Darüber hinaus umfassen die Websites, wie OP sagt, Facebook; Die Windows-Authentifizierung funktioniert hier nicht.
ColinM
0

Eine einfache Lösung, bei der mehrere WebBrowser gleichzeitig betrieben werden

  1. Erstellen Sie eine neue Windows Forms-Anwendung
  2. Platzieren Sie die Schaltfläche mit dem Namen button1
  3. Platzieren Sie das Textfeld mit dem Namen textBox1
  4. Festlegen der Eigenschaften des Textfelds: Multiline true und ScrollBars Both
  5. Schreiben Sie den folgenden button1 click handler:

    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
quelle