Search code examples
c#jqueryasynchronouswebbrowser-controlinvokescript

Using jQuery to asynchronously get text value of a DIV element of a .NET Windows Forms WebBrowser control's document


I have the following C# code to get a DIV's html element text value from a .NET Windows Forms WebBrowser control:

private void cmdGetText_Click(object sender, EventArgs e)
{
    string codeString = string.Format("$('#testTextBlock').text();");
    object value = this.webBrowser1.Document.InvokeScript("eval", new[] { codeString });
    MessageBox.Show(value != null ? value.ToString() : "N/A", "#testTextBlock.text()");
}

private void myTestForm_Load(object sender, EventArgs e)
{
    webBrowser1.DocumentText =
        @"<!DOCTYPE html><html>
            <head>
                <script src='http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js'></script>
            </head>  
            <body>
                <div id='testTextBlock'>Lorem ipsum dolor sit amet, consectetur adipisicing elit...</div>
            </body>
            </html>";  
}

It works well. It works synchronously.

Here is the first asynchronous variation of cmdGetText_Click method:

private async void cmdGetText_Click(object sender, EventArgs e)
{
    string codeString = string.Format("$('#testTextBlock').text();");

    object value = await Task.Factory.StartNew<object>(() =>
    {
        return this.Invoke(
                new Func<object>(() =>
                {
                    return 
                       this.webBrowser1.Document
                        .InvokeScript("eval", new[] { codeString });
                }));
    });

    MessageBox.Show(value != null ? value.ToString() : "N/A", "#myTestText.text()");
}

And here is the second asynchronous variation of cmdGetText_Click method:

[PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
[System.Runtime.InteropServices.ComVisibleAttribute(true)]
public partial class myTestForm : Form {
...

private async void cmdGetText_Click(object sender, EventArgs e)
{
    webBrowser1.ObjectForScripting = this;
    string codeString = string.Format("window.external.SetValue($('#testTextBlock').text());");

    await Task.Run(() =>
    {
        this.Invoke((MethodInvoker)(()=>{this.webBrowser1.Document.InvokeScript("eval", new[] { codeString });})); 
    });
}

public void SetValue(string value)
{
    MessageBox.Show(value != null ? value.ToString() : "N/A", "#myTestText.text()");
}

Question: Are there any other practical asynchronous variations of original cmdGetText_Click method, variations which would use some other approaches than presented here? If you post them here could you please post also your reasons why would you prefer your approach of a coding solution of the subject task.

Thank you.

[UPDATE]

Here is a screenshot demonstrating that WebBrowser control is accessed in the first sample/async variant from UI thread.

enter image description here

[UPDATE]

Here is a screenshot demonstrating that WebBrowser control is accessed in the second sample/async variant from UI thread.

enter image description here


Solution

  • In both cases (either Task.Factory.StartNew or Task.Run), you start a new task on a pool thread, from the UI thread, only to synchronously call back the UI thread via Control.Invoke.

    Essentially, the same "asynchronous" invocation can be done without the overhead of thread switching and inter-thread marshaling:

    await Task.Factory.StartNew(
        () => {
            this.webBrowser1.Document.InvokeScript(...);
        },
        CancellationToken.None,
        TaskCreationOptions.None,
        TaskScheduler.FromCurrentSynchronizationContext());
    

    It can also be done like this:

    await Task.Yield();
    this.webBrowser1.Document.InvokeScript(...);
    

    Or, without await, like this:

    this.webBrowser1.BeginInvoke(new Action( ()=> 
        this.webBrowser1.Document.InvokeScript(...) ));
    

    In either case, the usefulness of the above constructs is questionable: they all just execute a callback on a future iteration of the UI thread message loop, same as your original await Task.Run( () => this.Invoke(...) ) does.

    [UPDATE] In case you start a long-running operation on a pool thread, and would like to update the UI asynchronously while continuing doing the work, the code might look like this:

    private async void button_Click(object sender, EventArgs e)
    {
        try
        {
            // start and await the background task
            var words = new String[] { "fire", "water", "air" };
    
            await Task.Run(() =>
            {
                // do some CPU-bound work, e.g. find synonyms of words
                foreach (var word in words)
                {
                    // do the next piece of work and get the result
                    var synonyms = FindSynonyms(word);
    
                    // queue an async UI update
                    var wordArg = word;
                    var synonymsArg = String.Join(",", synonyms);
    
                    this.webBrowser.BeginInvoke(new Action(() =>
                    {
                        this.webBrowser.Document.InvokeScript("updateSynonyms",
                            new object[] { wordArg, synonymsArg });
                    }));
                }
            });
        }
        catch (Exception ex)
        {
            // catch all exceptions inside this "async void" event handler
            MessageBox.Show(ex.Message);
        }
    }