'How to truly avoid multiple buttons being clicked at the same time in Xamarin.Forms?

I am using multiple buttons in a view, and each button leads to its own popup page. While clicking multiple button simultaneously, it goes to different popup pages at a time.

I created a sample content page with 3 buttons (each goes to a different popup page) to demonstrate this issue:

screenshots

XAML page:

<ContentPage.Content>
    <AbsoluteLayout>

        <!-- button 1 -->
        <Button x:Name="button1" Text="Button 1"
            BackgroundColor="White" Clicked="Button1Clicked"
            AbsoluteLayout.LayoutFlags="All"
            AbsoluteLayout.LayoutBounds="0.5, 0.3, 0.5, 0.1"/>

        <!-- button 2 -->
        <Button x:Name="button2" Text="Button 2"
            BackgroundColor="White" Clicked="Button2Clicked"
            AbsoluteLayout.LayoutFlags="All"
            AbsoluteLayout.LayoutBounds="0.5, 0.5, 0.5, 0.1"/>

        <!-- button 3 -->
        <Button x:Name="button3" Text="Button 3"
            BackgroundColor="White" Clicked="Button3Clicked"
            AbsoluteLayout.LayoutFlags="All"
            AbsoluteLayout.LayoutBounds="0.5, 0.7, 0.5, 0.1"/>

        <!-- popup page 1 -->
        <AbsoluteLayout x:Name="page1" BackgroundColor="#7f000000" IsVisible="false"
            AbsoluteLayout.LayoutFlags="All"
            AbsoluteLayout.LayoutBounds="0.5, 0.5, 1.0, 1.0">
            <BoxView Color="Red"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.5, 0.75, 0.3"/>
            <Label Text="Button 1 clicked" TextColor="White"
                HorizontalTextAlignment="Center"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.45, 0.75, 0.05"/>
            <Button Text="Back" BackgroundColor="White" Clicked="Back1Clicked"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.6, 0.5, 0.1"/>
        </AbsoluteLayout>

        <!-- popup page 2 -->
        <AbsoluteLayout x:Name="page2" BackgroundColor="#7f000000" IsVisible="false"
            AbsoluteLayout.LayoutFlags="All"
            AbsoluteLayout.LayoutBounds="0.5, 0.5, 1.0, 1.0">
            <BoxView Color="Green"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.5, 0.75, 0.3"/>
            <Label Text="Button 2 clicked" TextColor="White"
                HorizontalTextAlignment="Center"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.45, 0.75, 0.05"/>
            <Button Text="Back" BackgroundColor="White" Clicked="Back2Clicked"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.6, 0.5, 0.1"/>
        </AbsoluteLayout>

        <!-- popup page 3 -->
        <AbsoluteLayout x:Name="page3" BackgroundColor="#7f000000" IsVisible="false"
            AbsoluteLayout.LayoutFlags="All"
            AbsoluteLayout.LayoutBounds="0.5, 0.5, 1.0, 1.0">
            <BoxView Color="Blue"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.5, 0.75, 0.3"/>
            <Label Text="Button 3 clicked" TextColor="White"
                HorizontalTextAlignment="Center"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.45, 0.75, 0.05"/>
            <Button Text="Back" BackgroundColor="White" Clicked="Back3Clicked"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.6, 0.5, 0.1"/>
        </AbsoluteLayout>

    </AbsoluteLayout>
</ContentPage.Content>

C# event handlers:

void Button1Clicked(object sender, EventArgs e)
{
    // ... do something first ...
    page1.IsVisible = true;
    Console.WriteLine("Button 1 Clicked!");
}

void Button2Clicked(object sender, EventArgs e)
{
    // ... do something first ...
    page2.IsVisible = true;
    Console.WriteLine("Button 2 Clicked!");
}

void Button3Clicked(object sender, EventArgs e)
{
    // ... do something first ...
    page3.IsVisible = true;
    Console.WriteLine("Button 3 Clicked!");
}

void Back1Clicked(object sender, EventArgs e)
{
    page1.IsVisible = false;
}

void Back2Clicked(object sender, EventArgs e)
{
    page2.IsVisible = false;
}

void Back3Clicked(object sender, EventArgs e)
{
    page3.IsVisible = false;
}

