1

In my Xamarin.Forms application, I have some code that looks like this

private async void OnEntryLosesFocus(object sender, EventArgs e) {
    var vm = (MainPageViewModel)BindingContext;
    if (vm == null)
    {
        return;
    }
    if (!IsAnyNavButtonPressedOnUWP())
    {
        // CheckAndSaveData() causes dialogs to show on the UI.
        // I need to ensure that CheckAndSaveData() completes its execution and control is not given to another function while it is running.
        var _saveSuccessfulInUnfocus = await vm.CheckAndSaveData();
        if (_saveSuccessfulInUnfocus)
        {
            if (Device.RuntimePlatform == Device.UWP)
            {
                if (_completedTriggeredForEntryOnUWP)
                {
                    var navAction = vm.GetNavigationCommand();
                    navAction?.Invoke();
                    _completedTriggeredForEntryOnUWP = false;
                }
            }
            else 
            {
                vm._stopwatchForTap.Restart();
            }
        }
        else
        {
            vm._stopwatchForTap.Restart();
        }
    }    
}

The method above is an EventHandler for the unfocus event for one of my entries. However due to the way Xamarin works when a button is clicked the Unfocused event is triggered before the Command attached is executed.

I need to ensure that the function vm.CheckAndSaveData() finishes executing before the command attached to this button hence why I need to run it synchronously.

I have tried multiple things but they all result in deadlocks.

Some of the solutions I have tried are in this question: How would I run an async Task<T> method synchronously?

THEY ALL RESULT IN DEADLOCKS. There has to be some way I can run my function synchronously or at least force the function CheckAndSaveData to finish before anything else.

Kikanye
  • 1,198
  • 1
  • 14
  • 33
  • Can CheckAndSaveData be made sync? – Caius Jard Jan 14 '22 at 02:08
  • nope it has async methods inside so it cannot be made sync – Kikanye Jan 14 '22 at 02:11
  • 1
    I don't think this is necessarily a duplicate, although the title suggests it is. But to address the issue, I can think of two solutions: 1. In `CheckAndSaveData()`, use `.ConfigureAwait(false)` on all the uses of `await`, as long as it's not changing the UI after that line. Then use `.GetAwaiter().GetResult()` in your unfocus event. Or 2. Use [`SemaphoreSlim`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.semaphoreslim) in your unfocus event and your command event to make the command event wait until the unfocus event is complete. – Gabriel Luci Jan 14 '22 at 02:35
  • I can edit `CheckAndSaveData()`. I can share the contents of that function here if needed. That function does in fact show a dialog on my app's UI and I believe that may be whats causing the issue. I have tried using `.ConfigureAwait(false)` as well as using await `Device.InvokeOnMainThreadAsync()` but that didn't work. I am not very well versed with Async code and I am not sure how I would use the SemaphoreSlim in my case. Are you able to expand on this a little bit? – Kikanye Jan 14 '22 at 02:41
  • in uwp, you can't run an async method in sync mode, GetAwaiter().GetResult() is not useful. – player2135 Jan 14 '22 at 02:45

3 Answers3

3

Congratulations, you have found one of the corner cases where there is no solution. There are hacks for sync-over-async that work in some cases, but no solution will work here.

Here's why no known solutions will work:

  • You can't directly block because CheckAndSaveData needs to run a UI loop.
  • You can't block on a thread pool thread because CheckAndSaveData interacts with the UI.
  • You can't use a replacement single-threaded SynchronizationContext (as in the incorrectly linked duplicate), because CheckAndSaveData needs to pump Win32 messages.
  • You can't even use a nested UI loop (Dispatcher Frame in this case), because pumping those same Win32 messages will cause your command to be executed.

Put more simply:

  • CheckAndSaveData must pump messages to show a UI.
  • CheckAndSaveData cannot pump messages to prevent the command from executing.

So, there is no solution here.

