Skip to content

Instantly share code, notes, and snippets.

@Gh61
Created March 15, 2024 17:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Gh61/6ad00d8b69e98c5d9f8e80567a825973 to your computer and use it in GitHub Desktop.
Save Gh61/6ad00d8b69e98c5d9f8e80567a825973 to your computer and use it in GitHub Desktop.
Class for creating debounced method in C# (includes support for Dispatcher)
using System;
using System.Threading;
using System.Windows.Threading;
namespace Gh61
{
/// <summary>
/// Class for creating debounced variant of given Action, delay for given milliseconds.
/// Every <see cref="Invoke()"/> call will start the delay counting again.
/// </summary>
/// <remarks>
/// Not hitting the delay counter:
/// |--------------X |--------------X
/// ^ Invoke() ^ action ^ Invoke() ^ action
///
/// Hitting the delay counter:
/// |-----------|--------------X
/// ^ Invoke() ^ Invoke() ^ action
///
/// Cancelling
/// |-----------_
/// ^ Invoke() ^ Cancel()
/// </remarks>
public class Debounced
{
private Thread _thread;
private readonly Action _action;
private readonly int _waitMs;
private readonly int _tickMs;
private volatile int _remainingTicks;
private volatile bool _isCancelled;
/// <summary>
/// Will create debounced verion of given action.
/// </summary>
/// <param name="dispatcher">Dispatcher for running <see cref="action"/> within.</param>
/// <param name="action">Action to debounce.</param>
/// <param name="waitMs">How logn to wait before the action will be invoked.</param>
public Debounced(Dispatcher dispatcher, Action action, int waitMs)
: this(CreateDispatched(action, dispatcher), waitMs)
{
}
/// <summary>
/// Will create debounced verion of given action.
/// </summary>
/// <param name="action">Action to debounce.</param>
/// <param name="waitMs">How logn to wait before the action will be invoked.</param>
public Debounced(Action action, int waitMs)
{
if (waitMs < 5)
throw new ArgumentOutOfRangeException(nameof(waitMs), $@"The minimal value of {nameof(waitMs)} is 5.");
_action = action ?? throw new ArgumentNullException(nameof(action));
_waitMs = waitMs;
_tickMs = waitMs / 5;
}
/// <summary>
/// Will start the waiting before action invoke.
/// </summary>
public Debounced Invoke()
{
_remainingTicks = _waitMs / _tickMs;
_isCancelled = false;
if (_thread == null)
{
_thread = new Thread(ThreadStart);
_thread.IsBackground = true;
_thread.Start();
}
return this;
}
/// <summary>
/// Will cancel the pending waiting and stops the action invoke.
/// </summary>
public Debounced Cancel()
{
_isCancelled = true;
return this;
}
private void ThreadStart()
{
while (true)
{
Thread.Sleep(_tickMs);
Interlocked.Decrement(ref _remainingTicks);
if (_isCancelled)
{
break;
}
if (_remainingTicks <= 0 && _action != null)
{
_action();
break;
}
}
_thread = null;
}
private static Action CreateDispatched(Action action, Dispatcher dispatcher)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
if (dispatcher == null)
throw new ArgumentNullException(nameof(dispatcher));
return () =>
{
if (dispatcher.CheckAccess())
{
action();
}
else
{
dispatcher.Invoke(action);
}
};
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment