5

I am working on automated testing script and am using the WebBrowser control. I am trying to submit the following HTML and testing when the user accepts the terms of service:

    <form action="http://post.dev.dealerconnextion/k/6hRbDTwn4xGVl2MHITQsBw/hrshq" method="post">
        <input name="StepCheck" value="U2FsdGVkX18zMTk5MzE5OUgFyFgD3V5yf5Rwbtfhf3gjdH4KSx4hqj4vkrw7K6e-" type="hidden">
        <button type="submit" name="continue" value="y">ACCEPT the terms of use</button>
        <button type="submit" name="continue" value="n">DECLINE the terms of use</button>
    </form>

    // Terms of Use Information

    <form action="http://post.dev.dealerconnextion/k/6hRbDTwn4xGVl2MHITQsBw/hrshq" method="post">
        <input name="StepCheck" value="U2FsdGVkX18zMTk5MzE5OUgFyFgD3V5yf5Rwbtfhf3gjdH4KSx4hqj4vkrw7K6e-" type="hidden">
        <button type="submit" name="continue" value="y">ACCEPT the terms of use</button>
        <button type="submit" name="continue" value="n">DECLINE the terms of use</button>
    </form>

Here is the code in C#, but does not submit the form.

            HtmlElementCollection el = webBrowser.Document.GetElementsByTagName("button");
            foreach (HtmlElement btn in el)
            {
                if (btn.InnerText == "ACCEPT the terms of use")
                {
                    btn.InvokeMember("Click");
                }
            }

Any help would be much appreciated. Thanks.

noseratio
  • 59,932
  • 34
  • 208
  • 486
PiE
  • 335
  • 1
  • 7
  • 24
  • You should be calling `btn.InvokeMember("Click")` upon `DocumentCompleted` event. If that's what you do, and you put a breakpoint on that line, does it get hit in debugger? – noseratio Sep 27 '13 at 07:27
  • I do add a DocumentCompleted event. Yes, the btn.InvokeMember("Click") does get hit in the debugger. It's just that nothing happens. – PiE Sep 27 '13 at 10:29
  • Your code works for me (with a custom local action URL), when invoked from `DocumentComplete`. – noseratio Sep 27 '13 at 12:37
  • Could you show me the code that you are using - just to be clear - Thanks – PiE Sep 27 '13 at 13:09
  • The code is really simple: http://pastebin.com/M08bxjwP. `test.html` contains HTML copied from your question (besides URLs). – noseratio Sep 27 '13 at 13:37
  • @Noseratio Still not working with the post url I have - Why would it not work? Do you know what the problems might be? Is there a workaround - E.g., WebRequest()? – PiE Sep 27 '13 at 15:17
  • Can you try `http://example.com` as form's post URL, will it navigate there? If so, obviously the problem is with the target URL, perhaps it returns a malformed HTTP response. – noseratio Sep 27 '13 at 15:25
  • @Noseratio - I give up - Here is the exact Url that I am trying to post to: https://post.craigslist.org/u/MoAhBkcn4xGGb33IVInhMw/4wuhd using the code above - If you have any suggestions, that would be much appreciated - Thanks - PMF – PiE Sep 27 '13 at 19:22
  • Did the code I posted in the answer work for you at all? – noseratio Sep 30 '13 at 01:23
  • @Noseratio - It did not work at all - For that Url, in fact with the registry changes with WebBrowser, it caused the WebBrowser control to work quite differently than it did before the registry changes. In fact, I had to rollback to a previous checkpoint of the OS to get the WebBrowser control to work as it did before. So, to solve the problem, I utlized NHtmlUnit (i.e., the .NET version) and accessed the buttons via XPath commands. I think .NET WebBrowser control is defective – PiE Oct 02 '13 at 09:44

3 Answers3

6

The following code works for me, using the live form action URL from the question comments, tested with IE10. Try it as is. If it works for you as well, feel free to use it as a template for your web automation tasks. A couple of points:

  • FEATURE_BROWSER_EMULATION is used to make sure the WebBrowser behaves in the same way as standalone IE browser (or as close as possible). This is a must for almost any WebBrowser-based project. I believe that's what should help to solve the original problem on your side.

  • Asynchronous code is used to improve the automation logic reliability, add support timeouts and cancellation and promote natural linear code flow (using async/await).

C#:

