【问题标题】:Suspend thread until WebBrowser has finished loading暂停线程直到 WebBrowser 完成加载
【发布时间】:2012-07-03 18:43:59
【问题描述】:

我正在尝试浏览网站并使用 Windows 窗体中的 WebBrowser 控件以编程方式在页面上做一些工作。我在寻找阻止我的线程直到 WebBrowser 的 DocumentCompleted 事件被触发的方法时找到了this。鉴于此,这是我当前的代码:

public partial class Form1 : Form
{
    private AutoResetEvent autoResetEvent;

    public Form1()
    {
        InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        Thread workerThread = new Thread(new ThreadStart(this.DoWork));
        workerThread.SetApartmentState(ApartmentState.STA);
        workerThread.Start();
    }

    private void DoWork()
    {
        WebBrowser browser = new WebBrowser();
        browser.DocumentCompleted += new WebBrowserDocumentCompletedEventHandler(browser_DocumentCompleted);
        browser.Navigate(login_page);
        autoResetEvent.WaitOne();
        // log in

        browser.Navigate(page_to_process);
        autoResetEvent.WaitOne();
        // process the page
    }

    private void browser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
    {
        autoResetEvent.Set();
    }
}

线程看起来不是必需的,但是当我扩展此代码以通过网络接受请求时(线程将侦听连接,然后处理请求)。此外,我不能只将处理代码放在 DocumentCompleted 处理程序中,因为我必须导航到几个不同的页面并在每个页面上执行不同的操作。

现在,据我了解,这不起作用的原因是 DocumentCompleted 事件使用了调用 WaitOne() 的同一线程,因此在 WaitOne() 返回之前不会触发该事件(从不,在这种情况下)。

有趣的是,如果我从工具箱(拖放)将 WebBrowser 控件添加到表单,然后使用该控件进行导航,则此代码可以完美运行(除了将 Navigate 调用放在调用中之外,没有任何更改调用 - 见下文)。但是如果我手动将 WebBrowser 控件添加到 Designer 文件,它就不起作用。而且我真的不想在我的表单上看到一个可见的 WebBrowser,我只想报告结果。

public delegate void NavigateDelegate(string address);
browser.Invoke(new NavigateDelegate(this.browser.Navigate), new string[] { login_page });

那么,我的问题是:在浏览器的 DocumentCompleted 事件触发之前暂停线程的最佳方法是什么?

