Monday, December 29, 2014

UI Considerations When Using Asynchronous Methods

Our user interface behaves differently if we use asynchronous methods than if we use synchronous methods. We need to pay careful attention to our UI when we use the "await" operator in our code.

Last time, we saw how to consume an awaitable method by using Task directly or by using the "await" operator. What we found is that it is very easy to create methods that run asynchronous code but still look like "normal" code.

You can get the code download for this article (and the previous one) from GitHub: https://github.com/jeremybytes/using-task. Articles in this series are collected here: Exploring Task, Await, and Asynchronous Methods.

[Update 09/2015: A video version of this information is available here: Basic Exception Handling.]

We're making these asynchronous method calls in a WPF application. And with the way that we left things, we've got some weird behavior that we need to deal with. Let's review the code and look at the problem.

Expected Behavior
Here's the method that we created to consume an awaitable method:


With the exception of the "await" operator, this looks pretty normal -- like code that would run sequentially on a single thread. And when we click our button, it does what we expect:

Single Button Click

But this code does *not* run sequentially on a single thread. The "Get" method runs code on a separate thread which keeps our UI thread free to process requests.

Problem Behavior
As a reminder, our "Get" method has a 3 second delay built in. So what happens if we click the button and then click the button *again* before the original method has completed? Let's try it:

2 Button Clicks

If we look at the output, we'll see that each record is in there twice: we can see "John Koenig", "Dylan Hunt", and "John Crichton". And if we scroll down, we'll see the others.

Here's a diagram of what happened in our code:


The first time we click the button, we clear the list box and then call our asynchronous method. Then if we click the button again, we clear the list box (again) and then call our asynchronous method.

After 3 seconds, the first "Get" returns, and we populate the list box with the results. Then the second "Get" method returns, and we put those items into the list box.

Fixing the Behavior
So the problem occurs because we get two sets of data back. We're clearing the list box twice, but we're clearing it *before* any of our data comes back. We could move the "ClearListBox" method below the "await" step. But that would leave our list box populated with whatever was there before while we're waiting for the data. I'd rather have the list box empty to show that something is happening (we might want to add a wait icon as well).

We could call the clear method both before and after the "await" step. But it would be better if we just prevented the user from making 2 different calls to start with.

Disabling the Button
So instead of changing how we're clearing the list box, we'll disable the button so that the user cannot click it twice.

This code is pretty simple:


We just disable the button at the top of the method and enable the button at the bottom of the method. And if we run the application, we'll see that we get the behavior that we expect:

Before Click
After Click (but still waiting for data)
Completed

Because we disable the button while we're waiting for things to complete, we can only click the button one time. This also gives a further visual clue to the user that they shouldn't try to click the button again.

Preparing for Errors
We do want to consider another situation. What if this method throws an exception somewhere along the way? With the way things are set up now, the code that re-enables the button will not get called.

We can fix this pretty easily by adding a try/finally block to our code:


Now even if we get an exception, the button will still be re-enabled.

Notice that we're treating this code exactly like "normal" code. We're wrapping it in a try/finally block, and we're relying on the compiler to take care of the asynchronous parts for us.

We'll spend a lot more time on exceptions and error handling in upcoming articles. That will include things like adding a "catch" block to this code and checking exceptions that may come back from the awaitable method.

The final code for this method is in the 06-UIConsiderations branch: https://github.com/jeremybytes/using-task/tree/06-UIConsiderations.

Fixing the Task Method
We could stop here and be happy with the results. But let's take a closer look at what's happening here. We'll do this by looking at how we would accomplish this functionality by using Task directly.

Last time, we looked at both using "Task" and using "await" so that we could get a better understanding of what's going on. We'll do that same thing here. This will give us an appreciation for what we get for free when we use the "await" operator.

Here's the method that interacts with Task:


This has the same problem as our "await" method had: if we click the button again before the original operation has completed, we end up with multiple result sets in our list box.

The simplest way to fix this is to disable the button at the top of the method and re-enable it in our continuation. Here's what that looks like:


And this works along the "happy path" (meaning nothing unexpected happens). But let's put a little bit of error handling in place.

Limiting Continuations
If we look at the documentation for the "ContinueWith" method, we'll find 40 overloads; so we have a lot of options to choose from. Our current method has the parameters of "Action<T>" (our continuation method) and "TaskScheduler" (which makes sure our continuation runs on the UI thread).