using Microsoft.Win32;
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WebAutomation
{
    // http://stackoverflow.com/q/19044659/1768303

    public partial class MainForm : Form
    {
        WebBrowser webBrowser;

        // non-deterministic delay to let AJAX code run
        const int AJAX_DELAY = 1000;

        // keep track of the main automation task
        CancellationTokenSource mainCts;
        Task mainTask = null;

        public MainForm()
        {
            SetBrowserFeatureControl(); // set FEATURE_BROWSER_EMULATION first

            InitializeComponent();

            InitBrowser();

            this.Load += (s, e) =>
            {
                // start the automation when form is loaded
                // timeout the whole automation task in 30s
                mainCts = new CancellationTokenSource(30000);
                mainTask = DoAutomationAsync(mainCts.Token).ContinueWith((completedTask) =>
                {
                    Trace.WriteLine(String.Format("Automation task status: {0}", completedTask.Status.ToString()));
                }, TaskScheduler.FromCurrentSynchronizationContext());
            };

            this.FormClosing += (s, e) =>
            {
                // cancel the automation if form closes
                if (this.mainTask != null && !this.mainTask.IsCompleted)
                    mainCts.Cancel();
            };
        }

        // create a WebBrowser instance (could use an existing one)
        void InitBrowser()
        {
            this.webBrowser = new WebBrowser();
            this.webBrowser.Dock = DockStyle.Fill;
            this.Controls.Add(this.webBrowser);
            this.webBrowser.Visible = true;
        }

        // the main automation logic
        async Task DoAutomationAsync(CancellationToken ct)
        {
            await NavigateAsync(ct, () => this.webBrowser.Navigate("http://localhost:81/test.html"), 10000); // timeout in 10s
            // page loaded, log the page's HTML
            Trace.WriteLine(GetBrowserDocumentHtml());

            // do the DOM automation
            HtmlElementCollection all = webBrowser.Document.GetElementsByTagName("button");
            // throw if none or more than one element found
            HtmlElement btn = all.Cast<HtmlElement>().Single(
                el => el.InnerHtml == "ACCEPT the terms of use");

            ct.ThrowIfCancellationRequested();

            // simulate a click which causes navigation
            await NavigateAsync(ct, () => btn.InvokeMember("click"), 10000); // timeout in 10s

            // form submitted and new page loaded, log the page's HTML
            Trace.WriteLine(GetBrowserDocumentHtml());

            // could continue with another NavigateAsync
            // othrwise, the automation session completed
        }

        // Get the full HTML content of the document
        string GetBrowserDocumentHtml()
        {
            return this.webBrowser.Document.GetElementsByTagName("html")[0].OuterHtml;
        }

        // Async navigation
        async Task NavigateAsync(CancellationToken ct, Action startNavigation, int timeout = Timeout.Infinite)
        {
            var onloadTcs = new TaskCompletionSource<bool>();
            EventHandler onloadEventHandler = null;

            WebBrowserDocumentCompletedEventHandler documentCompletedHandler = delegate
            {
                // DocumentCompleted may be called several time for the same page,
                // beacuse of frames
                if (onloadEventHandler != null || onloadTcs == null || onloadTcs.Task.IsCompleted)
                    return;

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

            using (var cts = CancellationTokenSource.CreateLinkedTokenSource(ct))
            {
                if (timeout != Timeout.Infinite)
                    cts.CancelAfter(Timeout.Infinite);

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

        // Browser feature conntrol
        void SetBrowserFeatureControl()
        {
            // http://msdn.microsoft.com/en-us/library/ee330720(v=vs.85).aspx

            // FeatureControl settings are per-process
            var fileName = System.IO.Path.GetFileName(Process.GetCurrentProcess().MainModule.FileName);

            // make the control is not running inside Visual Studio Designer
            if (String.Compare(fileName, "devenv.exe", true) == 0 || String.Compare(fileName, "XDesProc.exe", true) == 0)
                return;

            SetBrowserFeatureControlKey("FEATURE_BROWSER_EMULATION", fileName, GetBrowserEmulationMode()); // Webpages containing standards-based !DOCTYPE directives are displayed in IE10 Standards mode.
        }

        void SetBrowserFeatureControlKey(string feature, string appName, uint value)
        {
            using (var key = Registry.CurrentUser.CreateSubKey(
                String.Concat(@"Software\Microsoft\Internet Explorer\Main\FeatureControl\", feature),
                RegistryKeyPermissionCheck.ReadWriteSubTree))
            {
                key.SetValue(appName, (UInt32)value, RegistryValueKind.DWord);
            }
        }

        UInt32 GetBrowserEmulationMode()
        {
            int browserVersion = 7;
            using (var ieKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Internet Explorer",
                RegistryKeyPermissionCheck.ReadSubTree,
                System.Security.AccessControl.RegistryRights.QueryValues))
            {
                var version = ieKey.GetValue("svcVersion");
                if (null == version)
                {
                    version = ieKey.GetValue("Version");
                    if (null == version)
                        throw new ApplicationException("Microsoft Internet Explorer is required!");
                }
                int.TryParse(version.ToString().Split('.')[0], out browserVersion);
            }

            UInt32 mode = 10000; // Internet Explorer 10. Webpages containing standards-based !DOCTYPE directives are displayed in IE10 Standards mode. Default value for Internet Explorer 10.
            switch (browserVersion)
            {
                case 7:
                    mode = 7000; // Webpages containing standards-based !DOCTYPE directives are displayed in IE7 Standards mode. Default value for applications hosting the WebBrowser Control.
                    break;
                case 8:
                    mode = 8000; // Webpages containing standards-based !DOCTYPE directives are displayed in IE8 mode. Default value for Internet Explorer 8
                    break;
                case 9:
                    mode = 9000; // Internet Explorer 9. Webpages containing standards-based !DOCTYPE directives are displayed in IE9 mode. Default value for Internet Explorer 9.
                    break;
                default:
                    // use IE10 mode by default
                    break;
            }

            return mode;
        }
    }
}

The content of http://localhost:81/test.html:

<!DOCTYPE html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
</head>
<body>
    <form action="<the URL from OP's comments>" method="post">
        <input name="StepCheck" value="U2FsdGVkX18zMTk5MzE5OUgFyFgD3V5yf5Rwbtfhf3gjdH4KSx4hqj4vkrw7K6e-" type="hidden">
        <button type="submit" name="continue" value="y">ACCEPT the terms of use</button>
        <button type="submit" name="continue" value="n">DECLINE the terms of use</button>
    </form>
</body>
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • May I ask you kindly what the code `this.FormClosing += (s, e) =>` does? Thanks. – Sabuncu Jan 26 '14 at 13:26
  • @Sabuncu, it handles `FormClosing` event to cancel any pending background navigation operation. – noseratio Feb 06 '14 at 03:05
  • 1
    @Noseratio The solution is so obscure that it would have taken me ages longer to find. My upvote and gratitude =) – Fung May 12 '16 at 03:26
  • May I ask you if HTML Form Tag contains a Relative path the code is not working?is there any issue with that? can you please provide ans for this Question : https://stackoverflow.com/questions/69567116/web-browser-control-of-windows-form-application-not-handle-half-url-path-in-form – Hardik Oct 14 '21 at 08:08
  • @Hardik can't answer this off top of my head, but the answers/comments you got there are good. You really should be using WebView2 vs IE11 `WebBrowser` in 2021. – noseratio Oct 15 '21 at 03:32
2

This works for me as follow. may that would be useful for someone.

First I create an event handler for the button element when got focus. Once all the other form element are filled up with the appropriate values, You should give the focus to the button as follow:

HtmlElement xUsername = xDoc.GetElementById("username_txt");
HtmlElement xPassword = xDoc.GetElementById("password_txt");
HtmlElement btnSubmit = xDoc.GetElementById("btnSubmit");
if (xUsername != null && xPassword != null && btnSubmit != null)
{
    xUsername.SetAttribute("value", "testUserName");
    xPassword.SetAttribute("value", "123456789");
    btnSubmit.GotFocus += BtnSubmit_GotFocus;
    btnSubmit.Focus();
}

Then event handler implementation would be like this:

private void BtnSubmit_GotFocus(object sender, HtmlElementEventArgs e)
{
    var btnSubmit = sender as HtmlElement;
    btnSubmit.RaiseEvent("onclick");
    btnSubmit.InvokeMember("click");
}
Ali Tabandeh
  • 86
  • 1
  • 3
1

In my case I also couldn't get element clicked by simply invoking Click method of the found element. What worked is similar solution that Ali Tabandeh listed in his answer above:

  1. find the needed html element
  2. define proper GotFocus event handler
  3. then invoke Focus method of the found element.

Event handler for GotFocus should

  1. RaiseEvent "onclick"
  2. Invoke "click" method

The problem was that this worked in my case from 3rd time (3 times needed to call htmlelement.Focus()).

Tigran
  • 21
  • 6