Expected:
Clicking button1 opens page1 popup page, and clicking the back button in the popup hides the popup page. Similar behavious for button2 and button3.

Actual:
Clicking multiple buttons (eg. button1 and button2) at the same time opens both popup pages (page1 and page2). Double clicking a single button quickly can also fire the same button twice.


Some research on avoid double clicking
By searching similar questions in stackoverflow (such as this and this), I come to a conclusion where you should set an external variable to control whether the events are executed or not. This is my implementation in Xamarin.forms:

C# struct as the external variable so that I can access this variable in separate classes:

// struct to avoid multiple button click at the same time
public struct S
{
    // control whether the button events are executed
    public static bool AllowTap = true;

    // wait for 200ms after allowing another button event to be executed
    public static async void ResumeTap() {
        await Task.Delay(200);
        AllowTap = true;
    }
}

Then each button event handler is modified like this (same applies to Button2Clicked() and Button3Clicked()):

void Button1Clicked(object sender, EventArgs e)
{
    // if some buttons are clicked recently, stop executing the method
    if (!S.AllowTap) return; S.AllowTap = false; //##### * NEW * #####//

    // ... do something first ...
    page1.IsVisible = true;
    Console.WriteLine("Button 1 Clicked!");

    // allow other button's event to be fired after the wait specified in struct S
    S.ResumeTap(); //##### * NEW * #####//
}

This works generally pretty well. Double-tapping the same button quickly fire the button event once only, and clicking multiple buttons at the same time only open 1 popup page.


The Real Problem
It's still possible to open more than 1 popup pages after modifing the code (adding a shared state variable AllowTap in struct S) as described above. E.g., if the user hold down button1 and button2 using 2 fingers, release button1, wait for around a second, and then release button2, both popup pages page1 and page2 will be opened.


A failed attempt to fix this issue
I tried to disable all buttons if either button1, button2 or button3 is clicked, and enable all buttons if the back button is clicked.

void disableAllButtons()
{
    button1.IsEnabled = false;
    button2.IsEnabled = false;
    button3.IsEnabled = false;
}

void enableAllButtons()
{
    button1.IsEnabled = true;
    button2.IsEnabled = true;
    button3.IsEnabled = true;
}

Then each button event handler is modified like this (same applies to Button2Clicked() and Button3Clicked()):

void Button1Clicked(object sender, EventArgs e)
{
    if (!S.AllowTap) return; S.AllowTap = false;

    // ... do something first ...
    disableAllButtons(); //##### * NEW * #####//
    page1.IsVisible = true;
    Console.WriteLine("Button 1 Clicked!");

    S.ResumeTap();
}

And each back button event handler is modified like this (same applies to Back2Clicked() and Back3Clicked()):

void Back1Clicked(object sender, EventArgs e)
{
    page1.IsVisible = false;
    enableAllButtons(); //##### * NEW * #####//
}

However, the same issue still persists (able to hold another button and release them later to fire 2 buttons simultaneously).


Disabling multi-touch in my app won't be an option, since I need that in other pages in my app. Also, the popup pages may also contain multiple buttons which leads to other pages as well, so simply using the back button in the popup page to set the variable AllowTap in struct S won't be an option as well.

Any help will be appreciated. Thanks.

EDIT
"The Real Problem" affects both Android and iOS. On Android, a button can't be activated once the button is disabled some time when the user is holding the button. This holding-a-disabled-button issue does not affect buttons in iOS.



Solution 1:[1]

I have this in my base view model:

public bool IsBusy { get; set; }

protected async Task RunIsBusyTaskAsync(Func<Task> awaitableTask)
{
    if (IsBusy)
    {
        // prevent accidental double-tap calls
        return;
    }
    IsBusy = true;
    try
    {
        await awaitableTask();
    }
    finally
    {
        IsBusy = false;
    }
}

The command delegate then looks like this:

    private async Task LoginAsync()
    {
        await RunIsBusyTaskAsync(Login);
    }

...or if you have parameters:

    private async Task LoginAsync()
    {
        await RunIsBusyTaskAsync(async () => await LoginAsync(Username, Password));
    }

The login method would contain your actual logic

    private async Task Login()
    {
        var result = await _authenticationService.AuthenticateAsync(Username, Password);

        ...
    }

also, you could use inline delegate:

    private async Task LoginAsync()
    {
        await RunIsBusyTaskAsync(async () =>
        {
            // logic here
        });
    }

no IsEnabled setting necessary. you can replace Func with Action if the actual logic you're executing isn't async.

You can also bind to the IsBusy property for things like the ActivityIndicator

Solution 2:[2]

I'd recommend using a MVVM approach, and binding the IsEnabled property for each button to the same property in the View Model, for example, AreButtonsEnabled:

MyViewModel.cs:

private bool _areButtonsEnabled = true;

public bool AreButtonsEnabled
{
    get => _areButtonsEnabled;
    set
    {
        if (_areButtonsEnabled != value)
        {
            _areButtonsEnabled = value;
            OnPropertyChanged(nameof(AreButtonsEnabled)); // assuming your view model implements INotifyPropertyChanged
        }
    }
}

MyPage.xaml (code for just one button shown):

...
<Button 
    Text="Back" 
    BackgroundColor="White" 
    Clicked="HandleButton1Clicked"
    AbsoluteLayout.LayoutFlags="All"
    AbsoluteLayout.LayoutBounds="0.5, 0.6, 0.5, 0.1"
    IsEnabled={Binding AreButtonsEnabled} />
...

Then for the event handlers for each button, you can set the AreButtonsEnabled property to false to disable all buttons. Note that you should first check if the value of AreButtonsEnabled is true, because there is a chance the user can click twice before the PropertyChanged event is invoked. However, because the buttons' click event handlers run on the main thread, the value of AreButtonsEnabled will be set to false before the next HandleButtonXClicked is called. In other words, the value of AreButtonsEnabled will be updated, even if the UI has not updated yet.

MyPage.xaml.cs:

HandleButton1Clicked(object sender, EventArgs e)
{
    if (viewModel.AreButtonsEnabled)
    {
        viewModel.AreButtonsEnabled = false;
        // button 1 code...
    }
}

HandleButton2Clicked(object sender, EventArgs e)
{
    if (viewModel.AreButtonsEnabled)
    {
        viewModel.AreButtonsEnabled = false;
        // button 2 code...
    }
}

HandleButton3Clicked(object sender, EventArgs e)
{
    if (viewModel.AreButtonsEnabled)
    {
        viewModel.AreButtonsEnabled = false;
        // button 3 code...
    }
}

And then just called viewModel.AreButtonsEnabled = true; when you want to re-enable the buttons.


If you want a "true" MVVM pattern, you can bind Commands to the buttons instead of listening to their Clicked events.

MyViewModel.cs:

private bool _areButtonsEnabled = true;

public bool AreButtonsEnabled
{
    get => _areButtonsEnabled;
    set
    {
        if (_areButtonsEnabled != value)
        {
            _areButtonsEnabled = value;
            OnPropertyChanged(nameof(AreButtonsEnabled)); // assuming your view model implements INotifyPropertyChanged
        }
    }
}

public ICommand Button1Command { get; protected set; }

public MyViewModel()
{
    Button1Command = new Command(HandleButton1Tapped);
}

private void HandleButton1Tapped()
{
    // Run on the main thread, to make sure that it is getting/setting the proper value for AreButtonsEnabled
    // And note that calls to Device.BeginInvokeOnMainThread are queued, therefore
    // you can be assured that AreButtonsEnabled will be set to false by one button's command
    // before the value of AreButtonsEnabled is checked by another button's command.
    // (Assuming you don't change the value of AreButtonsEnabled on another thread)
    Device.BeginInvokeOnMainThread(() => 
    {
        if (AreButtonsEnabled)
        {
            AreButtonsEnabled = false;
            // button 1 code...
        }
    });
}

// don't forget to add Commands for Button 2 and 3

MyPage.xaml (Only one button shown):

<Button 
    Text="Back" 
    BackgroundColor="White"
    AbsoluteLayout.LayoutFlags="All"
    AbsoluteLayout.LayoutBounds="0.5, 0.6, 0.5, 0.1"
    Command={Binding Button1Command}
    IsEnabled={Binding AreButtonsEnabled} />

And now you don't need to add any code in your MyPage.xaml.cs code-behind file.

Solution 3:[3]

Move your disabling call (disableAllButtons()) to the Pressed event of the buttons.

