Skip to content

Instantly share code, notes, and snippets.

@pmunin
Last active December 20, 2022 02:03
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save pmunin/634d8971be1e0e7b16edfd548f2a8526 to your computer and use it in GitHub Desktop.
Save pmunin/634d8971be1e0e7b16edfd548f2a8526 to your computer and use it in GitHub Desktop.
Action Debouncer for C#
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Timers;
namespace DebounceUtils
{
/// <summary>
/// Event debouncer helps to prevent calling the same event handler too often (like mark Dirty or Invalidate)
/// </summary>
public static class Debouncer
{
/// <summary>
/// Configuration contract of debouncer
/// </summary>
public interface IConfig
{
/// <summary>
/// Timeout that timer will countdown
/// </summary>
TimeSpan Timeout { get; }
/// <summary>
/// Sync invoke object for timer
/// </summary>
ISynchronizeInvoke SyncronizationObjectForTimer { get; }
/// <summary>
/// Action that will be invoked when timeout passed
/// </summary>
/// <param name="config"></param>
void OnTimeout();
}
/// <summary>
/// Internal debouncer entry
/// </summary>
class DebounceRegistration : IDisposable
{
public object Key;
public IConfig Config;
//public Timer Timer;
public IDisposable TimerDisposer = null;
public void Dispose()
{
TimerDisposer?.Dispose();
DebounceRegistration foo;
activeDebouncers.TryRemove(this.Key, out foo);
}
public IDisposable Lock()
{
return new DisposeAction(a => {
Monitor.Enter(this);
a.OnDispose = () =>
{
Monitor.Exit(this);
};
});
}
public void Debounce()
{
//lock (this) //moved to static method debounce
{
if (TimerDisposer != null)
TimerDisposer.Dispose();
TimerDisposer = new DisposeAction(d => {
//TODO: consider using System.Threading.Timer instead
var timer = null as System.Timers.Timer;
if (timer == null)
{
timer = new System.Timers.Timer();
timer.Elapsed += Timer_Elapsed;
timer.AutoReset = false;
timer.Enabled = false;
}
timer.Interval = Config.Timeout.TotalMilliseconds;
timer.SynchronizingObject = Config.SyncronizationObjectForTimer;
timer.Start();
d.OnDispose = () => {
using (this.Lock())
{
timer.Stop();
timer.Elapsed -= Timer_Elapsed;
timer.Dispose();
this.TimerDisposer = null;
}
};
});
}
}
void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
//fist we dispose debouncer, and then invoke it's action
using (this.Lock())
{
Dispose();
}
//Happening in syncronized mode already thanks to Timer.SyncronizingObject
//but if dispatcher is used, then need to sync manually
Config.OnTimeout();
}
}
/// <summary>
/// Debounce abstract key-ed object
/// </summary>
/// <typeparam name="TConfig"></typeparam>
/// <param name="key"></param>
/// <param name="createConfigIfNotExist"></param>
/// <param name="configDebounce"></param>
public static void Debounce<TConfig>(object key, Func<TConfig> createConfigIfNotExist, Action<TConfig> configDebounce)
where TConfig : IConfig
{
bool created = false;
var debouncer = activeDebouncers.GetOrAdd(key, k => {
created = true;
var config = createConfigIfNotExist();
var res = new DebounceRegistration()
{
Key = key,
Config = config
};
configDebounce(config);
return res;
});
using (debouncer.Lock())
{
if (!created)
configDebounce((TConfig)debouncer.Config);
debouncer.Debounce();
}
}
static ConcurrentDictionary<object, DebounceRegistration> activeDebouncers = new ConcurrentDictionary<object, DebounceRegistration>();
public abstract class ConfigBase : IConfig
{
public ISynchronizeInvoke SyncronizationObject { get; set; }
ISynchronizeInvoke IConfig.SyncronizationObjectForTimer
{
get
{
return SyncronizationObject;
}
}
public Dispatcher SyncronizationDispatcher { get; set; }
public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(500);
void IConfig.OnTimeout()
{
OnTimeout();
}
protected void OnTimeout()
{
Action doInvoke = InvokeOnTimeout;
if (SyncronizationDispatcher != null)
SyncronizationDispatcher.BeginInvoke(doInvoke, null);
else
doInvoke();
}
protected abstract void InvokeOnTimeout();
}
/// <summary>
/// Standard action debounce config
/// </summary>
public class ActionConfig : ConfigBase
{
public new Action<ActionConfig> OnTimeout;
public object Data;
protected override void InvokeOnTimeout()
{
OnTimeout?.Invoke(this);
}
}
public static void DebounceAction(object key, Action<ActionConfig> actionOnTimeout, Dispatcher sync, TimeSpan ? timeout = null)
{
DebounceActionCustom(key, d => {
d.OnTimeout = actionOnTimeout;
d.SyncronizationObject = null;
d.SyncronizationDispatcher = sync;
if (timeout.HasValue)
d.Timeout = timeout.Value;
});
}
public static void DebounceAction(object key, Action<ActionConfig> actionOnTimeout, TimeSpan ? timeout=null, ISynchronizeInvoke sync=null)
{
DebounceActionCustom(key, d => {
d.OnTimeout = actionOnTimeout;
d.SyncronizationObject = sync;
d.SyncronizationDispatcher = null;
if (timeout.HasValue)
d.Timeout = timeout.Value;
});
}
public static void DebounceActionCustom(object key, Action<ActionConfig> configDebounce)
{
Debounce(key, ()=>new ActionConfig(), configDebounce);
}
public class DebounceQueueConfig<T> : ConfigBase
{
/// <summary>
/// Does not have to be thread-safe, because it's already thread-safe due to DebounceRegistration
/// </summary>
public List<T> Queue { get; } = new List<T>();
public new Action<DebounceQueueConfig<T>> OnTimeout;
protected override void InvokeOnTimeout()
{
OnTimeout(this);
}
}
/// <summary>
/// Enqueue item to debounce
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="itemToEnqueue"></param>
/// <param name="actionOnQueueOnTimeout"></param>
/// <param name="timeout"></param>
/// <param name="dispatcher"></param>
/// <param name="syncObj"></param>
public static void DebounceQueue<T>(object key, T itemToEnqueue, Action<DebounceQueueConfig<T>> actionOnQueueOnTimeout, TimeSpan? timeout = null, Dispatcher dispatcher = null, ISynchronizeInvoke syncObj=null)
{
Debounce(key, () => new DebounceQueueConfig<T>(), config => {
config.Queue.Add(itemToEnqueue);
config.OnTimeout = actionOnQueueOnTimeout;
if (timeout.HasValue) config.Timeout = timeout.Value;
config.SyncronizationDispatcher = dispatcher;
});
}
}
}
@PrinceOwen9466
Copy link

Interesting Piece.
There is no definition for DisposeAction though. Does it come with .NET 5?

@pmunin
Copy link
Author

pmunin commented Dec 19, 2020

There is no definition for DisposeAction though. Does it come with .NET 5?

Sorry for late response. DisposeAction in another gist

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment