4

I'm using a WebBrowser control to test a form submission, in this particular case the action is an MVC action that redirects to another result page.

The code is really simple...

 void _browser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
 {
     var parentForm = _my_find_form_function();

     parentForm.SetAttribute("action", "http://localhost/tests/TestSubmission");
     parentForm.InvokeMember("submit");
 }

After submitting (which works as expected) i would have thought the next documentcompleted event would be fired after the redirected page is loaded. instead, the same page (with the form) is loaded.

Is there something i'm missing?

Thanks

Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
sambomartin
  • 6,663
  • 7
  • 40
  • 64
  • Does your page have frames? – noseratio Feb 13 '14 at 11:29
  • iframes possibly in use for social network links? why's that? – sambomartin Feb 13 '14 at 12:43
  • 1
    How many times do you see `DocumentCompleted` fired after the simulated click? If more than once, check [this](http://stackoverflow.com/a/21715946/1768303). – noseratio Feb 13 '14 at 12:48
  • thanks for link. i kind of sussed that out and ended up filtering the event using the URL. the problem i have now is that the submit doesn't redirect the page.. any further ideas? – sambomartin Feb 13 '14 at 14:45
  • What if you click manually instead of `parentForm.InvokeMember("submit")`, does it work correctly then? – noseratio Feb 13 '14 at 15:20
  • yes it does, normal submit by clicking the button works. maybe i should emulate click? – sambomartin Feb 13 '14 at 15:57
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/47439/discussion-between-sambomartin-and-noseratio) – sambomartin Feb 13 '14 at 16:16
  • yes, you could try simulating a button click. I don't have any other ideas, either – noseratio Feb 13 '14 at 19:05
  • Ok it worked to a degree. emulating click on first submit input on the form actually submitted data but again no redirect. like a return false on event handler. hmmm – sambomartin Feb 14 '14 at 00:20
  • 2
    thanks @Noseratio managed to adapt that link, now have an async method that invokes member on element and waits for content to finish being modified. works really well! thanks for comments – sambomartin Feb 14 '14 at 09:48
  • 1
    if anyone reads this in the future, msg me and I'll share my method. the scenario is where i needed to load a page, populate a form and automatically submit the data. the problem was the form's page and the redirected page both contained iframes which made it difficult to identify when each of the pages had finished loading - taking into account any on-page document ready ajax scripts that manipulated the dom. – sambomartin Feb 14 '14 at 12:28
  • You can just post your own answer, someone may appreciate this later. – noseratio Feb 14 '14 at 12:30
  • @sambomartin Can you please post your answer? – LCJ Apr 02 '14 at 15:03
  • 1
    @Lijo i'll post later this eve – sambomartin Apr 03 '14 at 15:00
  • @sambomartin .. Thanks..Looking forward for your answer – LCJ Apr 04 '14 at 12:31

1 Answers1

1

Ok, sorry for delay, I moved on to something else...

Anyway, it's REALLY rough but you should be able to pick the bits you need.

The original idea was from Noseratio found here.

The raisedynamicevent method, works in a similar way to async nav, by monitoring the state of the document after event. when ok again, returns. should deal with ajax stuff. needs re factoring a bit and probs lots wrong with it but will hopefully help someone.

