1

I have a small windows forms app with a webbrowser control (here: wb) which performs some tasks like logging in on a portal, searching a project, changing some properties/checkboxes and submit a form. Each step that is done is announced by changing a label's text so that the user can see what is currently done.

What's the best way to handle the interaction with the webbrowser control so that the UI does not get blocked?

I tried it this way:

private async void buttonStart_Click(object sender, EventArgs e)
{
    //Here it is still possible to access the UI thread
    buttonStart.Enabled = false;

    //do the background work
    await Task.Run(() =>
    {
        Login();
        CallProject();
        TriggerTask1();
        TriggerTask2();
    });

    //back on UI thread
    buttonStart.Enabled = false;
}

private void Login()
{
    //may not access labelProgress from this thread
    //labelProgress.Text = "logging in";

    Invoke(new Action(() =>
    {
        //can access labelProgress here
        labelProgress.Text = "logging in";

        //can access wb here
        wb.Navigate(Settings.Default.LoginUrl);
        WebBrowserTools.Wait(wb);

        wb.Document.GetElementById("username").InnerText = Settings.Default.LoginUsername;
        wb.Document.GetElementById("password").InnerText = Settings.Default.LoginPassword;
        wb.Document.Forms[0].InvokeMember("submit");
        WebBrowserTools.Wait(wb);

        labelProgress.Text = "logged in successfully";
    }));
}

Each access to the webbrowser or any other ui control like the labelProgress must be wrapped in Invoke(new Action(() => {}));. Is there a better way (using modern versions of .NET Framework >= 4.5)?

Oliver Kötter
  • 1,006
  • 11
  • 29
  • `Invoke` calls on the UI thread, which will in turn block the UI. You just figured out a fancy way to block. How about only `Invoke`ing the UI parts and asynchronously call `WebBrowserTools.Wait` from a task? That seems to be the blocking call. – Ron Beyer Feb 19 '18 at 13:28
  • 2
    You are requesting to do `Login` on a threadpool thread but within `Login` you are stating to do everything on the UI thread. What is the point of this? – CodingYoshi Feb 19 '18 at 13:33
  • Well as I mentioned I am looking for best practices. So thanks to CodingYoshi for pointing out that my code is bad. And thanks to Ron-Beyer for a nice idea. – Oliver Kötter Feb 19 '18 at 13:39
  • You need to figure out what can truly be done asynchronously. For example, during login the only part which can be done asynchronously is when the request is being sent to a server or a database to authenticate the user. The next decision you need to make is when that work is being done asynchronously, what will the UI thread be doing? Maybe you do not want to allow anything to be done except for showing a progressbar "authenticating...". Or maybe you want to show a progress bar but also allow the user to click on other parts in the UI. – CodingYoshi Feb 19 '18 at 13:48
  • Asynchronous coding requires good careful planning. The technicalities are easy but it is the planning which is hard. Thus create a plan and then try to code it. If you have issues, tell us what your plan is and what is not working. – CodingYoshi Feb 19 '18 at 13:50
  • This may help: https://stackoverflow.com/a/19063643/1768303 – noseratio Feb 19 '18 at 17:10

1 Answers1

2

You are using async-await in a way that was not intended.

This interview with Eric Lippert helped me to understand async-await. Search somewhere in the middle for async-await.

He compared async-await with a cook having to cook breakfast. After he put on the kettle to boil water for the tea he could decide not to do anything, until the water boils, after which he makes the tea and start toasting bread. Don't do anything until the bread has been toasted, etc.

It would be much faster if instead of doing nothing, whenever the cook would be waiting for another process to end he would start doing other things instead, before waiting for the first thing to complete:

Start boiling water
Start toasting bread
wait until the water boils
use the boiling water to start making tea
...

You'll see async-await typically in situations where a thread orders some process to do something that takes some time that the thread can't do itself: fetch data from a database, write data to a file, fetch data from the internet: actions where the thread can't do anything but wait.

You start your procedure correctly. Your cook does some preparations, but then he decides he should hire another cook to do the work for him while your cook does nothing but wait until the other cook finished working. Why isn't your cook doing this?

A more typical async-await would be like follows:

async void buttonStart_Click(object sender, EventArgs e)
{
    buttonStart.Enabled = false;

    await LoginAsync();
    await CallProjectAsync();
    await TriggerTask1Async();
    await TriggerTask2Async();

    buttonStart.Enabled = false;
}

Normally this would work if you used objects that support async-await. For every async function you know that somewhere there is a call to an awaitable async function. In fact your compiler will complain if you declare your function async without awaiting any call.

Alas, sometimes your cook can't depend on a process to continue while your cook is doing other things. The reason for this is because the WebBrowser class does not support async-await like the WebClient class does.

This is similar to times like when your cook has to do some heavy processing, while you still want to keep your procedure async-await, like slicing tomatoes: the kettle will boil the water automatically, you need another cook to slice the tomatoes.

That is typically the case when in an async-await scenario you would use Task.Run. In your case, this is for instance within your login function

async void LoginAsync()
{
    // still main thread. Notify the operator:
    labelProgress.Text = "logging in";

    // we start something that takes some time that this cook can't spend:
    // let another cook do the logging in
    await Task.Run( () =>
    {
        wb.Navigate(Settings.Default.LoginUrl);
        WebBrowserTools.Wait(wb);
        wb.Document.GetElementById("username").InnerText = Settings.Default.LoginUsername;
        wb.Document.GetElementById("password").InnerText = Settings.Default.LoginPassword;
        wb.Document.Forms[0].InvokeMember("submit");
        WebBrowserTools.Wait(wb);
    }

    // back in main thread:
    labelProgress.Text = "logged in successfully";
}

So the trick is: use as often async-await prepared classes as possible. Only if your cook has to do something lengthy but he wants to be free for other things, do Task.Run(). await when your cook needs to be certain that the other Cook finished its job. Don't await while you have other things to do. Let your cook be the one who handles the user interface.

Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116