Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Async-await seems to use the UI thread

In a view-model I use a factory:

private async Task<BaseData> InitializeAsync()
{
    await InstancesAsync();
    await ProjectsAsync();
    await AdminAsync();
    return this;
}
public static async Task<BaseData> CreateAsync()
{
    var ret = new BaseData();
    return await ret.InitializeAsync();
}

The awaited methods are rather staightforward, with e.g.

 var instances = await TaskEx.Run(new Func<List<string>>(() => Agent.GetInstances()));

In the wpf view I want to set the DataContext in the constructor:

Loaded += delegate
{
    Dispatcher.Invoke(new Action(async () => { DataContext = await BasisGegevens.CreateAsync(); }));
};

Although it works, I feel rather uncomfortable because the UI thread is used everywhere, also after the callbacks when the awaits complete. What am I missing?

Also I don't understand how to use the factory pattern for the DataContext because without the Invoke above I get the error that a different thread owns the object.

EDIT: using the ideas of Mr. Cleary I get:

Loaded += async (object sender, RoutedEventArgs e) =>
          { DataContext = await BaseData.CreateAsync(); };
public static Task<BaseData> CreateAsync()
{
    var ret = new BaseData();
    return ret.InitializeAsync();
}
private async Task<BaseData> InitializeAsync()
{
    // UI thread here
    await InstancesAsync().ConfigureAwait(false);
    // thread 'a' here
    await ProjectsAsync().ConfigureAwait(false);
    // thread 'a' sometimes 'b' here
    await AdminAsync().ConfigureAwait(false);
    // thread 'a' or 'b' here
    return this;
}

This works fine, except I cannot understand how ConfigureAwait(false) works.
Inside the method InstancesAsync() I have the awaited task:
var instances = await TaskEx.Run(new Func<List<string>>(() => Agent.GetInstances()));
After awaiting the repsonse, I return in the UI thread - I never expected that to happen!
Note that ProjectsAsync() and AdminAsync() behave the same, although they start on a worker (or background) thread!
I thougth that ConfigureAwait(true) has the effect of returning in the calling thread (in my case UI thread). I tested that and it is so.
Why do I see this with ConfigureAwait(false) too: because of a nested await, see comments.

like image 819
Gerard Avatar asked Mar 22 '23 14:03

Gerard


1 Answers

I find it most useful to treat the ViewModel as having UI thread affinity. Think of it as the logical UI, even if it's not the actual UI. So all property and observable collection updates on ViewModel classes should be done on the UI thread.

In your async methods, if you don't need to return to the UI thread, then you can use ConfigureAwait(false) to avoid resuming on the UI thread. For example, if your various initialization methods are independent, you could do something like this:

private async Task<BaseData> InitializeAsync()
{
  // Start all methods on the UI thread.
  var instancesTask = InstancesAsync();
  var projectsTask = ProjectsAsync();
  var adminTask = AdminAsync();

  // Await for them all to complete, and resume this method on a background thread.
  await Task.WhenAll(instancesTask, projectsTask, adminTask).ConfigureAwait(false);

  return this;
}

Also, any time you have return await, take another look to see if you can just avoid async/await entirely:

public static Task<BaseData> CreateAsync()
{
  var ret = new BaseData();
  return ret.InitializeAsync();
}

Finally, you should strongly avoid Dispatcher. Your Loaded event could be simplified to this:

Loaded += async ()
{
  DataContext = await BasisGegevens.CreateAsync();
};
like image 167
Stephen Cleary Avatar answered Mar 31 '23 13:03

Stephen Cleary