/// the _profileQueue was a queue of URLs i wanted to nav through and find an 
/// form elem and "click" the submit button on
private async void Next()
    {
        Submission res = null;
        if (_profileQueue.TryDequeue(out res))
        {
            // dirty, but hold the details of the url i'm navigating to in the Tag
            _browser.Tag = res;

            var cts = new CancellationTokenSource(Properties.Settings.Default.BrowserNavigationTimeout); // cancel in 10s
            var html = await LoadDynamicPage(res.SiteProfile.URL, cts.Token);

            // this parses the dom once loaded (awaits for the page)
            ProcessSiteProfile();
            Next();
        }
    }

    // navigate and download 
    async Task<string> LoadDynamicPage(string url, CancellationToken token)
    {
        // navigate and await DocumentCompleted
        var tcs = new TaskCompletionSource<bool>();
        WebBrowserDocumentCompletedEventHandler handler = (s, arg) =>
            tcs.TrySetResult(true);

        // i'm keeping the tcs in a concurrentdictionary against the browser object
        // again, this is pretty dirty but obviously felt like i needed it.
        _browserTasks[_browser] = tcs;

        using (token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: true))
        {

            // nav to page async
            this._browser.DocumentCompleted += handler;
            try
            {
                if (!string.IsNullOrWhiteSpace(url))
                {
                    this._browser.Navigate(url);                 
                    await tcs.Task; // wait for DocumentCompleted
                }
            }
            finally
            {
                this._browser.DocumentCompleted -= handler;
            }
        }

        // get the root element
        var documentElement = this._browser.Document.GetElementsByTagName("html")[0];

        // poll the current HTML for changes asynchronosly
        var html = documentElement.OuterHtml;
        while (true)
        {
            // wait asynchronously, this will throw if cancellation requested
            await Task.Delay(Properties.Settings.Default.BrowserNavigationWait, token);

            // continue polling if the WebBrowser is still busy
            if (this._browser.IsBusy)
                continue;

            var htmlNow = documentElement.OuterHtml;
            if (html == htmlNow)
                break; // no changes detected, end the poll loop

            html = htmlNow;
        }

        // consider the page fully rendered 
        token.ThrowIfCancellationRequested();

        // remove from task dictionary
        _browserTasks[this._browser] = null;

        return html;
    }

    async void ProcessSiteProfile()
    {
        // now process submission  

        HtmlElement parentForm = null;

        /////////////////
        // parse dom to find the form you're looking for 
        // couple of helpers below
        ///////////////////////

        parentForm = HtmlElementQuery(_browser.Document, "myTextFieldInput");

        var sub = (_browser.Tag as Submission);

        HtmlDocument doc = _browser.Document;

        if (parentForm != null)
        {               
            var elements = parentForm.GetElementsByTagName("input");
            foreach (HtmlElement el in elements)
            {
                // If there's more than one button, you can check the
                // element.InnerHTML to see if it's the one you want
                if (el.GetAttribute("type").ToLower() == "submit")
                {
                    var cts = new CancellationTokenSource(Properties.Settings.Default.BrowserNavigationTimeout); // cancel in 10s

                    var html = await RaiseDynamicEvent(el, "click", cts.Token);
                }
            }
        }
    }

      // used to raise an event with a dom element that would cause the document to change 
    async Task<string> RaiseDynamicEvent(HtmlElement element, string evt, CancellationToken token)
    {
        // navigate and await DocumentCompleted
        var tcs = new TaskCompletionSource<bool>();
        WebBrowserDocumentCompletedEventHandler handler = (s, arg) =>
            tcs.TrySetResult(true);
        _browserTasks[_browser] = tcs;
        using (token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: true))
        {
            this._browser.DocumentCompleted += handler;

            try
            {
                element.InvokeMember(evt);
                try
                {
                    await tcs.Task; // wait for DocumentCompleted
                }
                catch (TaskCanceledException)
                {
                    // no the end of the world

                }
            }
            finally
            {
                this._browser.DocumentCompleted -= handler;
            }
        }

        // get the root element
        var documentElement = this._browser.Document.GetElementsByTagName("html")[0];

        // poll the current HTML for changes asynchronosly
        var html = documentElement.OuterHtml;
        while (true)
        {
            // wait asynchronously, this will throw if cancellation requested
            await Task.Delay(500, token);

            // continue polling if the WebBrowser is still busy
            if (this._browser.IsBusy)
                continue;

            var htmlNow = documentElement.OuterHtml;
            if (html == htmlNow)
                break; // no changes detected, end the poll loop

            html = htmlNow;
        }

        // consider the page fully rendered 
        token.ThrowIfCancellationRequested();

        // remove from task dictionary
        _browserTasks[this._browser] = null;

        return html;
    }

    // couple of useful helpers

    HtmlElement FindParentByElement(string elementName, HtmlElement element)
    {
        if (element.Parent != null)
        {
            if (element.Parent.TagName.ToLower() == elementName.ToLower())
            {
                return element.Parent;
            }
            else
            {
                return FindParentByElement(elementName, element.Parent);
            }
        }
        else
        {
            return null;
        }
    }

    HtmlElement HtmlElementQuery(HtmlDocument container, string query)
    {
        HtmlElement el = null;
        if (query.StartsWith("#"))
        {
            el = container.GetElementById(query.TrimStart('#'));
        }
        else
        {
            el = container.All[query];
        }

        return el;
    }
Community
  • 1
  • 1
sambomartin
  • 6,663
  • 7
  • 40
  • 64
  • It may be a good idea to include a [reference link to where](http://stackoverflow.com/a/22262976/1768303) you copied and adapted the code from, originally. – noseratio Apr 09 '14 at 06:49
  • 1
    Full credit @Noseratio, I wasn't trying to claim your code, just provide example/adaptation. thanks again! – sambomartin Apr 10 '14 at 11:26