Instead, you'll need to modify your command so that it waits for CheckAndSaveData somehow, either using a synchronization primitive or by just calling CheckAndSaveData directly from the command.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thanks for your response. I ended up using a solution that involved a `TaskCompletionSource` this question was helpful https://stackoverflow.com/questions/15122936/write-an-async-method-that-will-await-a-bool – Kikanye Jan 15 '22 at 03:35
1

An option is to use SemaphoreSlim in your unfocus event and your command event to make the command event wait until the unfocus event is complete.

When a part of your code enters the semaphore (using Wait or WaitAsync), it will block other parts of your code that try to enter the semaphore until the semaphore is released.

Here's an example of what that could look like:

private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1,1);

private async void OnEntryLosesFocus(object sender, EventArgs e) {
    var vm = (MainPageViewModel)BindingContext;
    if (vm == null)
    {
        return;
    }
    if (!IsAnyNavButtonPressedOnUWP())
    {
        try {
            await _semaphore.WaitAsync();
            
            // CheckAndSaveData() causes dialogs to show on the UI.
            // I need to ensure that CheckAndSaveData() completes its execution and control is not given to another function while it is running.
            _saveSuccessfulInUnfocus = await vm.CheckAndSaveData();
            if (_saveSuccessfulInUnfocus)
            {
                if (Device.RuntimePlatform == Device.UWP)
                {
                    if (_completedTriggeredForEntryOnUWP)
                    {
                        var navAction = vm.GetNavigationCommand();
                        navAction?.Invoke();
                        _completedTriggeredForEntryOnUWP = false;
                    }
                }
                else 
                {
                    vm._stopwatchForTap.Restart();
                }
            }
            else
            {
                vm._stopwatchForTap.Restart();
            }
        }
        finally
        {
            _semaphore.Release();
        }
    }    
}

private async void OnCommand(object sender, EventArgs e) {
    try {
        // Execution will wait here until Release() is called in OnEntryLosesFocus
        await _semaphore.WaitAsync();
        
        // do stuff
    }
    finally
    {
        _semaphore.Release();
    }
}

The try/finally blocks are optional, but it helps make absolutely sure that the semaphore is released even if an unhandled exception happens.

Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • This was helpful in pointing me in the right direction. I ended up using a `TaskCompletionSource` inspired by https://stackoverflow.com/questions/15122936/write-an-async-method-that-will-await-a-bool. – Kikanye Jan 15 '22 at 03:38
0

Solved this using info form the answers provided as well as help from this question

Here's how I did it.

private async void OnEntryLosesFocus(object sender, EventArgs e) {
    var vm = (MainPageViewModel)BindingContext;
    if (vm == null)
    {
        return;
    }
    // A public variable in my view model which is initially set to null.
    vm.UnfocusTaskCompletionSource = new TaskCompletionSource<bool>();
    var saveSuccessful = await vm.CheckAndSaveData();
    vm.UnfocusTaskCompletionSource.SetResult(saveSuccessful);

    if (saveSuccessful && Device.RuntimePlatform == Device.UWP &&
        _completedTriggeredForEntryOnUWP)
    {
        var navAction = vm.GetNavigationCommand();
        navAction?.Invoke();
        _completedTriggeredForEntryOnUWP = false;
    }
    /*Set this back to null. (This is very important for my use case.). Just in case this function executes completely before the Button click command executes for whatever reason */
    vm.UnfocusTaskCompletionSource = null;
}

In the function for my Command attached to the button I have something like below

private async void OnButtonTap(ArrorDirection direction)
{
    bool preventMove = false;

    if (UnfocusTaskCompletionSource != null)
    {
        /*Wait for `CheckAndSaveData() from `OnEntryLoosesFocus()` to complete and get the return value. */
        await UnfocusTaskCompletionSource.Task;
        var dataCheckAndSaveSuccessful = UnfocusTaskCompletionSource.Task.Result;
        preventMove = !dataCheckAndSaveSuccessful;
    }

    // Do stuff in function:
    if (preventMove){
        DoMove()
    }


    UnfocusTaskCompletionSource = null;
    
}

Kikanye
  • 1,198
  • 1
  • 14
  • 33