But what if the "Get" method throws an exception. We're not watching for that now. Let's assume that the "Get" method fails. That means we do not have a valid result. So in our continuation where we ask for "t.Result", we would get an exception (since "Result" is invalid).

Let's add a couple more parameters to our method:


This has 2 new parameters. First is a cancellation token. We don't really need it for this implementation, but the overload of "ContinueWith" that we want needs a cancellation token. When we create a cancellation token like this, it is always "false" (meaning, not canceled). We'll look at cancellation in an upcoming article.

[Update 12/30/2014: As Alois Kraus notes in the comments, we can use "CancellationToken.None" instead of creating a new cancellation token. This is a better way to go since we're not using the cancellation token in this instance. Check the GitHub project for the update: https://github.com/jeremybytes/using-task/tree/06-UIConsiderations.]

The other new parameter is the one we want to look at: "TaskContinuationOptions.OnlyOnRanToCompletion". This means that the continuation will *only* run if the Task operation completed successfully (meaning, no exceptions and not canceled). By including this option, we know that the Task completed successfully, and we can safely use the "Result" property of the task.

We'll look at other "TaskContinuationOptions" in future articles. This includes interesting options such as "OnlyOnFaulted", "NotOnFaulted", "NotOnCanceled", and others. Continuations are pretty flexible.

Another Problem
But now we have another problem: we are re-enabling the button inside our continuation, but the continuation only runs in a "success" state. What if we get an exception along the way?

This is the same problem that we had to consider with our "await" method. In that case, we used a try/finally block to make sure that the button was re-enabled. There are a couple of approaches that we can take here. What we'll do is create another continuation for our task:


Now we have a separate continuation that only re-enables the button. And notice that we do not have the restriction of "OnlyOnRanToCompletion". That means that this continuation will run regardless of whether the Task was faulted (i.e., an exception was thrown) or if it was canceled.

We still need the "FromCurrentSynchronizationContext" since we interacting with UI elements (the button) and we need to make sure this runs on the UI thread.

Fixed Task Method
Here's the entire method with the updated continuations:


So we disable the button at the top of the method. Then we have a continuation that *only* runs if the Task completes successfully -- this will update our list box. Then we have another continuation that *always* runs -- this will re-enable our button.

The final code for this method is in the 06-UIConsiderations branch: https://github.com/jeremybytes/using-task/tree/06-UIConsiderations.

And we get the output that we expect. Here's the disabled button after we click the button but before the Task completes:


And here's the re-enabled button when everything is done.


Again, there is some additional error handling that we can do here, and we also need to look at things like "What if the 'Get' method never completes?" So stay tuned for additional articles.

Wrap Up
As we've seen, the "await" operator lets us treat our asynchronous code very much the same as our synchronous code. This is a little dangerous because we might not think of the implications the asynchronous code may have. As we saw here, if we don't watch for it, our users can click a button multiple times and kick off multiple operations. This gives us an invalid output.

But with a little thought, we can guard against these types of situations. It's pretty easy to disable and enable buttons to prevent users from clicking them. And we saw that this is very easy to do when we are using "await". Things get a bit more complicated if we use Task directly. If nothing else, this gives us a better appreciation for the work that the compiler does for us when we use the "await" operator.

Happy Coding!

4 comments:

  1. Nice article. For the Task based continuation expecting a CancellationToken you do not need to create one but you can use CancellationToken.None instead.

    ReplyDelete
    Replies
    1. Thanks for the tip, Alois. That's definitely a better approach. I'll update the code and drop a note in the article.
      -Jeremy

      Delete
  2. How do you expect to set a cancellation on the token source if that is only a local variable inside the event handling code you want to cancel?

    ReplyDelete
    Replies
    1. Hi Brady. In this example, the CancellationTokenSource is a class-level variable. So it is accessible by both the method that calls the asynchronous method (the "Fetch" button) and also by the method that sets the token to the canceled state (the "Cancel" button). Details are in this article: https://jeremybytes.blogspot.com/2015/01/task-and-await-basic-cancellation.html

      This particular example is a bit unrealistic (since we have 2 separate calls to the same method that can cancel it), but normally we would keep a reference to our token source in the calling code so that we can set the canceled state when appropriate.
      -Jeremy

      Delete