.netwpf

Awaiting Tasks without Async/Await while keeping the UI alive


I "inherited" a WPF application following the MVVM pattern (as far as I know).

In most windows the initial data retrieval is done in the view model like this:

Dim t1 As Task = Task.Factory.StartNew(Of IEnumerable(Of Item))(AddressOf Me.GetItems) _
                             .ContinueWith(Sub(t2 As Task(Of IEnumerable(Of Item)))
                                               For Each item As Item In t2.Result
                                                   _items.Add(New ItemViewModel(item))
                                               Next item
                                           End Sub, TaskScheduler.FromCurrentSynchronizationContext)

_items is an ObservableCollection, and a ListView in the window is bound to it.

Considering a few code comments this is done like this to retrieve the data while the UI is still initializing and loading and so on.

This seems to work as expected.

Now I'd like to show some kind of loading animation while the data is initially retrieved or while the data is refreshed. For that I added the animated control to the XAML and bound it's Visibility to a boolean property in the view model.

When I test this setting and set the boolean property in the view model to True, then my animated control is shown and it's spinning and spinning until I set the boolean property in the view model to False again.

But when I do something like this

Me.ShowBusy = True

<Data Retrieval Code shown above>

Me.ShowBusy = False

the data is still retrieved, but I don't see my animation.

I assume it's because the data retrieval code isn't awaited (as far as I can see), so the switching on and off of the Visibility of the animated control happens so fast, that I don't see anything in the UI.

Is there an easy way to achieve my goal without spreading Asyncs and Awaits in my code?

I'm not very familiar with Async and Await yet and I'd like to read and learn about it first, before I decide about the proper way to use it in this program.

Answers are fine in VB.net or C#.


Edit:

Following Elshasps suggestion I tried a few different things:

First test (left the TaskScheduler.FromCurrentSynchronizationContext where it was):

Dim t1 As Task = Task.Factory.StartNew(Of IEnumerable(Of Item))(AddressOf Me.GetItems) _
                             .ContinueWith(Sub(t2 As Task(Of IEnumerable(Of Item)))
                                               For Each item As Item In t2.Result
                                                   _items.Add(New ItemViewModel(item))
                                               Next item
                                           End Sub, TaskScheduler.FromCurrentSynchronizationContext) _
                             .ContinueWith(Sub() Me.ShowBusy = False)

This showed the animation, but only for a very short time, it seems it's only shown while the loaded data gets shown in the ListView, but not while the data is retrieved from the database).

Second test (put the TaskScheduler.FromCurrentSynchronizationContext at the end again):

Dim t1 As Task = Task.Factory.StartNew(Of IEnumerable(Of Item))(AddressOf Me.GetItems) _
                             .ContinueWith(Sub(t2 As Task(Of IEnumerable(Of Item)))
                                               For Each item As Item In t2.Result
                                                   _items.Add(New ItemViewModel(item))
                                               Next item
                                           End Sub) _
                             .ContinueWith(Sub() 
                                               Me.ShowBusy = False
                                           End Sub, TaskScheduler.FromCurrentSynchronizationContext)

Now I get an exception "This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread" while I add the items to _items.

But I guess I get around this exception reading about it in other articles on Stack Overflow.


Solution

  • You need to change the property value after the Task is executed.

    Something like this:

    Me.ShowBusy = true;
    Task.Run(/* Method that performs data loading */)
        .ContinueWith(_ => Me.ShowBusy = false);
    

    P.S. I still recommend that you learn how to create and use asynchronous methods. They greatly simplify coding, testing, bug detection, etc.