Skip to content

Instantly share code, notes, and snippets.

@CodingOctocat
Created August 29, 2022 13:01
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 CodingOctocat/f9a28dd124d52f707a4a9fc8a4832bec to your computer and use it in GitHub Desktop.
Save CodingOctocat/f9a28dd124d52f707a4a9fc8a4832bec to your computer and use it in GitHub Desktop.
Debounce Dispatcher.
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using System.Timers;
using System.Windows.Threading;
namespace CodingNinja.Common;
/// <summary>
/// Debounce Dispatcher.
/// <para>
/// forked from: <seealso href="https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/84adf91186bfe5fff007a730fb50dcacadd639c0/Microsoft.Toolkit.Uwp/Extensions/DispatcherQueueTimerExtensions.cs">Microsoft.Toolkit.Uwp.Extensions.DispatcherQueueTimerExtensions</seealso>.
/// </para>
/// </summary>
public class DebounceDispatcher
{
private static readonly ConcurrentDictionary<DispatcherTimer, Action> _debounceActionDispatcherTimerInstances = new();
private static readonly ConcurrentDictionary<Timer, Action> _debounceActionTimerInstances = new();
private static readonly ConcurrentDictionary<DispatcherTimer, Func<Task>> _debounceFuncDispatcherTimerInstances = new();
private static readonly ConcurrentDictionary<Timer, Func<Task>> _debounceFuncTimerInstances = new();
private readonly Timer _timer;
private DispatcherTimer _dispatcherTimer;
/// <summary>
/// Create a <seealso cref="DebounceDispatcher"/> instance.
/// </summary>
public DebounceDispatcher()
{
_timer = new();
_dispatcherTimer = new();
}
/// <summary>
/// <para>Used to debounce (rate-limit) an event. The action will be postponed and executed after the interval has elapsed. At the end of the interval, the function will be called with the arguments that were passed most recently to the debounced function.</para>
/// <para>Use this method to control the timer instead of calling Start/Interval/Stop manually.</para>
/// <para>A scheduled debounce can still be stopped by calling the stop method on the timer instance.</para>
/// <para>Each timer can only have one debounced function limited at a time.</para>
/// </summary>
/// <param name="action">Action to execute at the end of the interval.</param>
/// <param name="interval">Interval to wait before executing the action.</param>
/// <param name="immediate">Determines if the action execute on the leading edge instead of trailing edge.</param>
/// <example>
/// <code>
/// private DebounceDispatcher _debounceDispatcher = new();
///
/// _debounceDispatcher.Debounce(() => {
/// // Only executes this code after 0.3 seconds have elapsed since last trigger.
/// }, TimeSpan.FromSeconds(0.3));
/// </code>
/// </example>
public void Debounce(Action action, TimeSpan interval, bool immediate = false)
{
Debounce(action, interval.TotalMilliseconds, immediate);
}
/// <summary>
/// <para>Used to debounce (rate-limit) an event. The action will be postponed and executed after the interval has elapsed. At the end of the interval, the function will be called with the arguments that were passed most recently to the debounced function.</para>
/// <para>Use this method to control the timer instead of calling Start/Interval/Stop manually.</para>
/// <para>A scheduled debounce can still be stopped by calling the stop method on the timer instance.</para>
/// <para>Each timer can only have one debounced function limited at a time.</para>
/// </summary>
/// <param name="action">Action to execute at the end of the interval.</param>
/// <param name="interval">Interval to wait before executing the action.</param>
/// <param name="immediate">Determines if the action execute on the leading edge instead of trailing edge.</param>
/// <example>
/// <code>
/// private DebounceDispatcher _debounceDispatcher = new();
///
/// _debounceDispatcher.Debounce(() => {
/// // Only executes this code after 0.3 seconds have elapsed since last trigger.
/// }, 300);
/// </code>
/// </example>
public void Debounce(Action action, double interval, bool immediate = false)
{
// Check and stop any existing timer
bool timeout = _timer.Enabled;
if (timeout)
{
_timer.Stop();
}
// Reset timer parameters
_timer.Elapsed -= ActionTimer_Elapsed;
_timer.Interval = interval;
if (immediate)
{
// If we're in immediate mode then we only execute if the timer wasn't running beforehand
if (!timeout)
{
action.Invoke();
}
}
else
{
// If we're not in immediate mode, then we'll execute when the current timer expires.
_timer.Elapsed += ActionTimer_Elapsed;
// Store/Update function
_debounceActionTimerInstances.AddOrUpdate(_timer, action, (k, v) => action);
}
// Start the timer to keep track of the last call here.
_timer.Start();
}
/// <summary>
/// <para>Used to debounce (rate-limit) an event. The action will be postponed and executed after the interval has elapsed. At the end of the interval, the function will be called with the arguments that were passed most recently to the debounced function.</para>
/// <para>Use this method to control the timer instead of calling Start/Interval/Stop manually.</para>
/// <para>A scheduled debounce can still be stopped by calling the stop method on the timer instance.</para>
/// <para>Each timer can only have one debounced function limited at a time.</para>
/// </summary>
/// <param name="func">Func to execute at the end of the interval.</param>
/// <param name="interval">Interval to wait before executing the action.</param>
/// <param name="immediate">Determines if the action execute on the leading edge instead of trailing edge.</param>
/// <example>
/// <code>
/// private DebounceDispatcher _debounceDispatcher = new();
///
/// await _debounceDispatcher.DebounceAsync(async () => {
/// // Only executes this code after 0.3 seconds have elapsed since last trigger.
/// }, TimeSpan.FromSeconds(0.3));
/// </code>
/// </example>
public async Task DebounceAsync(Func<Task> func, TimeSpan interval, bool immediate = false)
{
await DebounceAsync(func, interval.TotalMilliseconds, immediate);
}
/// <summary>
/// <para>Used to debounce (rate-limit) an event. The action will be postponed and executed after the interval has elapsed. At the end of the interval, the function will be called with the arguments that were passed most recently to the debounced function.</para>
/// <para>Use this method to control the timer instead of calling Start/Interval/Stop manually.</para>
/// <para>A scheduled debounce can still be stopped by calling the stop method on the timer instance.</para>
/// <para>Each timer can only have one debounced function limited at a time.</para>
/// </summary>
/// <param name="func">Func to execute at the end of the interval.</param>
/// <param name="interval">Interval to wait before executing the action.</param>
/// <param name="immediate">Determines if the action execute on the leading edge instead of trailing edge.</param>
/// <example>
/// <code>
/// private DebounceDispatcher _debounceDispatcher = new();
///
/// await _debounceDispatcher.DebounceAsync(async () => {
/// // Only executes this code after 0.3 seconds have elapsed since last trigger.
/// }, 300);
/// </code>
/// </example>
public async Task DebounceAsync(Func<Task> func, double interval, bool immediate = false)
{
// Check and stop any existing timer
bool timeout = _timer.Enabled;
if (timeout)
{
_timer.Stop();
}
// Reset timer parameters
_timer.Elapsed -= FuncTimer_ElapsedAsync;
_timer.Interval = interval;
if (immediate)
{
// If we're in immediate mode then we only execute if the timer wasn't running beforehand
if (!timeout)
{
await func();
}
}
else
{
// If we're not in immediate mode, then we'll execute when the current timer expires.
_timer.Elapsed += FuncTimer_ElapsedAsync;
// Store/Update function
_debounceFuncTimerInstances.AddOrUpdate(_timer, func, (k, v) => func);
}
// Start the timer to keep track of the last call here.
_timer.Start();
}
/// <summary>
/// <para>Used to debounce (rate-limit) an event. The action will be postponed and executed after the interval has elapsed. At the end of the interval, the function will be called with the arguments that were passed most recently to the debounced function.</para>
/// <para>Use this method to control the timer instead of calling Start/Interval/Stop manually.</para>
/// <para>A scheduled debounce can still be stopped by calling the stop method on the timer instance.</para>
/// <para>Each timer can only have one debounced function limited at a time.</para>
/// </summary>
/// <param name="action">Action to execute at the end of the interval.</param>
/// <param name="interval">Interval to wait before executing the action.</param>
/// <param name="priority">Optional priorty for the dispatcher.</param>
/// <param name="dispatcher">Optional dispatcher. If not passed or null CurrentDispatcher is used.</param>
/// <param name="immediate">Determines if the action execute on the leading edge instead of trailing edge.</param>
/// <example>
/// <code>
/// private DebounceDispatcher _debounceDispatcher = new();
///
/// _debounceDispatcher.DebounceWithDispatcher(() => {
/// // Only executes this code after 0.3 seconds have elapsed since last trigger.
/// }, 300);
/// </code>
/// </example>
public void DebounceWithDispatcher(Action action, double interval, DispatcherPriority priority = DispatcherPriority.Background,
Dispatcher? dispatcher = null, bool immediate = false)
{
DebounceWithDispatcher(action, TimeSpan.FromMilliseconds(interval), priority, dispatcher, immediate);
}
/// <summary>
/// <para>Used to debounce (rate-limit) an event. The action will be postponed and executed after the interval has elapsed. At the end of the interval, the function will be called with the arguments that were passed most recently to the debounced function.</para>
/// <para>Use this method to control the timer instead of calling Start/Interval/Stop manually.</para>
/// <para>A scheduled debounce can still be stopped by calling the stop method on the timer instance.</para>
/// <para>Each timer can only have one debounced function limited at a time.</para>
/// </summary>
/// <param name="action">Action to execute at the end of the interval.</param>
/// <param name="interval">Interval to wait before executing the action.</param>
/// <param name="priority">Optional priorty for the dispatcher.</param>
/// <param name="dispatcher">Optional dispatcher. If not passed or null CurrentDispatcher is used.</param>
/// <param name="immediate">Determines if the action execute on the leading edge instead of trailing edge.</param>
/// <example>
/// <code>
/// private DebounceDispatcher _debounceDispatcher = new();
///
/// _debounceDispatcher.DebounceWithDispatcher(() => {
/// // Only executes this code after 0.3 seconds have elapsed since last trigger.
/// }, TimeSpan.FromSeconds(0.3));
/// </code>
/// </example>
public void DebounceWithDispatcher(Action action, TimeSpan interval, DispatcherPriority priority = DispatcherPriority.Background,
Dispatcher? dispatcher = null, bool immediate = false)
{
// Check and stop any existing timer
bool timeout = _dispatcherTimer.IsEnabled;
if (timeout)
{
_timer.Stop();
}
dispatcher ??= Dispatcher.CurrentDispatcher;
// Reset timer parameters
_dispatcherTimer.Tick -= ActionTimer_Tick;
_dispatcherTimer = new(priority, dispatcher) {
Interval = interval
};
if (immediate)
{
// If we're in immediate mode then we only execute if the timer wasn't running beforehand
if (!timeout)
{
action();
}
}
else
{
// If we're not in immediate mode, then we'll execute when the current timer expires.
_dispatcherTimer.Tick += ActionTimer_Tick;
// Store/Update function
_debounceActionDispatcherTimerInstances.AddOrUpdate(_dispatcherTimer, action, (k, v) => action);
}
// Start the timer to keep track of the last call here.
_dispatcherTimer.Start();
}
/// <summary>
/// <para>Used to debounce (rate-limit) an event. The action will be postponed and executed after the interval has elapsed. At the end of the interval, the function will be called with the arguments that were passed most recently to the debounced function.</para>
/// <para>Use this method to control the timer instead of calling Start/Interval/Stop manually.</para>
/// <para>A scheduled debounce can still be stopped by calling the stop method on the timer instance.</para>
/// <para>Each timer can only have one debounced function limited at a time.</para>
/// </summary>
/// <param name="func">Func to execute at the end of the interval.</param>
/// <param name="interval">Interval to wait before executing the action.</param>
/// <param name="priority">Optional priorty for the dispatcher.</param>
/// <param name="dispatcher">Optional dispatcher. If not passed or null CurrentDispatcher is used.</param>
/// <param name="immediate">Determines if the action execute on the leading edge instead of trailing edge.</param>
/// <example>
/// <code>
/// private DebounceDispatcher _debounceDispatcher = new();
///
/// await _debounceDispatcher.DebounceWithDispatcherAsync(async () => {
/// // Only executes this code after 0.3 seconds have elapsed since last trigger.
/// }, 300);
/// </code>
/// </example>
public async Task DebounceWithDispatcherAsync(Func<Task> func, double interval, DispatcherPriority priority = DispatcherPriority.Background,
Dispatcher? dispatcher = null, bool immediate = false)
{
await DebounceWithDispatcherAsync(func, TimeSpan.FromMilliseconds(interval), priority, dispatcher, immediate);
}
/// <summary>
/// <para>Used to debounce (rate-limit) an event. The action will be postponed and executed after the interval has elapsed. At the end of the interval, the function will be called with the arguments that were passed most recently to the debounced function.</para>
/// <para>Use this method to control the timer instead of calling Start/Interval/Stop manually.</para>
/// <para>A scheduled debounce can still be stopped by calling the stop method on the timer instance.</para>
/// <para>Each timer can only have one debounced function limited at a time.</para>
/// </summary>
/// <param name="func">Func to execute at the end of the interval.</param>
/// <param name="interval">Interval to wait before executing the action.</param>
/// <param name="priority">Optional priorty for the dispatcher.</param>
/// <param name="dispatcher">Optional dispatcher. If not passed or null CurrentDispatcher is used.</param>
/// <param name="immediate">Determines if the action execute on the leading edge instead of trailing edge.</param>
/// <example>
/// <code>
/// private DebounceDispatcher _debounceDispatcher = new();
///
/// await _debounceDispatcher.DebounceWithDispatcherAsync(async () => {
/// // Only executes this code after 0.3 seconds have elapsed since last trigger.
/// }, TimeSpan.FromSeconds(0.3));
/// </code>
/// </example>
public async Task DebounceWithDispatcherAsync(Func<Task> func, TimeSpan interval, DispatcherPriority priority = DispatcherPriority.Background,
Dispatcher? dispatcher = null, bool immediate = false)
{
// Check and stop any existing timer
bool timeout = _dispatcherTimer.IsEnabled;
if (timeout)
{
_timer.Stop();
}
dispatcher ??= Dispatcher.CurrentDispatcher;
// Reset timer parameters
_dispatcherTimer.Tick -= FuncTimer_TickAsync;
_dispatcherTimer = new(priority, dispatcher) {
Interval = interval
};
if (immediate)
{
// If we're in immediate mode then we only execute if the timer wasn't running beforehand
if (!timeout)
{
await func();
}
}
else
{
// If we're not in immediate mode, then we'll execute when the current timer expires.
_dispatcherTimer.Tick += FuncTimer_TickAsync;
// Store/Update function
_debounceFuncDispatcherTimerInstances.AddOrUpdate(_dispatcherTimer, func, (k, v) => func);
}
// Start the timer to keep track of the last call here.
_dispatcherTimer.Start();
}
private void ActionTimer_Elapsed(object? sender, ElapsedEventArgs e)
{
// This event is only registered/run if we weren't in immediate mode above
if (sender is Timer timer)
{
timer.Elapsed -= ActionTimer_Elapsed;
timer.Stop();
if (_debounceActionTimerInstances.TryRemove(timer, out var action))
{
action?.Invoke();
}
}
}
private void ActionTimer_Tick(object? sender, EventArgs e)
{
// This event is only registered/run if we weren't in immediate mode above
if (sender is DispatcherTimer timer)
{
timer.Tick -= ActionTimer_Tick;
timer.Stop();
if (_debounceActionDispatcherTimerInstances.TryRemove(timer, out var action))
{
action?.Invoke();
}
}
}
private async void FuncTimer_ElapsedAsync(object? sender, ElapsedEventArgs e)
{
// This event is only registered/run if we weren't in immediate mode above
if (sender is Timer timer)
{
timer.Elapsed -= FuncTimer_ElapsedAsync;
timer.Stop();
if (_debounceFuncTimerInstances.TryRemove(timer, out var func))
{
if (func is not null)
{
await func();
}
}
}
}
private async void FuncTimer_TickAsync(object? sender, EventArgs e)
{
// This event is only registered/run if we weren't in immediate mode above
if (sender is DispatcherTimer timer)
{
timer.Tick -= FuncTimer_TickAsync;
timer.Stop();
if (_debounceFuncDispatcherTimerInstances.TryRemove(timer, out var func))
{
if (func is not null)
{
await func();
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment