1

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

ShamilS
  • 1,410
  • 2
  • 20
  • 40
  • Which part of the code do you want to make async? why do you think you need async code? – L.B Jan 12 '14 at 23:00
  • @L.B. I would like to make async the following code line: `object value = this.webBrowser1.Document.InvokeScript("eval", new[] { codeString });` to make this code non-blocking. (A variable to get returned value can be defined on class level.) Yes, .InvokeScript/jQuery return value quickly, but in general case .InvokeScript/jQuery manipulation - looking for a value could take more time. Also there could be several "value look-up" working threads started simultaneously and WebBrowser control is running in STA so working threads will block each-other(?)/make hosting form unresponsive. – ShamilS Jan 12 '14 at 23:36
  • ShamilS, How about posting a self-containing, reproducable, minimal case showing your problem, so that we can avoid talking on hypotatical cases. – L.B Jan 12 '14 at 23:46
  • @L.B. I Have extended my posting with two async solutions. I'm looking for others, which would use different approaches. Thank you. P.S. Sorry, if the topic still looks a bit hypothetical. If that would be an issue I should probably better close it. – ShamilS Jan 13 '14 at 01:39
  • I don't think what you want to do is possible. But you can create the browser in a separate thread as [here](http://stackoverflow.com/questions/18675606/perform-screen-scape-of-webbrowser-control-in-thread) or [here](http://stackoverflow.com/questions/16715620/how-should-i-properly-invoke-a-webbrowser-using-multiplethreads) – L.B Jan 13 '14 at 16:38
  • @L.B., Thank you for the links, I'm doing instantiation of WebBrowser control(s) in dedicated STA thread(s) routinely here for quite some time. But what is not possible? To find a third variant of an async solution? – ShamilS Jan 14 '14 at 14:45
  • No, to create the webbrowser control in one thread and to run `InvokeScript` in another thread. – L.B Jan 14 '14 at 18:46
  • @L.B, But I have no any plans to "*create webbrowser control in one thread and to run `InvokeScript` in another thread*". – ShamilS Jan 14 '14 at 19:02
  • Then see the links I posted previously. Create a browser in background thread and do whatever you like with it. Just do not try to access that browser directly. Only the thread created the browser should do it. – L.B Jan 14 '14 at 19:13
  • @L.B, Thank you. As I have noted here earlier I have working experience in creating and handling WebBrowser control instances in dedicated STA threads. – ShamilS Jan 14 '14 at 20:01

1 Answers1

1

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);
    }
}
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • Thank you for your edits. Please note that all the three async code variants you proposed would **block** UI thread in the case `this.webBrowser1.Document.InvokeScript(...)` would be a part of a long running procedure. – ShamilS Jan 14 '14 at 15:17
  • Your question does't say anything about long running procedure. For that case, there is `Progress` pattern. Anyway, Invoking JavaScript inside `WebBrowser` is no different from updating any other UI element, for that matter. Why do you call that *using jQuery asynchronously*? Calling `Control.Invoke` from another thread is **not** an asynchronous operation, unless you use `Control.BeginInvoke`. – noseratio Jan 14 '14 at 19:23
  • Yes, calling `Control.Invoke` is not an asynchronous operation. I see, the topic's title, my code samples and questions seems to be (a bit) misleading. The title should have been written something like that: "*How to implement an asynchronous (potentially long running and non-blocking UI thread) C# procedure issuing/evaluating a set of jQuery selectors to get (text) values of a set of elements of a .NET Windows Forms WebBrowser control's document*". Anyway, I'd like to thank you for all your valuable inputs and to accept your current answer. Agreed? – ShamilS Jan 14 '14 at 21:59
  • @ShamilS, no problem, glad if my answer has helped. – noseratio Jan 14 '14 at 22:21
  • @ShamilS, I've posted an update which shows how it can possibly be done for a long-running operation. – noseratio Jan 15 '14 at 09:39
  • Thank you. What happens in your sample in the case a subsequent cycle's .BeginInvoke is issued before the previous one completes? Does subsequent cycle's .BegingInvoke get queued or does the worker thread get blocked until previous .BeginInvoke completes? Probably not a big issue with subsequent .BeginInvoke blocking/serialization in a worker thread if it happens - I'm just wondering. Also if I wanted to get a return value from invoked script I should use IAsyncResult with .BeginInvoke and .EndInvoke before the subsequent cycle's call to the .BeginInvoke? – ShamilS Jan 15 '14 at 11:17
  • @ShamilS, all updates queued with `BeginInvoke` will be processed on the UI thread before the code inside button_Click continues after `await Task`. They're posted to the same message queue of the UI thread and processed in the same order. `BeginInvoke` doesn't block the calling thread, it uses `PostMessage` API behind the scene, which is asynchronous. You could call `BeginInvoke` multiple times, all callbacks will be queued without blocking. OTOH, you could indeed use `EndInvoke` to get the result, but that would block the calling worker thread (similar to `Invoke`). – noseratio Jan 15 '14 at 12:07
  • 1
    Thank you, your explanations and recommendations are very helpful. – ShamilS Jan 16 '14 at 10:05