WebBrowser Control w nowym wątku


84

Mam listę Uri, które chcę „kliknąć” Aby to osiągnąć, próbuję utworzyć nową kontrolkę przeglądarki internetowej na Uri. Tworzę nowy wątek na Uri. Problemem jest koniec wątku przed dokumentem jest w pełni załadowany, więc nigdy nie mogę skorzystać ze zdarzenia DocumentComplete. Jak mogę to przezwyciężyć?

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

Odpowiedzi:


152

Musisz utworzyć wątek STA, który pompuje pętlę komunikatów. To jedyne przyjazne środowisko dla składnika ActiveX, takiego jak WebBrowser. W przeciwnym razie nie otrzymasz zdarzenia DocumentCompleted. Przykładowy kod:

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
Tak! Po prostu dodaj System.Windows.Forms. Uratowałem też mój dzień. Dzięki
zee

4
Próbuję dostosować ten kod do mojej sytuacji. Muszę utrzymywać WebBrowserobiekt przy życiu (aby zapisać stan / pliki cookie itp.) I wykonywać wiele Navigate()połączeń w czasie. Ale nie jestem pewien, gdzie mam wykonać moje Application.Run()wywołanie, ponieważ blokuje dalsze wykonywanie kodu. Jakieś wskazówki?
dotNET

Możesz zadzwonić Application.Exit();i pozwolić Application.Run()wrócić.
Mike de Klerk

26

Oto jak zorganizować pętlę komunikatów w wątku spoza interfejsu użytkownika, aby uruchamiać zadania asynchroniczne, takie jak WebBrowserautomatyzacja. Używa async/awaitdo zapewnienia wygodnego liniowego przepływu kodu i ładuje zestaw stron internetowych w pętli. Kod jest gotową do uruchomienia aplikacją konsolową, która jest częściowo oparta na tym doskonałym poście .

Powiązane odpowiedzi:

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
Dzięki za tę genialną i pouczającą odpowiedź! Dokładnie tego szukałem. Jednak wydaje się, że (celowo?) Zgubiłeś instrukcję Dispose ().
wodzu

@ Paweł, masz rację, ten kod nawet się nie skompilował :) Myślę, że wkleiłem złą wersję, teraz naprawiono. Dzięki, że to zauważyłeś. Możesz sprawdzić bardziej ogólne podejście: stackoverflow.com/a/22262976/1768303
noseratio

Próbowałem uruchomić ten kod, ale zacina się task.Wait();. Robię coś źle
0014

1
Cześć, może mógłbyś mi pomóc z tym: stackoverflow.com/questions/41533997/… - metoda działa dobrze, ale jeśli wystąpienie Form zostało utworzone przed MessageLoopWorker, przestaje działać.
Alex Netkachov,

3

Z mojego doświadczenia wynika, że ​​przeglądarka internetowa nie lubi działać poza głównym wątkiem aplikacji.

Spróbuj zamiast tego użyć httpwebrequests, możesz ustawić je jako asynchroniczne i utworzyć procedurę obsługi dla odpowiedzi, która będzie wiedziała, kiedy się powiedzie:

how-to-use-httpwebrequest-net-asynchronously


Mój problem z tym jest taki. Kliknięcie Uri wymagało zalogowania się do witryny. Nie mogę tego osiągnąć za pomocą WebRequest. Używając WebBrowser, używa on już pamięci podręcznej IE, więc strony są zalogowane. Czy jest sposób na obejście tego? Linki dotyczą Facebooka. Czy mogę więc zalogować się na Facebooku i kliknąć łącze z webwrequest?
Art W

@ArtW Wiem, że to stary komentarz, ale ludzie prawdopodobnie mogą to rozwiązać, ustawiającwebRequest.Credentials = CredentialsCache.DefaultCredentials;
vapcguy

@vapcguy Jeśli jest to API, to tak, ale jeśli jest to strona internetowa z elementami HTML do logowania, będzie musiała użyć plików cookie IE lub pamięci podręcznej, w przeciwnym razie klient nie będzie wiedział, co zrobić z Credentialswłaściwością obiektu i jak wypełnić HTML.
ColinM

@ColinM Kontekst, o którym mówi cała ta strona, to użycie obiektu HttpWebRequest i C # .NET, a nie wysyłanie prostych elementów HTML i formularzy, tak jak w przypadku JavaScript / AJAX. Ale niezależnie, masz odbiornik. Do logowania powinieneś używać uwierzytelniania systemu Windows, a usługi IIS i tak obsługują to automatycznie. Jeśli chcesz przetestować je ręcznie, możesz użyć WindowsIdentity.GetCurrent().Namepo zaimplementowaniu personifikacji i przetestować je pod kątem wyszukiwania AD, jeśli chcesz. Nie jestem pewien, jak pliki cookie i pamięć podręczna zostałyby wykorzystane do tego celu.
vapcguy

@vapcguy Pytanie dotyczy tego, WebBrowserco wskazywałoby, że ładowane są strony HTML, OP powiedział nawet, że WebRequestnie osiągnie tego, czego chce, dlatego jeśli witryna oczekuje wejścia HTML do logowania, ustawienie Credentialsobiektu nie zadziała. Ponadto, jak mówi OP, strony te obejmują Facebooka; Uwierzytelnianie systemu Windows nie będzie działać w tym przypadku.
ColinM

0

Proste rozwiązanie, w którym występuje jednoczesne działanie kilku przeglądarek internetowych

  1. Utwórz nową aplikację Windows Forms
  2. Umieść przycisk o nazwie button1
  3. Umieść pole tekstowe o nazwie textBox1
  4. Ustaw właściwości pola tekstowego: Multiline true i ScrollBars Both
  5. Napisz następującą procedurę obsługi kliknięcia 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);
    
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.