Prevent multiple rapid clicks on a button

Greg.8674Greg.8674 USMember ✭✭

Is there a way to prevent someone from rapidly clicking a button multiple times? I tried to enable/disable, show/hide the button on the RunOnUiThread() but that doesnt seem to do anything.
Whats happening is users are clicking the buttons to advance to the next date which will stop them advancing when the currentdate is the last day of the current week. Works fine when doing regular clicking, but if you click it multiple times rapidly, the currentdate goes beyond the current week. I dont want them to go beyond the current week. Again this works fine if the user doesn't spazz out and click the button like crazy.

Current button code below:

        btnCalendarRightDateSelector.Click += delegate
        {
            RunOnUiThread(() => btnCalendarRightDateSelector.Visibility = ViewStates.Invisible);
            //btnCalendarRightDateSelector.Visibility = ViewStates.Invisible;
            if (FLXHelper.Pause())
            {
        //pauses for 3seconds
                CurrentDate = CurrentDate.AddHours(24);
        //this shows/hides the left/right nav button accordingly.
                User.ShowHideDateSelectorArrows(btnCalendarLeftDateSelector, btnCalendarRightDateSelector); 
                lblCalendarDateRangeDateSelector.Text = CurrentDate.ToString(ValueFormatsDateLongFormat);// +" - " + EndDate.ToShortDateString();
                tblStrengthRows.RemoveAllViews();
                GetStrenghtByMuscleGroup();
            }                 
            //Android.Widget.Toast.MakeText(this, FlxGlobals.ReloadingText, Android.Widget.ToastLength.Short).Show();                
        };

