Home > front end >  I would like to run an async method synchronously when the async function causes a dialog to show on
I would like to run an async method synchronously when the async function causes a dialog to show on

Time:01-15

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.
        _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.

CodePudding user response:

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.

CodePudding user response:

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.

  •  Tags:  
  • Related