-1

Consider an API that returns Tasks with some values.

I want to update the UI based on that values in parallel (when one of the values is ready I want to update it without waiting for the second one assuming the update of each value as its own update method).

public async Task MyFunc()
{
    Task<First> firstTask = MyAPI.GetFirstValue();
    Task<Second> secondTask = MyAPI.GetSecondValue();

    UpdateFirstValueUI(await firstTask)
    UpdateSecondValueUI(await secondTask)
}

the code example will wait for the first value, update the UI, wait for the second value and update the UI again.

What is the best practice for that scenario? I was wondering if ContinueWith is best practice because I mostly see it in legacy code (before there was async-await).

edit with a better example: assuming we have two implementations of that API and the code looks like that

public async Task MyFunc()
{
    Task<First> firstTask = null
    Task<Second> secondTask = null
    if (someCondition) 
    {
        firstTask = MyAPI1.GetFirstValue();
        secondTask = MyAPI1.GetSecondValue();
    }
    else 
    {
        firstTask = MyAPI2.GetFirstValue();
        secondTask = MyAPI2.GetSecondValue();
    }

    UpdateFirstValueUI(await firstTask)
    UpdateSecondValueUI(await secondTask)
}

now as you see I don't want call the update methods in two different branches (assuming we split that method for each API after the branching) so looking for a way to change only the update calls so they could happen in parallel

Eliot
  • 37
  • 5
  • 1
    You can't update the UI from another thread, in any OS - neither Windows nor Mac nor Linux. If you want to display progress from a background thread you can use the `Progress` class. Depending on the UI stack you use (WinForms? WPF? UWP? Xamarin? MAUI?) you could update the ViewModel object and any bound UI elements would update themselves – Panagiotis Kanavos Aug 30 '21 at 14:04
  • What UI stack are you using? The problem may already be solved through data binding – Panagiotis Kanavos Aug 30 '21 at 14:09
  • @CamiloTerevinto - lets say the second value will be ready before the first one, it will wait for the first one even though they are not related, I try to find the way to update first the one who is ready first – Eliot Aug 30 '21 at 14:09
  • @PanagiotisKanavos I'm using WPF, but the UI-updating is not the issue, it could be some things I want to execute in a non-UI context – Eliot Aug 30 '21 at 14:10
  • 1
    @Selvin to avoid waiting for all tasks to complete. – Panagiotis Kanavos Aug 30 '21 at 14:11
  • @LiorSwisa the updating *is* the issue. With non-UI you can use as many tasks as you want, in any combination you want. There's no sync context to. block. Instead of trying to combine different calls at the top level, you could have separate methods that do what you want, and only await the top-level methods – Panagiotis Kanavos Aug 30 '21 at 14:11
  • 1
    `await Task.WhenAll(CallGetFirstValueAndUpdateUI(), CallGetSecondValueAndUpdateUI());` ... where `CallGetFirstValueAndUpdateUI` is defined as `async Task CallGetFirstValueAndUpdateUI() { Task firstTask = MyAPI.GetFirstValue(); UpdateFirstValueUI(await firstTask);}` ... and `CallGetSecondValueAndUpdateUI` in similar way – Selvin Aug 30 '21 at 14:12
  • 1
    https://dotnetfiddle.net/wAHI3h – Selvin Aug 30 '21 at 14:31
  • @Selvin thanks for your answer and the example, it's a good suggestion and it is similar to the code I wrote, but I would prefer to keep the update calls in the top-level method, added a better explained example to my question – Eliot Aug 30 '21 at 14:37
  • 1
    [you can use similar code](https://dotnetfiddle.net/CC2NtM) – Selvin Aug 30 '21 at 14:49

1 Answers1

2

The ContinueWith is a primitive method that has some rare uses in library code, and should generally be avoided in application code. The main problem with using the ContinueWith in your case is that it's going to execute the continuation on a ThreadPool, which is not what you want, because your intention is to update the UI. And updating the UI from any other thread than the UI thread is a no no. It is possible to solve this¹ problem by configuring the ContinueWith with a suitable TaskScheduler, but it's much simpler to solve it with async/await composition. My suggestion is to add the Run method below in some static class in your project:

public static class UF // Useful Functions
{
    public static async Task Run(Func<Task> action) => await action();
}

This method just invokes and awaits the supplied asynchronous delegate. You could use this method to combine your asynchronous API calls with their UI-updating continuations like this:

public async Task MyFunc()
{
    Task<First> task1;
    Task<Second> task2;
    if (someCondition)
    {
        task1 = MyAPI1.GetFirstValueAsync();
        task2 = MyAPI1.GetSecondValueAsync();
    }
    else
    {
        task1 = MyAPI2.GetFirstValueAsync();
        task2 = MyAPI2.GetSecondValueAsync();
    }
    Task compositeTask1 = UF.Run(async () => UpdateFirstValueUI(await task1));
    Task compositeTask2 = UF.Run(async () => UpdateSecondValueUI(await task2));

    await Task.WhenAll(compositeTask1, compositeTask2);
}

This will ensure that the UI will be updated immediately after each asynchronous operation completes.

As a side note, if you have any suspicion that the MyAPI asynchronous methods may contain blocking code, you could offload them to the ThreadPool by using the Task.Run method, like this:

task1 = Task.Run(() => MyAPI1.GetFirstValueAsync());

For a thorough explanation about why this is a good idea, you can check out this answer.

The difference between the built-in Task.Run method and the custom UF.Run method presented above, is that the Task.Run invokes the asynchronous delegate on the ThreadPool, while the UF.Run invokes it on the current thread. If you have any idea about a better name than Run, please suggest. :-)

¹ The ContinueWith comes with a boatload of other problems as well, like wrapping errors in AggregateExceptions, making it easy to swallow exceptions by mistake, making it hard to propagate the IsCanceled status of the antecedent task, making it trivial to leak fire-and-forget tasks, requiring to Unwrap nested Task<Task>s created by async delegates etc.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Another method that you are advised to avoid in application code, along with the `ContinueWith`, is the `ConfigureAwait(false)`. This too is intended mainly for library code. – Theodor Zoulias Aug 31 '21 at 01:50