【问题标题】:Get ReadyState from WebBrowser control without DoEvents从没有 DoEvents 的 WebBrowser 控件中获取 ReadyState
【发布时间】:2014-01-15 14:40:57
【问题描述】:

这已在此处和其他站点及其工作中多次发现,但我想以其他方式提出想法:

在使用导航或发布后获得 ReadyState = Complete,而不使用 DoEvents,因为它的所有缺点。

我还要注意,在这里使用 DocumentComplete 事件无济于事,因为我不会只在一个页面上导航,而是像这样一个接一个地导航。

wb.navigate("www.microsoft.com")
//dont use DoEvents loop here
wb.Document.Body.SetAttribute(textbox1, "login")
//dont use DoEvents loop here
if (wb.documenttext.contais("text"))
//do something

它今天的工作方式是使用 DoEvents。我想知道是否有人有适当的方法来等待浏览器方法的异步调用,然后才能继续执行其余的逻辑。只是为了它。

提前致谢。

【问题讨论】:

  • 必须使用 DocumentCompleted 事件。您需要做的就是跟踪 what 已完成。该事件已经告诉你,你得到了 e.Url 属性。如果您需要了解更多信息,则只需使用一个跟踪状态的变量。一个简单的整数或枚举就可以了。

标签: c# .net webbrowser-control readystate doevents


【解决方案1】:

下面是一个基本的 WinForms 应用程序代码,说明了如何使用 async/await 异步等待 DocumentCompleted 事件。它一个接一个地导航到多个页面。一切都在主 UI 线程上进行。

它可能不是调用this.webBrowser.Navigate(url),而是模拟表单按钮单击,以触发 POST 样式的导航。

webBrowser.IsBusy 异步循环逻辑是可选的,其目的是(非确定性地)考虑可能在window.onload 事件之后发生的页面动态 AJAX 代码。

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WebBrowserApp
{
    public partial class MainForm : Form
    {
        WebBrowser webBrowser;

        public MainForm()
        {
            InitializeComponent();

            // create a WebBrowser
            this.webBrowser = new WebBrowser();
            this.webBrowser.Dock = DockStyle.Fill;
            this.Controls.Add(this.webBrowser);

            this.Load += MainForm_Load;
        }

        // Form Load event handler
        async void MainForm_Load(object sender, EventArgs e)
        {
            // cancel the whole operation in 30 sec
            var cts = new CancellationTokenSource(30000);

            var urls = new String[] { 
                    "http://www.example.com", 
                    "http://www.gnu.org", 
                    "http://www.debian.org" };

            await NavigateInLoopAsync(urls, cts.Token);
        }

        // navigate to each URL in a loop
        async Task NavigateInLoopAsync(string[] urls, CancellationToken ct)
        {
            foreach (var url in urls)
            {
                ct.ThrowIfCancellationRequested();
                var html = await NavigateAsync(ct, () => 
                    this.webBrowser.Navigate(url));
                Debug.Print("url: {0}, html: \n{1}", url, html);
            }
        }

        // asynchronous navigation
        async Task<string> NavigateAsync(CancellationToken ct, Action startNavigation)
        {
            var onloadTcs = new TaskCompletionSource<bool>();
            EventHandler onloadEventHandler = null;

            WebBrowserDocumentCompletedEventHandler documentCompletedHandler = delegate
            {
                // DocumentCompleted may be called several time for the same page,
                // if the page has frames
                if (onloadEventHandler != null)
                    return;

                // so, observe DOM onload event to make sure the document is fully loaded
                onloadEventHandler = (s, e) =>
                    onloadTcs.TrySetResult(true);
                this.webBrowser.Document.Window.AttachEventHandler("onload", onloadEventHandler);
            };

            this.webBrowser.DocumentCompleted += documentCompletedHandler;
            try
            {
                using (ct.Register(() => onloadTcs.TrySetCanceled(), useSynchronizationContext: true))
                {
                    startNavigation();
                    // wait for DOM onload event, throw if cancelled
                    await onloadTcs.Task;
                }
            }
            finally
            {
                this.webBrowser.DocumentCompleted -= documentCompletedHandler;
                if (onloadEventHandler != null)
                    this.webBrowser.Document.Window.DetachEventHandler("onload", onloadEventHandler);
            }

            // the page has fully loaded by now

            // optional: let the page run its dynamic AJAX code,
            // we might add another timeout for this loop
            do { await Task.Delay(500, ct); }
            while (this.webBrowser.IsBusy);

            // return the page's HTML content
            return this.webBrowser.Document.GetElementsByTagName("html")[0].OuterHtml;
        }
    }
}

如果您希望通过控制台应用程序执行类似操作,请访问an example of that

【讨论】:

  • 你为什么用 C 语法编写 Visual Basic .NET?
【解决方案2】:

解决方法很简单:

    // MAKE SURE ReadyState = Complete
            while (WebBrowser1.ReadyState.ToString() != "Complete") {
                Application.DoEvents();         
            }

// 继续你的子序列代码...