Posts

  • lumoslumos ATMember

    i have the same issue.
    if the button is clicked fast for serveral times, each click is taken.

    this is my solution - it is not very elegant and i hope there is a better way :)

    private Button button
    private void CreateButton()
         {
             button = new Button
             {
                 Text = "My Button"
             };
             button.Clicked += ButtonClicked;
         }
         private void ButtonClicked(object sender, EventArgs e)
         {
        //disable the buttonclick even
             button.Clicked -= ButtonClicked;
    
             button.IsEnabled = false;
             System.Threading.ThreadPool.QueueUserWorkItem(o =>
             {
                 //do stuff that can be handled async
    
                 System.Threading.Thread.Sleep(1000);
    
                 Device.BeginInvokeOnMainThread(() =>
                 {
             //reenable the button
                     button.IsEnabled = true;
             //subscribe the event again
                     button.Clicked += ButtonClicked;
                 });
             });
         }
    
  • Greg.8674Greg.8674 USMember ✭✭

    Thanks I will see if that helps

    After I replace the
    btnCalendarRightDateSelector.Click += delegate {}

    with function call like
    btnCalendarRightDateSelector.Click += btnCalendarRightDateSelectorClicked;

    It seemed to have helped a little bit in my case.

  • softlionsoftlion FRBeta ✭✭✭

    Aieaieaieaieaieaie i see so much bad patterns in your code.

    Don't RunOnUiThread in your event handler, it is already run on the ui thread.

            if (FLXHelper.Pause())
    

    Don't pause threads, use async / await on Task.Delay instead.

            System.Threading.Thread.Sleep(1000);
    

    Same remark

           QueueUserWorkItem
    

    This is deprecated. Use Tasks instead. Task.Run(() => ...)

    To fix your issue, read a bit about synchronization mechanisms in C#. Search for "lock". Sample:

    private object syncLock = new object();
    bool isInCall = false;
    
    public void ButtonClicked()
    {
           lock(syncLock)
          {
                if(isInCall)
                        return;
                 isInCall = true;
          }
    
         try {
                 ...
         }
         finally
         {
           lock(syncLock)
          {
                isInCall = false
            }
         }
    }
    
  • lumoslumos ATMember

    as far as i know QueueUserWorkItem is not deprecated. its just another way to add tasks to the thread pool.
    http://msdn.microsoft.com/en-us/library/System.Threading.ThreadPool_methods(v=vs.110).aspx
    and it has fewer overhead for a simple fire and forget task - but thats not the question

    on button click I send data to a server – this is done async.
    To notify the user that the click has been taken, the button should be disabled for a second, in this time, no additional
    click should be taken.
    The simplest solution would be to disable the button in the click handler, sleep for a second and enable the button again.
    The problem is that button.IsEnabled=false or unsubscribing the event is not working in this case (WHY) - and all clicks are taken during that second.

    e.g.

             private void ButtonClicked(object sender, EventArgs e)
             {
                    button.IsEnabled = false;
        SendData();
    
        //although the button is disabled, clicks are received.
        Sleep(1000);
        button.IsEnabled = true;
             }
    

    there is no difference if use your solution with lock because all clickes are running on the ui thread

        private void ButtonClicked(object sender, EventArgs e)
       { 
        lock(syncLock)
            {
                if(isInCall)
                        return;
                 isInCall = true;
          }
    
         try {
                 ...
         }
         finally
         {
           lock(syncLock)
          {
                isInCall = false
            }
         }
    
         }
    

    if I run the button click in an own task, with task.run – all clicks are taken, but during the
    timeout – the lock prevents them from beeing executed.
    I think it's odd to use a separate task just to disable the button for one sec.

      private void ButtonClicked(object sender, EventArgs e)
      {
       Task.Run(() =>
        {
          ButtonClicked();
        });
    }
    
      public void ButtonClicked()
      {
           lock(syncLock)
          {
                if(isInCall)
                        return;
                 isInCall = true;
          }
    
         try {
                 ...
         }
         finally
         {
           lock(syncLock)
          {
                isInCall = false
            }
         }
        }
    

    isn't it better to unsubscribe the event – so I just need one threadpool thread (no matter if QueueUserWorkItem or task.run is used)
    I don't need the overhead of the lock statement - which doesn't matter – as performance is no issue if we sleep for 1sec
    and don't require an additional object for sync?

    on the other hand I have to reinvoke the main thread to subscribe the event again.

    => isn't there another way to ignore the clicks during that timeout? i think your both solutions are odd

  • softlionsoftlion FRBeta ✭✭✭

    Check Interlock.Exchange

  • masterolearymasteroleary USMember ✭✭
    edited August 2016
        class NoDoubleClickButton : Button
        {
            public bool bClicked { get; set; } = false;
    
            public EventHandler ToDo;
            public NoDoubleClickButton()
            { 
                this.Clicked += (sender, args) =>
                 {
                     NoDoubleClickButton me = (NoDoubleClickButton)sender;
                     if(me.ToDo == null)
                     {
                         throw new Exception("This '" + this.Text + "' no double click button has not had a 'todo' EventHandler set.");
                     }
                     if (me.bClicked == false)
                     {
                         me.bClicked = true;
                         Device.StartTimer(
                             TimeSpan.FromMilliseconds(500), () =>
                             {
                                 System.Diagnostics.Debug.Write("clicked");
                                 me.bClicked = false;
                                 me.ToDo(sender, args); 
                                return false;
                             }
                         );
                     }
                 };
            }
        }
    

    // implementation
    NoDoubleClickButton test = new NoDoubleClickButton
    {
    Text = "Test",
    FontSize = 20,
    BorderRadius = 20,
    BackgroundColor = Color.White,
    TextColor = Color.FromHex("#4694ff"),
    ToDo = (sender, args) =>
    {
    Navigation.PushAsync(myNextPage);
    }
    };

  • I had the same issue.
    The way i solved it, its not the better way but works.

    private static bool CanExecute = true;
    
    public async void OnSelected(object sender, SelectedItemChangedEventArgs e)
    {
        if (CanExecute)
            {
            CanExecute = false;
    
            // your code
    
            await Task.Run(async () => {
                        await Task.Delay(500);
                        CanExecute = true;
                    });
        }
    }
    
  • RobertBruceRobertBruce USMember ✭✭

    Here's a simple solution to prevent rapid multiple clicks using a clock. You can set the timeout period to whatever you'd like. In my example, I have it set for 1 second. Cheers!

    using System;
    using Android.App;
    using Android.OS;
    using Android.Widget;
    using Android.Util;
    
    namespace MyTestApp {
    
        [Activity(Label = "StopRapidMultipleButtonClicksActivity")]
        public class StopRapidMultipleButtonClicksActivity : Activity {
    
            private Button btn;
            private long LastButtonClickTime;
    
            protected override void OnCreate(Bundle savedInstanceState) {
                base.OnCreate(savedInstanceState);
    
                var layout = new LinearLayout(this) { Orientation = Orientation.Vertical };
    
                btn = new Button(this) { Text = "Click Me Quickly!" };
                btn.Click += ButtonClicked;
    
                layout.AddView(btn);
                SetContentView(layout);
            }
    
            protected override void OnResume() {
                base.OnResume();
                LastButtonClickTime = 0;
            }
    
            private void ButtonClicked(object sender, EventArgs e) {
    
                // Don't allow multiple clicks unless they're at least a second apart
                if (SystemClock.ElapsedRealtime() - LastButtonClickTime < 1000) return;
    
                LastButtonClickTime = SystemClock.ElapsedRealtime();
    
                Log.Info("MyApp", "Hello World!");
            }
    
        }
    }
    
  • RobertBruceRobertBruce USMember ✭✭

    Here's a simple solution to prevent rapid clicking using a clock. You can set the minimum timeout period to whatever you'd like. In my example, I have it set for 1 second. Cheers!

    using System;
    using Android.App;
    using Android.OS;
    using Android.Widget;
    using Android.Util;
    
    namespace MyTestApp {
    
        [Activity(Label = "StopRapidMultipleButtonClicksActivity")]
        public class StopRapidMultipleButtonClicksActivity : Activity {
    
            private Button btn;
            private long LastButtonClickTime;
    
            protected override void OnCreate(Bundle savedInstanceState) {
                base.OnCreate(savedInstanceState);
    
                var layout = new LinearLayout(this) { Orientation = Orientation.Vertical };
    
                btn = new Button(this) { Text = "Click Me Quickly!" };
                btn.Click += ButtonClicked;
    
                layout.AddView(btn);
                SetContentView(layout);
            }
    
            protected override void OnResume() {
                base.OnResume();
                LastButtonClickTime = 0;
            }
    
            private void ButtonClicked(object sender, EventArgs e) {
    
                // Don't allow multiple clicks unless they're at least a second apart
                if (SystemClock.ElapsedRealtime() - LastButtonClickTime < 1000) return;
    
                LastButtonClickTime = SystemClock.ElapsedRealtime();
    
                Log.Info("MyApp", "Hello World!");
            }
    
        }
    }
    
  • RobertBruceRobertBruce USMember ✭✭

    Here's a simple solution to prevent rapid clicking using a clock. You can set the minimum timeout period to whatever you'd like. In my example, I have it set for 1 second. Cheers!

    using System;
    using Android.App;
    using Android.OS;
    using Android.Widget;
    using Android.Util;
    
    namespace MyTestApp {
    
        [Activity(Label = "StopRapidMultipleButtonClicksActivity")]
        public class StopRapidMultipleButtonClicksActivity : Activity {
    
            private Button btn;
            private long LastButtonClickTime;
    
            protected override void OnCreate(Bundle savedInstanceState) {
                base.OnCreate(savedInstanceState);
    
                var layout = new LinearLayout(this) { Orientation = Orientation.Vertical };
    
                btn = new Button(this) { Text = "Click Me Quickly!" };
                btn.Click += ButtonClicked;
    
                layout.AddView(btn);
                SetContentView(layout);
            }
    
            protected override void OnResume() {
                base.OnResume();
                LastButtonClickTime = 0;
            }
    
            private void ButtonClicked(object sender, EventArgs e) {
    
                // Don't allow multiple clicks unless they're at least a second apart
                if (SystemClock.ElapsedRealtime() - LastButtonClickTime < 1000) return;
    
                LastButtonClickTime = SystemClock.ElapsedRealtime();
    
                Log.Info("MyApp", "Hello World!");
            }
    
        }
    }
    
  • ClayBrinlee.7986ClayBrinlee.7986 USMember ✭✭

    For Xamarin.Forms users I think you can use something like this, which I'm currently using. I would assume you could do something similar in Xamarin.Android/iOS. Observables make this stuff so much easier.

        buttonClickObservable =
                        Observable
                            .FromEventPattern(
                                x => ScanButton.Clicked += x,
                                x => ScanButton.Clicked -= x
                            )
                            .Throttle(TimeSpan.FromSeconds(2), TaskPoolScheduler.Default)
                            .Subscribe(async x =>
                            {
                                // do something
                            });
    
  • rio_riyario_riya INMember ✭✭

    did you find the solution @Greg.8674

  • JoshuaLatusiaJoshuaLatusia USMember ✭✭

    I Solved this problem with all of my events by creating a static class which registers when an event is fired.
    What it does is save the event by a name and the time it fires. And everytime you fire another event it checks if the Event should or should not fire by checking if the event that is firing is on the list.

    /// <summary>
    /// Struct for storing an fired event by identifier and time
    /// </summary>
    public struct DebouncedEvent
    {
        private DateTime LastBounce;
        private string Identifier;
    }
    
    /// <summary>
    /// Limits Events from firing multiple times by tracking last time they were fired
    /// </summary>
    public static class EventDebouncer
    {
        private static List<DebouncedEvent> eventList = new List<DebouncedEvent>();
    
        public static List<DebouncedEvent> EventList { get => eventList; set => eventList = value; }
    
        /// <summary>
        /// Determines if event should be fired or not
        /// </summary>
        /// <param name="id">The identifier of the event.</param>
        /// <returns>If event may be fired</returns>
        public static bool ShouldBounce(string id)
        {
            ClearEventList();
    
            if (EventList.Any(x => x.Identifier.Equals(id)))
            {
                return false;
            }
            else
            {
                EventList.Add(new DebouncedEvent { Identifier = id, LastBounce = DateTime.Now });
                return true;
            }
        }
    
        /// <summary>
        /// Note Replace 2000 by delay in milliSeconds you want.
        /// Removes Event from the list when it can fire again
        /// </summary>
        private static void ClearEventList()
        {
            EventList.RemoveAll(x => (DateTime.Now - x.LastBounce).TotalMilliseconds > 2000);
        }
    }
    

    Then you put this code in the button on click events.

    /// The event you don't want to fire multiple times
    private void SomeClickEvent(object sender, EventArgs e)
    {
        if (!EventDebouncer.ShouldBounce("SomeName"))
            return;
    
        // Do stuff
    }
    

    This will check if the event may be fired or not by looking at the DebouncedEvent list.

Sign In or Register to comment.