Reputation: 139
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.
Upvotes: 9
Views: 7360
Reputation: 4840
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;
Upvotes: 19
Reputation: 456457
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.
Upvotes: 16
Reputation: 9990
You can do that using ManualResetEvent
like:
var mre = new ManualResetEvent(false);
Device.BeginInvokeOnMainThread(() =>
{
// do whatever
mre.Set();
});
mre.WaitOne();
//continues when the UI thread completes the work
Upvotes: 4
Reputation: 21223
If you choose to use C# async/await, you must get on to the correct thread before calling any async
method. Put a breakpoint on the start of the method in your question - before it calls BeginInvoke. Go up your call stack, till you get to a method that isn't declared async
. That is the method that must do BeginInvokeOnMainThread
, as a wrapper around all your async code.
Be sure to remove BeginInvokeOnMainThread
from the code you showed - once you are inside async/await
, that won't do what you want.
DISCLAIMER: It would be better to instead make a fundamental change somewhere else.
Why are you on a background thread, but require a UI call?
There are a variety of mechanisms for splitting your code into the UI portion and the background portion: you haven't fully done so, or you wouldn't be having this problem.
Perhaps you need a background task with a progress callback. That callback communicates with user as needed.
Or you need to have the background task's "completion" report to your main code that it cannot continue without user input, do the user input in that main code, then start another background task to finish the work.
Or earlier in your code, before beginning your background task, you needed to verify that you had all needed info from user, and communicate there.
If those aren't enough hints to head you in a workable direction, I recommend finding a more complete example of async/await used in an app, study what it does. If you still have this question, then add more detail to it.
That's the "pure" answer.
In practice, I have found it sometimes easier to throw in a BeginInvokeOnMainThread
- and then write code without async/await.
G.Hakim made the essential observation:
DisplayAlert is a Task returning method ...
See Microsoft doc- Chaining Tasks by Using Continuation Tasks if you want to understand how to call DisplayAlert, then do something else afterwards.
Going down this road, I think you need to remove async
and await
from your method. You'll be doing the "asynchronous" calling yourself.
Instead of trying to do work after that method returns, you'll pass in a parameter with the work to be done next. Something like:
public void ShowMessageBoxAsync(string message, Action afterAction)
{
Device.BeginInvokeOnMainThread(() =>
{
// See "Chaining Tasks ..." link. Use "afterAction" as "continuation".
... DisplayAlert("Error", message, "OK", "Cancel") ...
});
}
"afterAction" is what will become the "continuation".
Sorry, I don't have the details at my fingertips - see above link.
Upvotes: 0
Reputation: 16459
To be very honest in my Knowledge what you are trying to achieve here is not possible.
Because BeginInvokeOnMainThread
is a void method that takes an action, and both of them cannot return anything.
But I am curious about something else here that being, DisplayAlert
is a Task<bool>
returning method then why do you want to wrap it in another Task<bool>
? Can't you just directly use it wherever you need it I mean it's quite possible to do that isn't it?
Feel free to revert in case of queries
Goodluck!
Upvotes: 0