Last active
December 20, 2022 02:03
-
-
Save pmunin/634d8971be1e0e7b16edfd548f2a8526 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; | |
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; | |
}); | |
} | |
} | |
} |
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
Interesting Piece.
There is no definition for
DisposeAction
though. Does it come with .NET 5?