In Xamarin.Forms Device.BeginInvokeOnMainThread() doesn’t show message box from notification callback *only* in Release config on physical device

C#Xamarinxamarin.forms

C# Problem Overview


I'm rewriting my existing (swift) iOS physical therapy app "On My Nerves" to Xamarin.Forms. It's a timer app to help people with nerve damage (like me!) do their desensitization exercises. You have these "fabrics" (e.g. a feather) where each fabric has an 'x' second countdown. When once fabric's timer reaches 0, a message box appears saying "time's up". The user hits OK and the next fabric starts its countdown. Rinse and repeat for all fabrics in the list. Here's a video showing the workflow. Trust me on the UX in the video.

Here's my sample app code demonstrating this behavior.

The DoSomethingForNow method (forgive my naming strategy) is the callback from the NotificationService (see line 65 - not enough SO rep points for direct link) that is created when the timer starts, in case the app gets the background.

The specific call that is not working on Release mode on Device is at line 115

Device.BeginInvokeOnMainThread(
                async () => await ShowAlertAndWaitForUser(currentFabricCount));

The async () => await ShowAlertAndWaitForUser(currentFabricCount)) works as expected in Debug and Release configuration on iOS Simulator, and in Debug configuration on the device.

However, the message box indicating time is up does not appear in Release config on the physical device. I cannot figure out why Device.BeginInvokeOnMainThread() doesn't work in Release config on device. What is going on?

Side note re using Device.StartTimer()

The reason why I switched from Device.StartTimer() to James's FrenchPressTimer solution (see jamesmontemagno/FrenchPressTimer) early on is because I need a way to cancel / stop / whatever a Device.StartTimer() in case the user needs to pause or stop the countdown on the app.

The Device.StartTimer() code works great for me on all configurations on both simulator and device and thankfully someone showed me how to cancel a Device.StartTimer (thanks!). If you want to see this working solution, check out saraford/Device-StartTimer-Working-Xamarin on GitHub.

My specific question is why doesn't Device.BeginInvokeOnMainThread() display a message box when invoked from a notification callback in Release config on a physical device.

I've also tried the different linker combinations. No effect.

C# Solutions


Solution 1 - C#

Here's an answer that will work, but as @JamesMallon has mentioned, don't use it:

Device.BeginInvokeOnMainThread(ShowAlertAndWaitForUser(currentFabricCount).Result);

Your issue is very common in situations where the code is not run in the Main/UI thread. It seems that you begin the invoke on the main thread but the UI thread doesn't actually read the line, and another thread instead is performing the actions you require. And that's also why it works some times and doesn't work during other times.

So instead of performing the whole ShowAlertAndWaitForUser() on the UI thread, try to instead run only the DisplayAlert function on that thread.

Solution 2 - C#

For IOS 8 use Alert Controllers.

https://developer.xamarin.com/recipes/ios/standard_controls/alertcontroller/

var okAlertController = UIAlertController.Create ("OK Alert", "This is a 
sample alert with an OK button.", UIAlertControllerStyle.Alert);

Solution 3 - C#

I was facing similar issue with Acr.UserDialogs plugin while displaying Loading screen on Android.

seems your main thread is in use, instead of Device.BeginInvokeOnMainThread just call the code with await ShowAlertAndWaitForUser(currentFabricCount) by making DoSomethingForNow an async method.

And its good idea to implement INotifyPropertyChanged in a viewmodel class and then bind it rather dumping everything in the code-behind.

Solution 4 - C#

So there seems to be a common misunderstanding here. Device.BeginInvokeOnMainThread()is a synchronous call which is what the problem is in this case. Here is an example of whats happening:

Imagine we have a method LogOut. When the user wants to log out of the application the method LogOut is called off of the UI Thread.

async void LogOut() //Called on thread 5
{
    await ShowAlertAndWaitForUser(currentFabricCount); //pop up shows "You are about to be logged out"
    Console.WriteLine("User logged out");
}

Well I have some bad news for you, because this has been called off of the UI Thread the dialogue will not be displayed to the user.

Well lets try doing what you have tried to do above!

async void LogOut() //Called on thread 5
{
    Device.BeginInvokeOnMainThread(async () => await ShowAlertAndWaitForUser(currentFabricCount));
    Console.WriteLine("User logged out");
}

So now that we are invoking this method on the main thread and forcing it to be called on the main thread, surely this will work?!

Unfortunately, no it won't. The reason it won't work is because you are not actually calling ShowAlertAndWaitForUser directly! You are calling Device.BeginOnMainThread which as soon as it is called then calls an awaited call within itself so the LogOut method will just go ahead and call Console.WriteLine and that call will finish. Meaning that there is no time to display the alert as we are not even awaiting the call properly!

If this confuses you I suggest you read my explanation of asynchronous programming here.

Fantastic, we know what the issue is, so how do we fix it?

There are 2 ways you can rectify this issue:

Method 1 (Preferred solution):

Actually make sure the LogOut method is called on the UI Thread. I know this sounds stupidly easy but I actually tend to stay as far away as I can with forcing calls onto UI threads whilst programming. Sometimes it is completely necessary but often a lot of the time it's completely unnecessary. In mobile apps, often most methods are called in a change originally called through user interaction.

Use Device.Timer as this occurs on the main thread, most other timers invoke methods on bg threads

User Pushes Button -> Call LogOut -> Dialogue Shown.

because the user has originally pushed the button this call has come from the main thread, so all synchronous calls made from here occurs on the main thread.

Method 2 (The method that you are all going to use because its the quickest to implement):

So as I said above sometimes Device.BeginInvokeOnMainThread must be used. So for cases like this, I wrote a small method called Device.BeginInvokeOnMainThreadAsync which is await-able which means you can actually await the method within Device.BeginInvokeOnMainThreadAsync which means your alert dialogue will actually display. So here is the code.

public static Task<T> BeginInvokeOnMainThreadAsync<T>(Func<T> f)
{
    var tcs = new TaskCompletionSource<T>(); 
    Device.BeginInvokeOnMainThread(() =>
    {
        try
        {
            var result = f();
            tcs.SetResult(result);
        }
        catch(Exception ex)
        {
            tcs.SetException(ex);
        }
     }); 
     return tcs.Task;
}

Here is how you would use it:

await BeginInvokeOnMainThreadAsync(async () => await ShowAlertAndWaitForUser(currentFabricCount));

So the final unanswered question is why does it work in debug mode & release mode in simulator and never in release mode on the device?

My honest answer is I really don't know. One thing I do know, is that it shouldn't work in debug mode. Also I know that there are large differences in the compilation processes between debug and release mode. The main cause is most likely a combination of release mode & the device's CPU architecture. For an answer to that question it would be brilliant if someone could comment with some expert knowledge?

Hopefully this is clear and helps you guys out.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionsarafordView Question on Stackoverflow
Solution 1 - C#SaamerView Answer on Stackoverflow
Solution 2 - C#RamankingdomView Answer on Stackoverflow
Solution 3 - C#MorseView Answer on Stackoverflow
Solution 4 - C#James MallonView Answer on Stackoverflow