又脏又快.. 我是 VBA 人,这种逻辑一直有效,只是花了我几天时间,没有找到 C#,但我自己想通了。

以下是我的完整功能,目的是从网页中获取一段信息:

private int maxReloadAttempt = 3;
    private int currentAttempt = 1;

    private string GetCarrier(string webAddress)
    {
        WebBrowser WebBrowser_4MobileCarrier = new WebBrowser();
        string innerHtml;
        string strStartSearchFor = "subtitle block pull-left\">";
        string strEndSearchFor = "<";

        try
        {
            WebBrowser_4MobileCarrier.ScriptErrorsSuppressed = true;
            WebBrowser_4MobileCarrier.Navigate(webAddress); 

            // MAKE SURE ReadyState = Complete
            while (WebBrowser_4MobileCarrier.ReadyState.ToString() != "Complete") {
                Application.DoEvents();         
            }

            // LOAD HTML
            innerHtml = WebBrowser_4MobileCarrier.Document.Body.InnerHtml;  

            // ATTEMPT (x3) TO EXTRACT CARRIER STRING
            while (currentAttempt <=  maxReloadAttempt) {
                if (innerHtml.IndexOf(strStartSearchFor) >= 0)
                {
                    currentAttempt = 1; // Reset attempt counter
                    return Sub_String(innerHtml, strStartSearchFor, strEndSearchFor, "0"); // Method: "Sub_String" is my custom function
                }
                else
                {
                    currentAttempt += 1;    // Increment attempt counter
                    GetCarrier(webAddress); // Recursive method call
                } // End if
            } // End while
        }   // End Try

        catch //(Exception ex)
        {
        }
        return "Unavailable";
    }

【讨论】:

    【解决方案3】:

    这是一个“快速而肮脏”的解决方案。它不是 100% 万无一失,但它不会阻塞 UI 线程,它应该可以满足 WebBrowser 控件自动化程序的原型:

        private async void testButton_Click(object sender, EventArgs e)
        {
            await Task.Factory.StartNew(
                () =>
                {
                    stepTheWeb(() => wb.Navigate("www.yahoo.com"));
                    stepTheWeb(() => wb.Navigate("www.microsoft.com"));
                    stepTheWeb(() => wb.Navigate("asp.net"));
                    stepTheWeb(() => wb.Document.InvokeScript("eval", new[] { "$('p').css('background-color','yellow')" }));
                    bool testFlag = false;
                    stepTheWeb(() => testFlag = wb.DocumentText.Contains("Get Started"));
                    if (testFlag) {    /* TODO */ }
                    // ... 
                }
            );
        }
    
        private void stepTheWeb(Action task)
        {
            this.Invoke(new Action(task));
    
            WebBrowserReadyState rs = WebBrowserReadyState.Interactive;
            while (rs != WebBrowserReadyState.Complete)
            {
                this.Invoke(new Action(() => rs = wb.ReadyState));
                System.Threading.Thread.Sleep(300);
            }
       }
    

    这里是testButton_Click 方法的更通用版本:

        private async void testButton_Click(object sender, EventArgs e)
        {
            var actions = new List<Action>()
                {
                    () => wb.Navigate("www.yahoo.com"),
                    () => wb.Navigate("www.microsoft.com"),
                    () => wb.Navigate("asp.net"),
                    () => wb.Document.InvokeScript("eval", new[] { "$('p').css('background-color','yellow')" }),
                    () => {
                             bool testFlag = false;
                             testFlag  = wb.DocumentText.Contains("Get Started"); 
                             if (testFlag)  {   /*  TODO */  }
                           }
                    //... 
                };
    
            await Task.Factory.StartNew(() => actions.ForEach((x)=> stepTheWeb (x)));  
        }
    

    [更新]

    我通过借用和轻微重构@Noseratio's NavigateAsync method from this topic 来调整我的“快速而肮脏”的示例。 新的代码版本将在 UI 线程上下文中异步自动/执行,不仅是导航操作,还有 Javascript/AJAX 调用 - 任何“lamdas”/一个自动化步骤任务实现方法。

    非常欢迎所有代码审查/cmets。特别是来自@Noseratio。我们将一起让这个世界变得更美好;)

        public enum ActionTypeEnumeration
        {
            Navigation = 1,
            Javascript = 2,
            UIThreadDependent = 3,
            UNDEFINED = 99
        }
    
        public class ActionDescriptor
        {
            public Action Action { get; set; }
            public ActionTypeEnumeration ActionType { get; set; }
        }
    
        /// <summary>
        /// Executes a set of WebBrowser control's Automation actions
        /// </summary>
        /// <remarks>
        ///  Test form shoudl ahve the following controls:
        ///    webBrowser1 - WebBrowser,
        ///    testbutton - Button,
        ///    testCheckBox - CheckBox,
        ///    totalHtmlLengthTextBox - TextBox
        /// </remarks> 
        private async void testButton_Click(object sender, EventArgs e)
        {
            try
            {
                var cts = new CancellationTokenSource(60000);
    
                var actions = new List<ActionDescriptor>()
                {
                    new ActionDescriptor() { Action = ()=>  wb.Navigate("www.yahoo.com"), ActionType = ActionTypeEnumeration.Navigation}  ,
                    new ActionDescriptor() { Action = () => wb.Navigate("www.microsoft.com"), ActionType = ActionTypeEnumeration.Navigation}  ,
                    new ActionDescriptor() { Action = () => wb.Navigate("asp.net"), ActionType = ActionTypeEnumeration.Navigation}  ,
                    new ActionDescriptor() { Action = () => wb.Document.InvokeScript("eval", new[] { "$('p').css('background-color','yellow')" }), ActionType = ActionTypeEnumeration.Javascript}, 
                    new ActionDescriptor() { Action =
                    () => {
                             testCheckBox.Checked = wb.DocumentText.Contains("Get Started"); 
                           },
                           ActionType = ActionTypeEnumeration.UIThreadDependent} 
                    //... 
                };
    
                foreach (var action in actions)
                {
                   string html = await ExecuteWebBrowserAutomationAction(cts.Token, action.Action, action.ActionType);
                   // count HTML web page stats - just for fun
                   int totalLength = 0;
                   Int32.TryParse(totalHtmlLengthTextBox.Text, out totalLength);
                   totalLength += !string.IsNullOrWhiteSpace(html) ? html.Length : 0;
                   totalHtmlLengthTextBox.Text = totalLength.ToString();   
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "Error");   
            }
        }
    
        // asynchronous WebBroswer control Automation
        async Task<string> ExecuteWebBrowserAutomationAction(
                                CancellationToken ct, 
                                Action runWebBrowserAutomationAction, 
                                ActionTypeEnumeration actionType = ActionTypeEnumeration.UNDEFINED)
        {
            var onloadTcs = new TaskCompletionSource<bool>();
            EventHandler onloadEventHandler = null;
    
            WebBrowserDocumentCompletedEventHandler documentCompletedHandler = delegate
            {
                // DocumentCompleted may be called several times for the same page,
                // if the page has frames
                if (onloadEventHandler != null)
                    return;
    
                // so, observe DOM onload event to make sure the document is fully loaded
                onloadEventHandler = (s, e) =>
                    onloadTcs.TrySetResult(true);
                this.wb.Document.Window.AttachEventHandler("onload", onloadEventHandler);
            };
    
    
            this.wb.DocumentCompleted += documentCompletedHandler;
            try
            {
                using (ct.Register(() => onloadTcs.TrySetCanceled(), useSynchronizationContext: true))
                {
                    runWebBrowserAutomationAction();
    
                    if (actionType == ActionTypeEnumeration.Navigation)
                    {
                        // wait for DOM onload event, throw if cancelled
                        await onloadTcs.Task;
                    }
                }
            }
            finally
            {
                this.wb.DocumentCompleted -= documentCompletedHandler;
                if (onloadEventHandler != null)
                    this.wb.Document.Window.DetachEventHandler("onload", onloadEventHandler);
            }
    
            // the page has fully loaded by now
    
            // optional: let the page run its dynamic AJAX code,
            // we might add another timeout for this loop
            do { await Task.Delay(500, ct); }
            while (this.wb.IsBusy);
    
            // return the page's HTML content
            return this.wb.Document.GetElementsByTagName("html")[0].OuterHtml;
        }
    

    【讨论】:

    • 没有冒犯,但这是一个糟糕的设计。它使用后台线程仅通过Control.Invoke 操作UI 线程上的WebBrowser 对象。 此任务不需要额外的线程。Thread.Sleep(300) 循环...有DocumentCompleted 事件。
    • @Noseratio,谢谢,我知道 :) 所以我注意到它是 “快速和肮脏” 解决方案 prototype WebBrowser 控件的自动化程序。我确实将DocumentCompleted 用于现实生活中的项目。显然var actions ...“控制结构”可以通用化,我的private void stepTheWeb(Action task)可以重构为使用DocumentCompleted和其他tricks不仅可以处理WebBrowser控制导航,还可以处理Javascript/AJAX动作/调用。至于额外线程 - 这种“快速而肮脏”的解决方案会在没有额外线程的情况下挂起UI线程,不是吗?
    • @Noseratio,我刚刚在这里发布了一个新的代码版本,通过借用和改编了本主题的部分代码示例,你可以吗?
    • 我看不出你的ExecuteWebBrowserAutomationAction 和我的NavigateAsync 有什么区别。虽然这段代码本身并没有什么特别之处,但我记得从附近的答案中借用关键部分并不是我经常在 SO 上看到的。
    • @Noseratio,这个主题的原始问题不仅是关于 WebBrowser 控制 URL 导航的自动化,而且还请求以 wb.Document.Body.SetAttribute(textbox1, "login") 执行代码行。您当前版本的 NavigateAsync 会为该代码行引发运行时错误。我对其进行了一些更正以显示差异。随意“借回”更正的代码,使其更加可靠 - 如果您发现任何问题,我将放弃我的答案部分,其中有您的 NavigateAsync 借用.
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-08-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多