【问题讨论】:

    标签: c#


    【解决方案1】:

    克里斯,

    我在这里向您传递了一个解决问题的可能实现,但请看一下这里的 cmets,在一切按预期工作之前我必须面对和修复。 这是在 webBrowser 的页面上执行某些活动的方法示例(请注意,在我的情况下,webBrowser 是表单的一部分):

        internal ActionResponse CheckMessages() //Action Response is a custom class of mine to store some data coming from pages
            {
            //go to messages
            HtmlDocument doc = WbLink.Document; //wbLink is a referring link to a webBrowser istance
            HtmlElement ele = doc.GetElementById("message_alert_box");
            if (ele == null)
                return new ActionResponse(false);
    
            object obj = ele.DomElement;
            System.Reflection.MethodInfo mi = obj.GetType().GetMethod("click");
            mi.Invoke(obj, new object[0]);
    
            semaphoreForDocCompletedEvent = WaitForDocumentCompleted();  //This is a simil-waitOne statement (1)
            if (!semaphoreForDocCompletedEvent)
                throw new Exception("sequencing of Document Completed events is failed.");
    
            //get the list
            doc = WbLink.Document;
            ele = doc.GetElementById("mailz");
            if (!ele.WaitForAvailability("mailz", Program.BrowsingSystem.Document, 10000)) //This is a simil-waitOne statement (2)
    
                ele = doc.GetElementById("mailz");
            ele = doc.GetElementById("mailz");
    
            //this contains a tbody
            HtmlElement tbody = ele.FirstChild;
    
            //count how many elemetns are espionage reports, these elements are inline then counting double with their wrappers on top of them.
            int spioCases = 0;
            foreach (HtmlElement trs in tbody.Children)
            {
                if (trs.GetAttribute("id").ToLower().Contains("spio"))
                    spioCases++;
            }
    
            int nMessages = tbody.Children.Count - 2 - spioCases;
    
            //create an array of messages to store data
            GameMessage[] archive = new GameMessage[nMessages];
    
            for (int counterOfOpenMessages = 0; counterOfOpenMessages < nMessages; counterOfOpenMessages++)
            {
    
                //open first element
                WbLink.ScriptErrorsSuppressed = true;
                ele = doc.GetElementById("mailz");
                //this contains a tbody
                tbody = ele.FirstChild;
    
                HtmlElement mess1 = tbody.Children[1];
                int idMess1 = int.Parse(mess1.GetAttribute("id").Substring(0, mess1.GetAttribute("id").Length - 2));
                //check if subsequent element is not a spio report, in case it is then the element has not to be opened.
                HtmlElement mess1Sibling = mess1.NextSibling;
                if (mess1Sibling.GetAttribute("id").ToLower().Contains("spio"))
                {
                    //this is a wrapper for spio report
                    ReadSpioEntry(archive, counterOfOpenMessages, mess1, mess1Sibling);
                    //delete first in line
                    DeleteFirstMessageItem(doc, ref ele, ref obj, ref mi, ref tbody);
                    semaphoreForDocCompletedEvent = WaitForDocumentCompleted(6); //This is a simil-waitOne statement (3)
    
                }
                else
                {
                    //It' s anormal message
                    OpenMessageEntry(ref obj, ref mi, tbody, idMess1); //This opens a modal dialog over the page, and it is not generating a DocumentCompleted Event in the webBrowser
    
                    //actually opening a message generates 2 documetn completed events without any navigating event issued
                    //Application.DoEvents();
                    semaphoreForDocCompletedEvent = WaitForDocumentCompleted(6);
    
                    //read element
                    ReadMessageEntry(archive, counterOfOpenMessages);
    
                    //close current message
                    CloseMessageEntry(ref ele, ref obj, ref mi);  //this closes a modal dialog therefore is not generating a documentCompleted after!
                    semaphoreForDocCompletedEvent = WaitForDocumentCompleted(6);
                    //delete first in line
                    DeleteFirstMessageItem(doc, ref ele, ref obj, ref mi, ref tbody); //this closes a modal dialog therefore is not generating a documentCompleted after!
                    semaphoreForDocCompletedEvent = WaitForDocumentCompleted(6);
                }
            }
            return new ActionResponse(true, archive);
        }
    

    在实践中,此方法获取 MMORPG 的页面并读取其他玩家发送到帐户的消息,并通过方法 ReadMessageEntry 将它们存储在 ActionResponse 类中。

    除了真正依赖于大小写的代码的实现和逻辑(并且对您没有用处)之外,还有一些有趣的元素可能会对您的案例产生很好的影响。 我在代码中放了一些 cmets 并突出显示了 3 个重要点[带有符号 (1)(2)(3)]

    算法是:

    1) 到达一个页面

    2) 从 webBrowser 获取底层 Document

    3) 找到一个元素以点击进入消息页面[完成方式:HtmlElement ele = doc.GetElementById("message_alert_box");]

    4) 通过 MethodInfo 实例和反射调用触发单击它的事件[这会调用另一个页面,因此 DocumentCompleted 迟早会到达]

    5) 等待调用完成的文档,然后继续[完成:semaphoreForDocCompletedEvent = WaitForDocumentCompleted(); at point (1)]

    6) 页面更改后从 webBrowser 获取新的 Document

    7) 在页面上找到一个特定的锚点,该锚点定义了我要阅读的消息的位置

    8) 确保页面中存在这样的 TAG(因为可能存在一些 AJAX 延迟我想要阅读的内容准备就绪)[完成:ele.WaitForAvailability("mailz", Program.BrowsingSystem.Document, 10000) 即第 (2) 点]

    9) 为读取每条消息执行整个循环,这意味着打开一个位于同一页面上的模式对话框表单,因此不会生成 DocumentCompleted,准备好时读取它,然后关闭它,然后重新循环。对于这种特殊情况,我在点 (3) 处使用称为 semaphoreForDocCompletedEvent = WaitForDocumentCompleted(6); 的 (1) 重载

    现在我用来暂停、查看和阅读的三种方法:

    (1) 在引发 DocumentCompleted 时停止而不会对 DocumentCompleted 方法过度收费,该方法可用于多个单一目的(如您的情况)

    private bool WaitForDocumentCompleted()
            {
                Thread.SpinWait(1000);  //This is dirty but working
                while (Program.BrowsingSystem.IsBusy) //BrowsingSystem is another link to Browser that is made public in my Form and IsBusy is just a bool put to TRUE when Navigating event is raised and but to False when the DocumentCOmpleted is fired.
                {
                    Application.DoEvents();
                    Thread.SpinWait(1000);
                }
    
                if (Program.BrowsingSystem.IsInfoAvailable)  //IsInfoAvailable is just a get property to cover webBroweser.Document inside a lock statement to protect from concurrent accesses.
                {
                    return true;
                }
                else
                    return false;
            }
    

    (2) 等待页面中的特定标签可用:

    public static bool WaitForAvailability(this HtmlElement tag, string id, HtmlDocument documentToExtractFrom, long maxCycles)
            {
                bool cond = true;
                long counter = 0;
                while (cond)
                {
                    Application.DoEvents(); //VERIFY trovare un modo per rimuovere questa porcheria
                    tag = documentToExtractFrom.GetElementById(id);
                    if (tag != null)
                        cond = false;
                    Thread.Yield();
                    Thread.SpinWait(100000);
                    counter++;
                    if (counter > maxCycles)
                        return false;
                }
                return true;
            }
    

    (3) 等待 DocumentCompleted 到达的肮脏技巧,因为页面上不需要重新加载框架!

    private bool WaitForDocumentCompleted(int seconds)
        {
            int counter = 0;
            while (Program.BrowsingSystem.IsBusy)
            {
                Application.DoEvents();
                Thread.Sleep(1000);
                if (counter == seconds)
                {
                return true;
                }
                counter++;
            }
            return true;
        }
    

    我还将 DocumentCompleted 方法和导航传递给您,让您全面了解我是如何使用它们的。

    private void webBrowser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
            {
                if (Program.BrowsingSystem.BrowserLink.ReadyState == WebBrowserReadyState.Complete)
                {
                    lock (Program.BrowsingSystem.BrowserLocker)
                    {
                        Program.BrowsingSystem.ActualPosition = Program.BrowsingSystem.UpdatePosition(Program.BrowsingSystem.Document);
                        Program.BrowsingSystem.CheckContentAvailability();
                        Program.BrowsingSystem.IsBusy = false;
                    }
                }
            }
    
    private void webBrowser_Navigating(object sender, WebBrowserNavigatingEventArgs e)
            {
                lock (Program.BrowsingSystem.BrowserLocker)
                {
                    Program.BrowsingSystem.ActualPosition.PageName = OgamePages.OnChange;
                    Program.BrowsingSystem.IsBusy = true;
                }
            }
    

    如果您现在了解此处介绍的实现背后的细节,请查看here 以了解 DoEvents() 背后的混乱情况(希望从 S.Overflow 链接其他站点不是问题) .

    最后一点说明,当您从 Form 实例中使用 Navigate 方法时,您需要将调用放在 Invoke 中:这很清楚您需要一个 Invoke,因为需要在webBrowser(或什至将其作为参考变量纳入范围)需要在 webBrowser 本身的同一线程上启动!

    此外,如果 WB 是某种 Form 容器的子容器,它还需要实例化它的线程与 Form 创建相同,并且为了传递性,需要在 WB 上工作的所有方法都需要在 Form 线程上调用(在您的情况下,调用会在 Form 本机线程上重新定位您的调用)。 我希望这对你有用(我只是用我的母语在代码中留下了 //VERIFY 注释,让你知道我对 Application.DoEvents() 的看法)。

    亲切的问候, 亚历克斯

    【讨论】:

      【解决方案2】:

      哈哈!我有同样的问题。您可以通过事件处理来做到这一点。如果您在页面中途停止线程,它将需要等到页面完成。你可以很容易地通过附加来做到这一点

       Page.LoadComplete += new EventHandler(triggerFunction);
      

      在 triggerFunction 中你可以这样做

      triggerFunction(object sender, EventArgs e)
      {
           autoResetEvent.reset();
      }
      

      让我知道这是否有效。我最终没有在我的线程中使用线程,而是将这些东西放入 triggerFunction 中。有些语法可能不是 100% 正确,因为我是在头脑中回答问题

      【讨论】:

      • 这基本上是我所拥有的,除了我直接调用 autoResetEvent.Set() (autoResetEvent.Reset() 将保持阻塞,而不是解除阻塞)而不是使用中间 triggerFunction。但是,这段代码不起作用(同样,据我了解,这是因为 EventHandler 与初始调用在同一个线程中执行,因此如果线程无限期阻塞,则事件将永远不会触发)。
      • 您的主页线程似乎有问题,导致它无法完成执行。如果你暂停你的线程,页面应该保持渲染(母鸡它是一个线程)。也许我只是显示了错误的函数调用?但是,如果您想在事件触发器上释放线程,因为它保证页面已经完成渲染(至少服务器认为它是)
      • 我编辑了我的帖子以使 DocumentCompleted 事件处理更加清晰。显然线程执行的方式有问题,但我不知道是什么。
      • 看起来您的 AutoResetEvent set() 函数被调用一次,然后再次进入等待状态。据我了解,您必须实际释放所有内部线程,以便服务器将页面提供给浏览器。你是如何结束第二个 waitOne 的?您能否更好地了解您的代码在卡住之前能走多远?它是否通过了第一个 waitOne?
      • 是的,在AutoResetEvent 上调用 Set() 将释放一个等待线程,然后返回到无信号状态。这正是我想要的,因为我只有一个线程在等待。我不太明白你所说的“内线”是什么意思。第二个 WaitOne() 应该以与第一个相同的方式释放 - 通过在 browser_DocumentCompleted 方法中调用 Set()。第一个 WaitOne() 像写的那样永远阻塞;如果我设置了超时,它总是超时。永远不会进行 Set() 调用。
      【解决方案3】:

      编辑

      像这样在初始化组件方法中注册,而不是在同一个方法中。

      WebBrowser browser = new WebBrowser(); 
      WebBrowserDocumentCompletedEventHandler(webBrowser_DocumentCompleted);
      

      ReadyState 将在 DocumentCompleted 事件中检查时告诉您文档加载的进度。

      void webBrowser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
      {
         if (browser.ReadyState == WebBrowserReadyState.Complete)
      {
      
      }
      }
      

      【讨论】:

      • 如果browser.ReadyState != WebBrowserReadyState.Complete,你有一个无限循环。 ReadyState 不会改变。唯一可行的方法是browser.ReadyState 已经是WebBrowserReadyState.Complete(这是有道理的:这是DocumentCompleted 事件),但在这种情况下,您不需要循环。
      • 正如我所提到的,DocumentCompleted 事件永远不会触发,因为它与 Navigate() 调用在同一个线程中执行,因此只会在 WaitOne() 返回后执行(它永远不会执行,因为永远不会调用 Set() 方法)。
      • 我已经尝试手动将 WebBrowser 添加到 InitializeComponent() 方法中,但结果是一样的。如果我让 Visual Studio 创建代码,它会起作用(通过将 WebBrowser 控件从工具箱拖放到表单中),但我不想在我的表单上使用可视化 WebBrowser 控件。我已经确定不同之处在于这行代码:InitializeComponent() 方法中的this.Controls.Add(this.browser);。如果我添加此行,则手动创建 Web 浏览器即可。但是,这也会将 WebBrowser 控件放置在表单上的默认位置。
      • 1- 您可以尝试在初始化方法之外的 Form_Load 中进行操作。
      • 2- 如果控件出现在默认位置,你不能调整它吗?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-01-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-10
      相关资源
      最近更新 更多