Home > Blockchain >  Async method locks user interface
Async method locks user interface

Time:12-12

As far as I know async method won't lock user interface. At least it seems to work most of the time. But here it doesn't, and I can't figure out why. It's Avalonia MVVM application. Here is the code:

public class MainWindowViewModel : ReactiveValidationObject
{
   public MainWindowViewModel()
   {
      OnRunClick = ReactiveCommand.CreateFromTask(
                () => OnRun(),
                this.IsValid());
   }
   public ReactiveCommand<Unit, Unit> OnRunClick { get; }
   
   private async Task OnRun()
   {
      await Dispatcher.UIThread.InvokeAsync(() =>
      {
         DoSomethingForVeryLongTime();
      }
   }
}

When button is clicked OnRunClick is called, and then user interface stops responding as long as DoSomethingForVeryLongTime runs. Which is not possible to happen, because async ensures interface is still active, yet it happens.

CodePudding user response:

This code runs DoSomethingForVeryLongTime on the UI thread, not a background thread. It issues the call asynchronously but the actual call still runs on the UI thread.

Running something in the background

To actually run in the background, use Task.Run :

private async Task OnRun()
{
   await Task.Run(DoSomethingForVeryLongTime);      
}

Updating the UI with async/await

Background methods can't modify the UI though, no matter how they're invoked. Execution needs to return to the UI thread somehow. That's what await does in the first place.

If DoSomethingForVeryLongTime can be split into background and UI parts, the background parts can run in the background using Task.Run. Execution will return to the UI thread with await. For example

private async Task OnRun()
{
    await DoSomethingForVeryLongTime();      
}

async Task DoSomethingForVeryLongTime()
{
    for int(i=0;i<1000;i  )
    {
        //Process in another thread background
        await Task.Run(()=>DoSomethingExpensive(i));
        //Return to the UI thread and update it
        UpdateProgressBar(i);
    }
    lblStatus.Text="Complete";
}

Updating the UI using IProgress

Another option is to use the IProgress interface and the Progress implementation to report progress from a background thread. Progress<T> calls a callback method for each message in the thread it was created in :

record FileProgress(string Name, int Index, int Total);

//This will run in the UI thread
void UpdateProgress(FileProgress fp)
{
...
}

private async Task OnRun()
{
    //pg is created in the UI thread
    var pg=new Progress<FileProgress>(UpdateProgress);

    await Task.Run(()=>DoSomethingForVeryLongTime(pg));      
}

async Task DoSomethingForVeryLongTime(IProgress<FileProgress> progress)
{
    for int(i=0;i<1000;i  )
    {
        ...
        //Return to the UI thread and update it
        progress.Report(new FileProgress(fileName,i,1000);
    }
}

Avoiding Task.Run

Task.Run may not be needed at all if the "long runinng" work is an asynchronous operation, like waiting for a database response, an HTTP call or any IO operations. In this case we can use the asynchronous version of the operation, and get back to the UI thread when the operation completes.

Let's say we need to make multiple HTTP calls which can take several seconds. We can use HttpClient.GetAsync for this :

private async Task OnRun()
{
    await DoSomethingForVeryLongTime();      
}

async Task DoSomethingForVeryLongTime()
{
    using var connection=new SqlConnection(_connectionString);
    foreach(var url in _urls)
    {
        var response=await _httpClient.GetStringAsync(url);
        await connection.ExecuteAsync(@"insert MyTable(url,response) values (@url, @response)",
            new { url,response });
        UpdateProgressBar(url);
    }
    lblStatus.Text="Complete";
}

This example uses Dapper to avoid the typical database boilerplate code. Dapper will open and close a connection as needed, so we avoid having a connection open while retrieving HTTP responses.

  • Related