Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to wait for Device.BeginInvokeOnMainThread code to finish (continue background work with results of a UI call)

In my code I have a task called "ShowMessageBoxAsync". I want to use this code to show (and await) the DisplayAlert to the user and return the result. Like this: var messageBoxResult = await View.ShowMessageBoxAsync("This is an error");

The code for the ShowMessageBoxAsync is:

public async System.Threading.Tasks.Task<bool> ShowMessageBoxAsync(string message)
{
    var result = false;
    Device.BeginInvokeOnMainThread(async () =>
    {
        result = await DisplayAlert("Error", message, "OK", "Cancel");
    });
    return result;
}

Before I added the Device.BeginInvokeOnMainThread, the task gave me an Exception that it wasn't running on the main/UI thread. So after adding BeginInvokeOnMainThread, it started to work without exceptions. The problem, however, is that the code goes directly to the result, without waiting for the result of the "await DisplayAlert".

Is it possible return the value of "result" only after the Device.BeginInvokeOnMainThread code finishes?

I did some research about it and someone suggested to use a TaskCompletionSource, but this blocks the UI thread and the DisplayAlert doesn't show up at all.

like image 290
R. Cats Avatar asked Nov 27 '22 20:11

R. Cats


2 Answers

I did some research about it and someone suggested to use a TaskCompletionSource

That is the correct solution. TaskCompletionSource acts as a "completer" for a Task. In this case, this Task is representing the user interaction, so you want the code on the UI thread to do the completing of the Task, and the code on the background thread to (a)wait for the `Task.

So, something like this:

public Task<bool> ShowMessageBoxAsync(string message)
{
  var tcs = new TaskCompletionSource<bool>();
  Device.BeginInvokeOnMainThread(async () =>
  {
    try
    {
      var result = await DisplayAlert("Error", message, "OK", "Cancel");
      tcs.TrySetResult(result);
    }
    catch (Exception ex)
    {
      tcs.TrySetException(ex);
    }
  });
  return tcs.Task;
}

This should get you unblocked for now. However, a better long-term solution would be to have your background thread logic take some kind of "interact with the UI" interface like this:

public interface IAskUser
{
  Task<bool> AskUserAsync(string message);
}

with a Xamarin-Forms-specific implementation similar to above.

That way, your background thread logic isn't tied to a specific UI, and can be more easily unit tested and reused.

like image 170
Stephen Cleary Avatar answered May 26 '23 09:05

Stephen Cleary


This question is worth an update now - in the latest version of Xamarin Forms there is now an awaitable method called Device.InvokeOnMainThreadAsync which is easier to use and less verbose than the other answers, in my opinion.

        var result = false;
        await Device.InvokeOnMainThreadAsync(async () =>
        {
            result = await DisplayAlert("Error", message, "OK", "Cancel");
        });
        return result;

Or if you're not running in Xamarin forms you can reference Xamarin.Essentials from Xamarin.iOS, Xamarin.Android or Xamarin.UWP too and call the MainThread extension like this

 var result = false;
       await  MainThread.InvokeOnMainThreadAsync(async () =>
        {
            result = await DisplayAlert("Error", message, "OK", "Cancel");
        })
       return result;
like image 34
Adam Diament Avatar answered May 26 '23 09:05

Adam Diament