Skip to content

Instantly share code, notes, and snippets.

@hayleyxyz
Forked from pmunin/Debouncer.cs
Last active July 26, 2020 20:41
Show Gist options
  • Save hayleyxyz/41fc925a1df4ff98fec6152cbe2184cd to your computer and use it in GitHub Desktop.
Save hayleyxyz/41fc925a1df4ff98fec6152cbe2184cd 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;
using System.Threading;
using System.Windows.Threading;
/// <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 DisposableAction((DisposableAction d) => {
Monitor.Enter(this);
d.Disposed += (object sender, EventArgs e) => {
Monitor.Exit(this);
};
});
}
public void Debounce() {
//lock (this) //moved to static method debounce
{
if(TimerDisposer != null)
TimerDisposer.Dispose();
TimerDisposer = new DisposableAction((DisposableAction 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.Disposed += (object sender, EventArgs e) => {
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;
});
}
class DisposableAction : IDisposable {
public Action<DisposableAction> Action { get; protected set; }
public event EventHandler Disposed;
public DisposableAction(Action<DisposableAction> action) {
Action = action;
action(this);
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing) {
if(!disposedValue) {
if(disposing) {
// TODO: dispose managed state (managed objects).
if(this.Disposed != null) {
this.Disposed.Invoke(this, EventArgs.Empty);
}
}
// TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
// TODO: set large fields to null.
disposedValue = true;
}
}
// TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
// ~DisposableAction()
// {
// // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
// Dispose(false);
// }
// This code added to correctly implement the disposable pattern.
public void Dispose() {
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
// TODO: uncomment the following line if the finalizer is overridden above.
// GC.SuppressFinalize(this);
}
#endregion
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment