ThommyB
ThommyB

Reputation: 1556

How to use async / await with a method returning an ObservableCollection<T>

I am writing a WPF application using MVVM pattern. I must say I am not (yet) familiar with the .NET async programming model (just learning it) but was writing multi-threaded applications in C/C++ and Java for many years.

The idea is to start long running queries in a separate thread from the UI thread so I have a chance to display a spinner or progress bar while the operation is running.

Most of my internal service methods return collections or other complex types like DTO's etc. The results are usually bound to user controls such as GridViews, ListViews etc. I always get compiler errors saying that my return types do not contain a definition for 'GetAwaiter'. So I must be doing something wrong.

Here is an example of such a method in my ViewModel that calls a service which does a DB query and returns an ObservableCollection.

    private async void GetInvoices()
    {
        IsOperationRunning = true; //Binding for a RadBusyIndicator
        Invoices = await _uiContractService.GetInvoices(SelectedInvoiceState, SelectedSupplierItem.Id, SelectedInvoiceDateFilter, FilterStartDate, FilterEndDate);
        IsOperationRunning = false;
        RaisePropertyChanged(() => Invoices);
        if (Invoices.Count == 0)
            SendUserInfoMsg($"NO invoices matched your search criteria.", UiUserNotificationMessageType.Warning);
    }

The compiler error in this case is: ObservableCollection<InvoiceDto>' does not contain a definition for 'GetAwaiter' and no extension method 'GetAwaiter' accepting a first argument of type 'ObservableCollection<InvoiceDto>' could be found

The Invoices collection and the service are declared as

    public ObservableCollection<InvoiceDto> Invoices { get; set; }
    var _uiContractService = SimpleIoc.Default.GetInstance<UiContractService>();

The signature of the service method is as follows:

public ObservableCollection<InvoiceDto> GetInvoices(InvoiceState invoiceState, Guid supplierId, UiInvoiceDateFilters dateType, DateTime begin, DateTime end)

Any help to guide me into the right direction is greatly appreciated.

Upvotes: 3

Views: 4603

Answers (4)

user6996876
user6996876

Reputation:

You con do a "fake" asynchronous task out of your GetInvoices:

public async Task<ObservableCollection<InvoiceDto>> GetInvoicesAsync(InvoiceState invoiceState, Guid supplierId, UiInvoiceDateFilters dateType, DateTime begin, DateTime end)
{
    return await Task.Run( () =>
    {
         return GetInvoices(invoiceState, supplierId, dateType, begin, end);
     });
}

This could be considered bad code, here it's simply to show how you can make it formally await-able by calling the method with Async suffix

Anyway this is wpf, not a asp.net or a web app. So if it's a pure desktop GUI, it would make sense to run a db task in background. It's more problematic if that were on an application server and not on a client..

    Invoices = await _uiContractService.GetInvoicesAsync(SelectedInvoiceState, SelectedSupplierItem.Id, SelectedInvoiceDateFilter, FilterStartDate, FilterEndDate);

Notice that there is a big difference between a wrong and a not ideal thing and that it is not always the case that you can have immediate support for an upgrade to the latest technology.

Another point that you're not asking but looks odd is how you raise property changed. A much more convenient place is the setter of the property. Then you don't need to call it after an assignment unless you are using the underlying private member.

I would have also cleaned the collection before just to enphasize the work in progress but these are personal preferences.

Upvotes: 0

Stephen Cleary
Stephen Cleary

Reputation: 456507

was writing multi-threaded applications in C/C++ and Java for many years.

This experience is probably misleading you. Asynchronous code is very different than multithreaded code.

await has nothing to do with threads. Or put another way, it doesn't increase the number of threads used; it decreases them.

I recommend you first read my async intro to get an idea of what async/await actually do.

Then, you can proceed one of two ways. The first way is not ideal, but is what you actually asked for:

The idea is to start long running queries in a separate thread from the UI thread so I have a chance to display a spinner or progress bar while the operation is running.

The first approach is to multithread. In this case, you can use async/await as a convenient way to retrieve results from an operation running on another thread (including clean, correct exception stacks):

private async Task GetInvoicesAsync()
{
  IsOperationRunning = true; //Binding for a RadBusyIndicator
  Invoices = await Task.Run(() => _uiContractService.GetInvoices(SelectedInvoiceState, SelectedSupplierItem.Id, SelectedInvoiceDateFilter, FilterStartDate, FilterEndDate));
  IsOperationRunning = false;
  RaisePropertyChanged(() => Invoices);
  if (Invoices.Count == 0)
    SendUserInfoMsg($"NO invoices matched your search criteria.", UiUserNotificationMessageType.Warning);
}

Note that it is the Task.Run that introduces multithreading - it schedules work to the thread pool. The async/await is how the UI thread pretends the operation is asynchronous.

This can be called from an event handler (or ICommand) as such:

public async void EventHandler(object sender, ...)
{
  await GetInvoicesAsync();
}

Note that only event handlers should be async void. See my article on async best practices for more information.


But the first approach isn't ideal, because it's not really asynchronous. It's what I call "fake-asynchronous"; that is, the UI is pretending it's asynchronous, but the actual operation is just synchronously blocking another thread.

When writing true asynchronous code, it's easier to start at the other end - not the top-level UI methods, but the low-level actual API calls. If your services are HTTP services, look into HttpClient; if they're WCF services, regenerate your proxies with asynchronous APIs enabled.

Once you have the lowest-level asynchronous APIs available, start using them. Eventually, you'll end up with an asynchronous GetInvoices:

public async Task<List<InvoiceDto>> GetInvoicesAsync(InvoiceState invoiceState, Guid supplierId, UiInvoiceDateFilters dateType, DateTime begin, DateTime end);

(note that I removed ObservableCollection from your service layer since it is a UI-oriented type)

Then you can call this method as such:

private async Task GetInvoicesAsync()
{
  IsOperationRunning = true; //Binding for a RadBusyIndicator
  Invoices = new ObservableCollection<InvoiceDto>(await _uiContractService.GetInvoicesAsync(SelectedInvoiceState, SelectedSupplierItem.Id, SelectedInvoiceDateFilter, FilterStartDate, FilterEndDate));
  IsOperationRunning = false;
  RaisePropertyChanged(() => Invoices);
  if (Invoices.Count == 0)
    SendUserInfoMsg($"NO invoices matched your search criteria.", UiUserNotificationMessageType.Warning);
}

This time, there's no Task.Run or any other explicit usage of thread pool threads.

For more details, see the "Transform" parts of my brownfield async article.

Upvotes: 6

valdetero
valdetero

Reputation: 4652

Is GetInvoices doing anything async? If it is, its signature should be:
public async Task<ObservableCollection<InvoiceDto>> GetInvoices(InvoiceState invoiceState, Guid supplierId, UiInvoiceDateFilters dateType, DateTime begin, DateTime end).
If you aren't, then you shouldn't worry about async and remove the async/await keywords from the outer GetInvoices method.

If you want to await something, it should return Task or Task<T>. (I won't even go into async void methods).

Upvotes: 1

William Xifaras
William Xifaras

Reputation: 5312

Your code should return a Task.

public Task<ObservableCollection<InvoiceDto>> GetInvoices(InvoiceState invoiceState, Guid supplierId, UiInvoiceDateFilters dateType, DateTime begin, DateTime end)
{
  // your code
}

Upvotes: 1

Related Questions