Forum Xamarin.Forms

Announcement:

The Xamarin Forums have officially moved to the new Microsoft Q&A experience. Microsoft Q&A is the home for technical questions and answers at across all products at Microsoft now including Xamarin!

To create new threads and ask questions head over to Microsoft Q&A for .NET and get involved today.

How to prevent multiple clicks on a button?

I know there are many ways to prevent multiple clicks on a button in Xamarin, but is there a single solution to prevent it globally? i.e. in every button in my app. Otherwise, what would be the best solution for me to do it to every button I have?

Tagged:

Best Answers

Answers

  • NMackayNMackay GBInsider, University admin

    I'd 2nd the event handler taking care of this rather than a threading hack

    It's easy in frameworks like Prism to guard against this

    RegisterAccountCommand = new DelegateCommand(RegisterAccount).ObservesCanExecute(() => CanExecute);
    
    

    The RegisterAccount event handler takes care of setting CanExecute

  • ClintStLaurentClintStLaurent USUniversity ✭✭✭✭✭

    I agree with @NMackay about using CanExecute with your commands. And it works with the out-of-box MVVM as well.
    But honestly, I notice it tends to be a little slow to affect the actual UI buttons, making it possible to double/triple click them. So I don't rely on it to block critical buttons.

    Also... Think about what the real reason is you want to avoid double-clicks... Its so you don't have code execute twice. So if you cut to the heart of it and just make your code robust so it doesn't execute twice, then the UI is no longer a problem you have to be so pedantic about: You can afford to have the CanExecute take 300ms.

    Also... UI is not the only way to call methods. It doesn't do you any good to have the UI robust if your code isn't. Some other developer on your team will do something bad and once again you're at this same point where a routine is called multiple times as part of a loop (by accident) or something else.

  • Jeanne_VieJeanne_Vie Member

    I just put this inside my method:

    await Task.Delay(1000);

  • ClintStLaurentClintStLaurent USUniversity ✭✭✭✭✭

    I'm not a fan of adding deliberate lack of responsiveness.

    Another technique is to add a handler for TapGestureRecognizer (or click as appropriate) where TapsRequired is set to 2. Then send that to an empty handler.
    If someone double-clicks as desktop people often do on tablets, the second click just goes no place.

  • CaseCase USMember ✭✭✭

    I've run into this too - test users are notorious for hammering buttons (which is a good thing).

    I've developed this system:

    I have these two helper classes

    public class ThreadSafeBool
    {
        private long _backing;
    
        public ThreadSafeBool(bool initValue)
        {
            _backing = initValue ? 1L : 0L;
        }
    
        public bool Get() => Interlocked.Read(ref _backing) == 1L;
    
        public void Set(bool value)
        {
            if (value) Interlocked.CompareExchange(ref _backing, 1L, 0L);
            else Interlocked.CompareExchange(ref _backing, 0L, 1L);
        }
    }
    
    public class ClickRegulator
    {
        public Dictionary<string, ThreadSafeBool> ClickStatus = new Dictionary<string, ThreadSafeBool>();
        private readonly ThreadSafeBool _preventOtherClicks = new ThreadSafeBool(false);
    
        // returns false if safe to execute, true means cannot execute
        public bool SetClicked(string name, bool preventOtherClicks = false)
        {
            if ( _preventOtherClicks.Get()) return true;
    
            if (ClickStatus.ContainsKey(name))
            {
                var alreadyClicked = ClickStatus[name].Get();
                if (alreadyClicked) return true;
                ClickStatus[name].Set(true);
                if (preventOtherClicks) _preventOtherClicks.Set(true);
                return false;
            }
    
            ClickStatus.Add(name, new ThreadSafeBool(true));
            if (preventOtherClicks) _preventOtherClicks.Set(true);
            return false;
        }
    
        public void ClickDone(string name)
        {
            ClickStatus[name].Set(false);
            _preventOtherClicks.Set(false);
        }
    }
    

    Then in all my ViewModels, I do

    private readonly ClickRegulator _clickRegulator = new ClickRegulator();
    
    private ICommand _someCommand;
    
    public ICommand SomeCommand =>
        _someCommand ?? (_someCommand = new Command(DoSomething));
    
    public void DoSomething()
    {
        if (_clickRegulator.SetClicked(nameof(DoSomething))) return;
        try
        {
            // My code
        }
        catch (Exception e)
        {
            // Handle the exception
        }
        finally
        {
            _clickRegulator.ClickDone(nameof(DoSomething));
        }
    }
    

    You can prevent the double click of that single button, or, if you pass "true" to the SetClicked method you can also prevent clicks of other buttons (under the control of that click regulator).

  • ClintStLaurentClintStLaurent USUniversity ✭✭✭✭✭

    I like Case's approach - not saying I don't. Just offering another line of thought.

    A Button should be wired to a Command. You click the button, the command is called.
    Commands have a CanExecute bool property. If the bool is false, the command doesn't execute.
    Since the command is what matters, not the UI - worry about this property in your ViewModel.
    You could have 10 different reasons the command shouldn't execute.

    • Its already running.
    • Not all the required properties are non-null yet (user needs to enter more data)
    • Device is disconnected from the internet
    • Whatever
      As part of the command handler executing, the first thing you do is trip that CanExecute so it returns false.

    When you wire your button XAML to the command, be sure to make use of the CanExecute property on the command.
    Now you don't care if the user mashes the button like a jack hammer since the command only executes the one time its supposed to. When the handler is done, untrip the flag you set at the start.

    Later as the user uses the program, fills in other criteria like the next user name or whatever - the CanExecute will keep getting evaluated until it is once again true.

  • ZoliZoli NLMember ✭✭✭
    edited September 2020
    bool SomeHandler()
    {
        if (HandlerAlreadyRunning) return;
        HandlerAlreadyRunning = true;
        // Do your work
        HandlerAlreadyRunning = false;
        return;
    }
    

    This does not work with MVVMCross (at least I tried only with that, not sure if anything to do with that), as multi click will by queued, and executed nicely synchronized. Unfortunately. Sooo, SomeHandler() will quit, HandlerAlreadyRunning will be restored to false, and the next call of SomeHandler() will start, and nicely run, ... , repeat until all button press is handled.

  • ClintStLaurentClintStLaurent USUniversity ✭✭✭✭✭

    @Zoli
    On the command wired to the button...
    First line set the "CanExecute" to false.
    Should, I say should, make the button unclickable. So there won't be a multi-click to deal with.
    Set the "CanExecute" property back to true again based on your business logic that determines when it can and can't be used.

  • ZoliZoli NLMember ✭✭✭

    @ClintStLaurent well, "CanExecute" is also checked right before the Execute, and because that happens after the previous handler finished - we are at the exact same problem as with HandlerAlreadyRunning flag. Does not matter if you set 'HandlerAlreadyRunning' or 'CanExecute' - the same problem is to "your business logic that determines when it can and can't be used."
    I would appreciate an out of the box solution for this - but no such thing. I.e.: do not queue same command if it's already running.

  • veringmveringm Member ✭✭

    @NMackay / @ClintStLaurent : Is there any smarter way of ensuring "CanExecute=true" again, other than a try-finally ?

  • JoaoFortesJoaoFortes ESMember ✭✭

    Hi have added this code bellow in the click events:

            if (lastClickDebouncer.AddMilliseconds(1000) >= DateTime.Now)
                return;
            lastClickDebouncer = DateTime.Now;
    
Sign In or Register to comment.