在新线程中使用WebBrowser控件

88

我有一组Uri列表,我想要"点击"它们。为了实现这个目的,我尝试为每个Uri创建一个新的Web浏览器控件,并为每个Uri创建一个新线程。但是,我的问题是线程在文档完全加载之前就结束了,所以我从来没有机会使用DocumentComplete事件。如何解决这个问题?

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);
}
4个回答

158

你需要创建一个STA线程并且运行一个消息循环。这是唯一适合像WebBrowser这样的ActiveX组件的宿主环境。否则你将得不到DocumentCompleted事件。以下是一些示例代码:

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
是的!只需添加System.Windows.Forms。这也解决了我的问题,谢谢。 - zee
4
我正在尝试将此代码调整到我的情况下。我需要保持WebBrowser对象的活动状态(以保存状态/cookies等),并随时间执行多个Navigate()调用。但是我不确定在哪里放置Application.Run()调用,因为它会阻止进一步的代码执行。有什么提示吗? - dotNET
你可以调用 Application.Exit();Application.Run() 返回。 - Mike de Klerk
如果我正在使用任务,如何设置STA? - camino

27
以下是翻译的结果:

以下是如何在非 UI 线程上组织消息循环以运行异步任务,例如 WebBrowser 自动化。它使用 async/await 提供方便的线性代码流,并在循环中加载一组 web 页面。该代码是一个准备好运行的控制台应用程序,部分基于此优秀帖子

相关答案:

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
谢谢你那个精彩而且富有信息量的回答!这正是我所需要的。然而,你似乎(故意?)放错了Dispose()语句。 - wodzu
@Paweł,你是对的,那段代码甚至都没有编译:)我想我粘贴了一个错误的版本,现在已经修复了。感谢你的发现。你可能想要检查更通用的方法:https://dev59.com/OGEh5IYBdhLWcg3wlEZq#22262976 - noseratio - open to work
我尝试运行这段代码,但它卡在了 task.Wait();。我做错了什么吗? - 0014
1
嗨,也许你可以帮我解决这个问题:http://stackoverflow.com/questions/41533997/nunit-test-with-application-loop-in-it-hangs-when-form-is-created-before-it - 这个方法很好用,但是如果在MessageLoopWorker之前实例化了Form,它就会停止工作。 - Alex Netkachov

3

根据我的经验,Web浏览器不喜欢在主应用程序线程之外运行。

尝试使用httpwebrequests代替,您可以将它们设置为异步,并创建一个响应处理程序以了解何时成功:

如何使用httpwebrequest-net异步


我的问题是这个。点击的Uri需要网站登录。我无法使用WebRequest实现这一点。通过使用WebBrowser,它已经使用IE缓存,因此网站已经登录。有没有办法绕过这个问题?这些链接涉及Facebook。那么我能否使用WebRequest登录Facebook并单击链接? - Art W
@ArtW 我知道这是一个旧评论,但人们可以通过设置 webRequest.Credentials = CredentialsCache.DefaultCredentials; 来解决这个问题。 - vapcguy
如果它是一个API,那么是的。但如果它是一个带有HTML元素用于登录的网站,则需要使用IE cookies或缓存,否则客户端不知道如何处理Credentials对象属性以及如何填充HTML。 - ColinM
@ColinM 这整个页面讨论的上下文是使用 HttpWebRequest 对象和 C# .NET,而不是像你可能会用 JavaScript/AJAX 那样简单地发布 HTML 和表单元素。但无论如何,您都有一个接收器。对于登录,您应该使用 Windows 身份验证,IIS 会自动处理此问题。如果您需要手动测试它们,可以在实现模拟后使用 WindowsIdentity.GetCurrent().Name 并将其与 AD 搜索进行测试。不确定 cookie 和缓存如何用于其中任何一个。 - vapcguy
@vapcguy 这个问题涉及到WebBrowser,这表明正在加载HTML页面。OP甚至已经说过WebRequest不能实现他想要的东西,因此,如果网站期望使用HTML输入进行登录,则设置Credentials对象将不起作用。此外,正如OP所说,这些网站包括Facebook;Windows身份验证在其中不起作用。 - ColinM
@vacpcguy 希望这可以帮到你。如果你能使用NTLM凭据登录Facebook,我会非常印象深刻。我已经在这张图片中为你突出了关键信息,如果你在这之后还在争论,那么你绝对不可能理解。 - ColinM

0

一个简单的解决方案,可以同时运行多个Web浏览器

  1. 创建一个新的Windows Forms应用程序
  2. 放置名为button1的按钮
  3. 放置名为textBox1的文本框
  4. 设置文本字段的属性:Multiline true和ScrollBars Both
  5. 编写以下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("任务 " + tmp + ",导航到 " + cur_e.Url + Environment.NewLine);
                    completed_count++;
                }
            };
            wb.Navigate("https://dev59.com/FW855IYBdhLWcg3wj1MS");
        }
        ));
    }
    
    while (completed_count != count)
    {
        Application.DoEvents();
        Thread.Sleep(10);
    }
    textBox1.AppendText("全部完成" + Environment.NewLine);
    

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接