21

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.

Patrick
  • 1,717
  • 7
  • 21
  • 28
  • I didn't actually try to click 2 buttons at the same time before. I am using a loading page when the user click it will cover the screen and will not allow any clicks. But can users click at 2 buttons at the exact same time ? One should be clicked first even by 0.001 seconds. I will try right now and get back to you. – Ali123 Apr 24 '18 at 05:41
  • I just tested and you were right. There is a small delay in showing the loading page that if I click at 2 buttons at the same time, It will trigger both actions which can leads to strange behaviors. I heard about the new release and how it has better view state control but I am not sure if they are handling this issue. – Ali123 Apr 24 '18 at 05:47
  • A manual solution could be done to disable other buttons or not exactly even close to the best since you need to disable other clicking events in lists and other views too. – Ali123 Apr 24 '18 at 05:48
  • As the post has mentioned, the user can still fire a button if they hold down that button before it's being disabled. So I don't think disabling other buttons will solve the issue. – Patrick Apr 24 '18 at 11:24
  • This is native behavior right? or just in Xamarin forms? – Ali123 Apr 24 '18 at 11:39
  • similar questions are here https://stackoverflow.com/questions/20971484/how-to-avoid-multiple-button-click-at-same-time-in-android?noredirect=1&lq=1 – Ali123 Apr 24 '18 at 11:40
  • @Ali123 I tested this in Xamarin.Forms only. I referenced that question in my post already, and this is how I came up with a struct to "disable" other buttons. – Patrick Apr 24 '18 at 13:12
  • 1
    @DiegoRafaelSouza as long as it's not possible to activate 2 buttons simultaneously, I guess it should be fine. – Patrick Apr 30 '18 at 13:27
  • 1
    Okay. I'm trying to write an answer with a few options for you. It may require some effort and creativity to understand my English, but I think it can solve your problem =). – Diego Rafael Souza Apr 30 '18 at 13:32
  • @Patrick is there any answer that deserve the 50 points bounty? I just discovered this big issue and it looks like it's general and not specific to only Xamarin forms. I tested with Google Play store and you can actually click 2 things at the same time but it will not cause any issues since they use simple navigation and last tapped item will show first and the first will be behind it. This bug should get more attention – Ali123 May 05 '18 at 10:00
  • @Patrick , there are actually multiple solution that I want to try but my mind is busy right now with another project. For example, create an absolute layout that covers the whole page when any button is clicked and it will nullify all clicks on the screens while you are clicking on a view. But, most of those solutions will require a lot of work and bindings which is inefficient. – Ali123 May 05 '18 at 10:04
  • @Ali123 thanks for the suggestion, but the example that I posted here creates an absolute layout that covers the whole page when any button is clicked, and that does not nullify all clicks on the screen. This is why this question is so tricky. – Patrick May 05 '18 at 10:23
  • @oh, maybe I got the idea from you then. I should really read before typing and this happen multiple times. Sorry, but my mind is very busy nowadays. – Ali123 May 05 '18 at 11:02
  • @Ali123 it's ok, life is busy sometimes. – Patrick May 05 '18 at 11:05

9 Answers9

10

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

InquisitorJax
  • 1,062
  • 11
  • 20
6

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.

sme
  • 4,023
  • 3
  • 28
  • 42
  • 1
    You could use the the [Command constructor overload](https://developer.xamarin.com/api/constructor/Xamarin.Forms.Command.Command/p/System.Action/System.Func%7BSystem.Boolean%7D/) which takes a `canExecute` callback to disable the button even more cleanly (without binding to `IsEnabled`). – Damian May 02 '18 at 12:40
  • Thanks for your MVVM approach of solving the question. But when do I call `viewModel.AreButtonsEnabled = true;` to enable all the buttons if there are more buttons (to lead to other popup pages) after clicking one of the 3 buttons in the menu? – Patrick May 02 '18 at 14:17
  • Does it mean that `AreButtonsEnabled` in `MyViewModel.cs` in your answer functions similarly as `AllowTap` in `struct S` in my question? – Patrick May 02 '18 at 18:03
  • @Patrick In my answer, setting the value of `AreButtonsEnabled` will enable/disable all buttons that are bound to that property. As to when to set `AreButtonsEnabled = true;`, you can just do that by listening to the `Clicked` event or the `Command` that is bound to the Back buttons. In other words, set `AreButtonsEnabled = true` when you click the Back button on the pop ups – sme May 03 '18 at 02:40
  • If the popup page includes other buttons rather than the back button (which is true for an app that I am developing, but I can't just post every code of my app to stackoverflow since I just need to provide a minimum working example code) that also do other functions, I can't just set `AreButtonsEnabled = true` while clicking some button. Do I need multiple `AreButtonsEnabled` variables to keep track of different buttons then? But using different variables that way is not a very expandable solution once I have a lot of popup pages/buttons – Patrick May 03 '18 at 02:50
  • 1
    @Patrick Most likely yes. In this case, i'd say have an `AreButtonsEnabled` property for each 'set' of buttons, ie a property for the buttons on the main screen, a property for the buttons in the pop up, etc. Thats how I'd go about it at least – sme May 03 '18 at 02:53
5

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.

Patrick
  • 1,717
  • 7
  • 21
  • 28
Tim
  • 2,089
  • 1
  • 12
  • 21
  • pressing a button and releasing it (at a different location of the screen) without activating the button will disable all buttons, and you can't interact with any of the buttons anymore. – Patrick Apr 30 '18 at 13:13
  • sadly the `Released` event of the buttons are only called if the `Clicked` event is called. So without activating the `Clicked` event of the button, all the buttons will remain disabled. Thanks for your suggestions though, now I can try to find a solution that involves the `Pressed` event – Patrick Apr 30 '18 at 13:15
  • What if you combine that with a short time-out? Let it auto-reset to enabled after a second or so. – Tim Apr 30 '18 at 13:16
  • Just tried a short time-out. It works better than the previous solutions, but it's still possible to activate 2 buttons at the same time (e.g. by holding `button1`, wait for the timer to pass, press `button2`, then release `button1`) – Patrick Apr 30 '18 at 13:25
  • I think you're going to need to create custom button renderers for iOS and Android. Actually, first check to see if the problem is on both, or just one. – Tim Apr 30 '18 at 13:31
  • I can't get my emulator working on android, so I can only test this in iOS for now – Patrick Apr 30 '18 at 13:45
  • 1
    Added to the answer. – Tim Apr 30 '18 at 13:51
  • I finally got the testing working on Android. On android, you can't activate a button if the button has been disabled when the user is holding the button (so `disableAllButtons()` with a short time-out won't work as well). Without the `disableAllButtons()` function, the problem persists on both iOS and Android. I updated the question to reflect this. – Patrick Apr 30 '18 at 17:44
  • 1
    Narrow down your logic so it only disables the other buttons. There's also a way to detect drag-out on Android, but I don't remember it and I think it's harder than iOS – Tim Apr 30 '18 at 17:47
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;
}
Patrick
  • 1,717
  • 7
  • 21
  • 28