This will disable other buttons as soon as the first touch is detected.

EDIT
To prevent accidentally disabling everything, create a custom button renderer, and hook into the native events to cancel the disabling on a drag-out: iOS: UIControlEventTouchDragOutside

EDIT
Android has been tested in this scenario, it has the same issue described in this question.

Solution 4:[4]

I think following code will work for you.

void Button1Clicked(object sender, EventArgs e)
{
    disableAllButtons();

    // ... do something first ...
    page1.IsVisible = true;
    Console.WriteLine("Button 1 Clicked!");
}

void Button2Clicked(object sender, EventArgs e)
{
    disableAllButtons();

    // ... do something first ...
    page2.IsVisible = true;
    Console.WriteLine("Button 2 Clicked!");
}

void Button3Clicked(object sender, EventArgs e)
{
    disableAllButtons();

    // ... do something first ...
    page3.IsVisible = true;
    Console.WriteLine("Button 3 Clicked!");
}

void Back1Clicked(object sender, EventArgs e)
{
    enableAllButtons();
}

void Back2Clicked(object sender, EventArgs e)
{
    enableAllButtons();
}

void Back3Clicked(object sender, EventArgs e)
{
    enableAllButtons();
}



void disableAllButtons()
{
    button1.IsEnabled = false;
    button2.IsEnabled = false;
    button3.IsEnabled = false;

    disableAllPages();
}

void enableAllButtons()
{
    button1.IsEnabled = true;
    button2.IsEnabled = true;
    button3.IsEnabled = true;

    disableAllPages();
}

void disableAllPages()
{
    page1.IsVisible = false;
    page2.IsVisible = false;
    page3.IsVisible = false;
}

Solution 5:[5]

Shared state is what you need. The least intrusive and most generic way is this - just wrap your code:

    private bool isClicked;
    private void AllowOne(Action a)
    {
        if (!isClicked)
        {
            try
            {
                isClicked = true;
                a();
            }
            finally
            {
                isClicked = false;
            }               
        }
    }

    void Button1Clicked(object sender, EventArgs e)
    {
        AllowOne(() =>
        {
            // ... do something first ...
            page1.IsVisible = true;
            Console.WriteLine("Button 1 Clicked!");
        });
    }

The try..finally pattern is important to keep it safe. In case a() throws an exception, the state is not compromised.

Solution 6:[6]

You can do through Command:

    bool CanNavigate = true;

    public ICommand OpenPageCommand
    {
        get
        {
            return new Command<View>(async (v) => await 
             GotoPage2ndPage(v), (v) => CanNavigate);
        }
    }


    private async Task GotoPage2ndPage(View view)
    {
        CanNavigate = false;

        // Logic for goto 2nd page

        CanNavigate = true;
    }


    Note : Where v indicates CommandParameter.

Solution 7:[7]

I think the simplest way is to use Commands, that already have built in functionality.

Inspired by the answers here, I implemented a ExecuteOnceAsyncCommand (based on the AsyncCommand) that relies on the CanExecute functionality already built in Commands.

Xamarin Forms buttons do automatically use CanExecute to enable or disable so there is no need for special flags in your ViewModel layer.

Here is a gist of the ExecuteOnceAsyncCommand, just replace your commands with this one and it should work:

Solution 8:[8]

Simplest way:

Use AsyncCommand from Xamarin.CommunityToolkit: https://docs.microsoft.com/en-us/xamarin/community-toolkit/objectmodel/asynccommand

Here you can simply set the allowsMultipleExecutions to false

In this case, your command will be only executed when not actually running.
So you could avoid a lot of CanExecute properties in your code.

Example:

NavigateToNextViewCommand = new AsyncCommand(NavigateToNewView, allowsMultipleExecutions: false);

Specially on Android, when you navigate to a new view where a lot of process happens, sometimes you have a kind of lag between the screen shifting, specially on older devices.

Sure, in an ideal world, you maybe have some performance issues which you could fix that it showing the next view a bit faster but the user anyway has the chance to click your button more than once, than you have 2 views of the same type in the navigation stack. With AsyncCommand you can avoid this behavior.

Maybe it helps somebody.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 Patrick
Solution 4 Patrick
Solution 5
Solution 6 Alamgeer
Solution 7 raver99
Solution 8 Stefan Habacher