Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I continue after multiple Tasks without blocking the UI thread?

In my MVVM application my view model calls 3 different service methods, converts the data from each into a common format and then updates the UI using property notification/observable collections etc.

Each method in the service layer starts a new Task and returns the Task to the view model. Here's an example of one of my service methods.

public class ResourceService
{
internal static Task LoadResources(Action<IEnumerable<Resource>> completedCallback, Action<Exception> errorCallback)
{
    var t = Task.Factory.StartNew(() =>
    {
        //... get resources from somewhere
        return resources;
    });

    t.ContinueWith(task =>
    {
        if (task.IsFaulted)
        {
            errorCallback(task.Exception);
            return;
        }
        completedCallback(task.Result);
    }, TaskScheduler.FromCurrentSynchronizationContext());

    return t;
}
}

Here's the calling code and other relevant parts of the view model...

private ObservableCollection<DataItem> Data = new ObservableCollection<DataItem>();

public ICollectionView DataView
{
    get { return _dataView; }
    set
    {
        if (_dataView != value)
        {
            _dataView = value;
            RaisePropertyChange(() => DataView);
        }
    }
}

private void LoadData()
{
    SetBusy("Loading...");

    Data.Clear();

    Task[] tasks = new Task[3]
    {
        LoadTools(),
        LoadResources(),
        LoadPersonel()
    };

    Task.WaitAll(tasks);

    DataView = CollectionViewSource.GetDefaultView(Data);
    DataView.Filter = FilterTimelineData;

    IsBusy = false;
}

private Task LoadResources()
{
    return ResourceService.LoadResources(resources =>
    {
        foreach(var r in resources)
        {
            var d = convertResource(r);
            Data.Add(d);
        }
    },
    error => 
    {
        // do some error handling
    });
}

This almost works but there are a couple of small issues.

Number 1: In the call to SetBusy at the very beginning, before I start any tasks and before I call WaitAll, I set the IsBusy property to true. This should update the UI and show the BusyIndicator control but it's not working. I've also tried adding simple string properties and binding those and they're not being updated either. The IsBusy functionality is part of a base class and works in other view models where I don't have more than one Task running so I don't believe there is an issue with the property notification or data binding in the XAML.

All the data bindings seem to be updated after the whole method has completed. I'm not seeing any "first time exceptions" or binding errors in the output Window which is leading me to believe the UI thread is somehow being blocked before the call to WaitAll.

Number 2: I seem to be returning the wrong Tasks from the service methods. I want everything after WaitAll to run after the view model has converted all the results from all the service methods in the callbacks. However if I return the continuation task from the service method the continuation never gets called and WaitAll waits forever. The strange thing is the UI control bound to the ICollectionView actually displays everything correctly, I assumed this is because Data is an observable collection and the CollectionViewSource is aware of the collection changed events.

like image 587
BenCr Avatar asked Aug 06 '13 22:08

BenCr


People also ask

Is it ever okay block the UI thread?

You should never block UI Thread. When you hold UI Thread for too long, this is when the system will show a dialog saying XXX is not responding and ask user to kill your application.

What is Task continuation?

A continuation task (also known just as a continuation) is an asynchronous task that's invoked by another task, known as the antecedent, when the antecedent finishes.


1 Answers

You can use TaskFactory.ContinueWhenAll to build a continuation that runs when the input tasks all complete.

Task[] tasks = new Task[3]
{
    LoadTools(),
    LoadResources(),
    LoadPersonel()
};

Task.Factory.ContinueWhenAll(tasks, t =>
{
    DataView = CollectionViewSource.GetDefaultView(Data);
    DataView.Filter = FilterTimelineData;

    IsBusy = false;
}, CancellationToken.None, TaskContinuationOptions.None, 
   TaskScheduler.FromCurrentSynchronizationContext());

Note that this becomes simpler if you use C# 5's await/async syntax:

private async void LoadData()
{
    SetBusy("Loading...");

    Data.Clear();

    Task[] tasks = new Task[3]
    {
        LoadTools(),
        LoadResources(),
        LoadPersonel()
    };

    await Task.WhenAll(tasks);

    DataView = CollectionViewSource.GetDefaultView(Data);
    DataView.Filter = FilterTimelineData;

    IsBusy = false;
}

However if I return the continuation task from the service method the continuation never gets called and WaitAll waits forever

The problem is that your continuation task requires the UI thread, and you're blocking the UI thread in the WaitAll call. This creates a deadlock which will not resolve.

Fixing the above should correct this - you'll want to return the Continuation as the Task, as that's what you need to wait for completion - but by using TaskFactory.ContinueWhenAll you free up the UI thread so it can process those continuations.

Note that this is another thing that gets simplified with C# 5. You can write your other methods as:

internal static async Task LoadResources(Action<IEnumerable<Resource>> completedCallback, Action<Exception> errorCallback)
{
  try
  {
    await Task.Run(() =>
    {
        //... get resources from somewhere
        return resources;
    });
  }
  catch (Exception e)
  {
    errorCallback(task.Exception);
  }

  completedCallback(task.Result);
}

That being said, it's typically better to write the methods to return a Task<T> instead of providing callbacks, as that simplifies both ends of the usage.

like image 169
Reed Copsey Avatar answered Sep 26 '22 15:09

Reed Copsey