Mobeen
  • 69
  • 7
  • Even other pages are hidden with `disableAllPages()`, it's still possible for a user to switch to another page by holding other buttons. Nice attempt though – Patrick May 02 '18 at 13:51
4

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.

Robin
  • 616
  • 3
  • 9
  • Please accept my answer. So I can start commenting on other's answers ;) – Robin May 03 '18 at 11:48
  • I already used a shared state variable (`AllowTap` in `struct S'), so the real problem isn't about using a shared state variable – Patrick May 03 '18 at 13:07
  • 1
    @Patrick I see. In that case use a IsOpen shared state to be set once a popup is shown, and cleared once one is closed. So even in the event of buttons handlers being fired on release 1s apart, it won't cause problems. – Robin May 05 '18 at 20:15
  • So I probably need a bunch of shared state variables to keep track of the buttons in different popup pages then? – Patrick May 07 '18 at 12:56
  • @Patrick. No, just a static one will do fine, since you'll only allow one popup page at any given time. You set it to true before a popup is shown, and to false in the event handler of the popup dealing with closing the popup. In your case that looks like the "Back" button on the popup. – Robin May 07 '18 at 16:53
  • If the popup page includes other buttons rather than the back button (which is true for an app that I am developing, but I can't just post every code of my app to stackoverflow since I just need to provide a minimum working example code) that also do other functions, I can't just use one static variable to keep track the popup page, since there might be multiple popup pages on top of one another (like a hierarchy). – Patrick May 07 '18 at 18:32
  • 1
    @Patrick In that case I would make a base class for the pages, which each page having their own IsPopupOpen, which denies any other sibling button to open a new popup for that page. But when you do open a popup, you pass a reference for the parent popup to the new child popup. So whenever a child popup is closed, it sets the parent IsPopupOpen to false again. And this will work at whatever popup depth you choose to venture. Just avoid circular references by creating new pages on each level of the hierarchy. – Robin May 07 '18 at 22:57
  • Sounds complicated, but I will definitely give it a try! – Patrick May 08 '18 at 12:58
  • @Patrick Let me know if you'll need some help, and I'll add some code to the answer. – Robin May 08 '18 at 19:24
  • How to write the above code for Async and Await tasks – samdubey Jul 04 '22 at 12:54
1

Simplest way:

Use AsyncCommand from Xamarin.CommunityToolkit: https://learn.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.

Stefan Habacher
  • 153
  • 1
  • 8
  • I believe you can still execute the command multiple times even with allowsMultipleExecutions=false. It disables the bounded button but if user clicks the button quickly before it is disabled, the command executes multiple times. – Daniel Jan 17 '23 at 10:38
0

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.
Alamgeer
  • 1
  • 1
0

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:

raver99
  • 99
  • 1
  • 4
0

starting from @Robin solution here, I realized the following. This prevents double taps also in .NET MAUI.

private bool isClicked;
private void AllowOne(Action a)
{
    if (!isClicked)
    {
        isClicked = true;
        a();
        Dispatcher.StartTimer(TimeSpan.FromMilliseconds(250), () =>
        {
            isClicked = false;
            return false;
        });
    }
}

private void cmdButton_Clicked(object sender, EventArgs e)
{
    AllowOne(() =>
    {
        //Do your stuff here...
    });
}

This should be the simple and most effective way to not allow double taps.

Also, I decided to remove the Try..finally statement because if there is an unhandled error it will be processed by Sentry, I will receive a notification and so on. With the try catch I will never realize there's an unhandled error in the application.

Michele
  • 101
  • 1
  • 6