-
-
Save hayleyxyz/41fc925a1df4ff98fec6152cbe2184cd to your computer and use it in GitHub Desktop.
Action Debouncer for C#
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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