using System; using System.Collections.Generic; using System.Threading; namespace Unclassified { /// /// Implements a timer that invokes a method after a user-defined timeout. /// /// /// Project website: http://beta.unclassified.de/code/dotnet/delayedcall/ /// public class DelayedCall : IDisposable { // This class maintains compatibility with an older version of the DelayedCall class. // See the "Compatibility code" regions for details. To remove the compatibility code, // you just need to remove these regions. public delegate void Callback(); /// /// List to keep track of all created DelayedCall instances /// protected static List dcList; protected System.Timers.Timer timer; protected object timerLock; private Callback callback; protected bool cancelled = false; protected SynchronizationContext context; // More information on the SynchronizationContext thing: http://www.codeproject.com/cs/threads/SyncContextTutorial.asp /// /// Static class constructor, initialising static fields /// static DelayedCall() { dcList = new List(); } /// /// Private instance constructor, initialising instance fields /// protected DelayedCall() { timerLock = new object(); } #region Compatibility code private DelayedCall.Callback oldCallback = null; private object oldData = null; [Obsolete("Use the static method DelayedCall.Create instead.")] public DelayedCall(Callback cb) : this() { PrepareDCObject(this, 0, false); callback = cb; } [Obsolete("Use the static method DelayedCall.Create instead.")] public DelayedCall(DelayedCall.Callback cb, object data) : this() { PrepareDCObject(this, 0, false); oldCallback = cb; oldData = data; } [Obsolete("Use the static method DelayedCall.Start instead.")] public DelayedCall(Callback cb, int milliseconds) : this() { PrepareDCObject(this, milliseconds, false); callback = cb; if (milliseconds > 0) Start(); } [Obsolete("Use the static method DelayedCall.Start instead.")] public DelayedCall(DelayedCall.Callback cb, int milliseconds, object data) : this() { PrepareDCObject(this, milliseconds, false); oldCallback = cb; oldData = data; if (milliseconds > 0) Start(); } [Obsolete("Use the method Restart of the generic class instead.")] public void Reset(object data) { Cancel(); oldData = data; Start(); } [Obsolete("Use the method Restart of the generic class instead.")] public void Reset(int milliseconds, object data) { Cancel(); oldData = data; Reset(milliseconds); } [Obsolete("Use the method Restart instead.")] public void SetTimeout(int milliseconds) { Reset(milliseconds); } #endregion /// /// Creates a new DelayedCall instance. /// /// Callback function /// Time to callback invocation /// Newly created DelayedCall instance that can be used for later controlling of the invocation process public static DelayedCall Create(Callback cb, int milliseconds) { DelayedCall dc = new DelayedCall(); PrepareDCObject(dc, milliseconds, false); dc.callback = cb; return dc; } /// /// Creates a new asynchronous DelayedCall instance. The callback function will be invoked on a ThreadPool thread. /// /// Callback function /// Time to callback invocation /// Newly created DelayedCall instance that can be used for later controlling of the invocation process public static DelayedCall CreateAsync(Callback cb, int milliseconds) { DelayedCall dc = new DelayedCall(); PrepareDCObject(dc, milliseconds, true); dc.callback = cb; return dc; } /// /// Creates and starts a new DelayedCall instance. /// /// Callback function /// Time to callback invocation /// Newly created DelayedCall instance that can be used for later controlling of the invocation process public static DelayedCall Start(Callback cb, int milliseconds) { DelayedCall dc = Create(cb, milliseconds); if (milliseconds > 0) dc.Start(); else if (milliseconds == 0) dc.FireNow(); return dc; } /// /// Creates and starts a new asynchronous DelayedCall instance. The callback function will be invoked on a ThreadPool thread. /// /// Callback function /// Time to callback invocation /// Newly created DelayedCall instance that can be used for later controlling of the invocation process public static DelayedCall StartAsync(Callback cb, int milliseconds) { DelayedCall dc = CreateAsync(cb, milliseconds); if (milliseconds > 0) dc.Start(); else if (milliseconds == 0) dc.FireNow(); return dc; } protected static void PrepareDCObject(DelayedCall dc, int milliseconds, bool async) { if (milliseconds < 0) { throw new ArgumentOutOfRangeException("milliseconds", "The new timeout must be 0 or greater."); } // Get the current synchronization context if required, with dummy fallback dc.context = null; if (!async) { dc.context = SynchronizationContext.Current; if (dc.context == null) throw new InvalidOperationException("Cannot delay calls synchronously on a non-UI thread. Use the *Async methods instead."); } if (dc.context == null) dc.context = new SynchronizationContext(); // Run asynchronously silently dc.timer = new System.Timers.Timer(); if (milliseconds > 0) dc.timer.Interval = milliseconds; dc.timer.AutoReset = false; dc.timer.Elapsed += dc.Timer_Elapsed; Register(dc); } protected static void Register(DelayedCall dc) { lock (dcList) { // Keep a reference on this object to prevent it from being caught by the Garbage Collector if (!dcList.Contains(dc)) dcList.Add(dc); } } protected static void Unregister(DelayedCall dc) { lock (dcList) { // Free a reference on this object to allow the Garbage Collector to dispose it // if no other reference is kept to it dcList.Remove(dc); } } /// /// Gets the number of currently registered DelayedCall instances. /// public static int RegisteredCount { get { lock (dcList) { return dcList.Count; } } } /// /// Gets a value indicating whether any registered DelayedCall instance is still waiting to fire. /// public static bool IsAnyWaiting { get { lock (dcList) { foreach (DelayedCall dc in dcList) { if (dc.IsWaiting) return true; } } return false; } } /// /// Cancels all waiting DelayedCall instances. /// public static void CancelAll() { lock (dcList) { foreach (DelayedCall dc in dcList) { dc.Cancel(); } } } /// /// Fires all waiting DelayedCall instances. /// public static void FireAll() { lock (dcList) { foreach (DelayedCall dc in dcList) { dc.Fire(); } } } /// /// Disposes all registered DelayedCall instances. This will empty the list of registered DelayedCall instances. /// public static void DisposeAll() { lock (dcList) { // Disposing an object removes it from the list, so foreach doesn't work while (dcList.Count > 0) { dcList[0].Dispose(); } } } protected virtual void Timer_Elapsed(object o, System.Timers.ElapsedEventArgs e) { // We're in the timer thread now. FireNow(); Unregister(this); } /// /// Frees all resources of this instance. /// public void Dispose() { Unregister(this); timer.Dispose(); } /// /// Starts or restarts the timer. /// public void Start() { lock (timerLock) { cancelled = false; timer.Start(); Register(this); } } /// /// Stops the timer to prevent the callback function to be invoked. /// public void Cancel() { lock (timerLock) { cancelled = true; Unregister(this); timer.Stop(); } } /// /// Gets a value indicating whether the DelayedCall instance is currently waiting to fire. /// public bool IsWaiting { get { lock (timerLock) { return timer.Enabled && !cancelled; } } } /// /// Fires the callback event regardless of whether the timeout has already finished. /// /// /// The event will not be fired when the timeout has finished before, so when calling Fire /// near the time when the timer would elapse, you won't risk of calling it twice. If you need /// to invoke the callback function now and the timer has already elapsed, use Reset(0). /// public void Fire() { lock (timerLock) { if (!IsWaiting) return; timer.Stop(); } FireNow(); } /// /// Fires the callback event regardless of whether the timeout has already finished or was started at all. /// /// /// The event will be fired even when the timeout has finished before. This is the method used by Fire() /// and it takes care of always unregistering the DelayedCall object from the internal list. /// public void FireNow() { OnFire(); Unregister(this); } protected virtual void OnFire() { // We're in the timer thread now, if invoked through the timer. context.Post(delegate { // We're in the target thread now. lock (timerLock) { // Only fire the callback if the timer wasn't cancelled in this very moment. // This check needs to be done inside the lock and in the target thread because // the invocation in the target thread can be delayed and Cancel() - setting // cancelled to true - may be called after this delegate has been posted to the // target thread. So, checking the cancelled variable here leads to a more current // and definitive decision (the target thread cannot call Cancel() between this // check and the real invocation of the callback method.) if (cancelled) return; } if (callback != null) callback(); #region Compatibility code if (oldCallback != null) oldCallback(oldData); #endregion }, null); } /// /// Resets the timeout and restarts the timer. /// public void Reset() { lock (timerLock) { Cancel(); Start(); // TODO: This sets and unsets the cancelled flag, so immediate execution of the // previous timer right after this method call cannot be eliminated. } } /// /// Resets the timeout to a new value and restarts the timer. /// /// New timeout value public void Reset(int milliseconds) { lock (timerLock) { Cancel(); Milliseconds = milliseconds; Start(); // TODO: This sets and unsets the cancelled flag, so immediate execution of the // previous timer right after this method call cannot be eliminated. // (Derived classes are also affected.) } } /// /// Gets or sets the timeout in milliseconds currently assigned to this DelayedCall instance. /// /// /// Changing the timeout before it elapsed will restart the timer with the new value. /// public int Milliseconds { get { lock (timerLock) { return (int) timer.Interval; } } set { lock (timerLock) { if (value < 0) { throw new ArgumentOutOfRangeException("Milliseconds", "The new timeout must be 0 or greater."); } else if (value == 0) { Cancel(); FireNow(); Unregister(this); } else { timer.Interval = value; // TODO: Is this untested? } } } } } /// /// Implements a timer that invokes a method with 1 argument after a user-defined timeout. /// public class DelayedCall : DelayedCall { public new delegate void Callback(T data); private Callback callback; private T data; /// /// Creates a new DelayedCall instance with 1 data argument. /// /// Callback function /// Argument for callback function /// Time to callback invocation /// Newly created DelayedCall instance that can be used for later controlling of the invocation process public static DelayedCall Create(Callback cb, T data, int milliseconds) { DelayedCall dc = new DelayedCall(); PrepareDCObject(dc, milliseconds, false); dc.callback = cb; dc.data = data; return dc; } /// /// Creates a new asynchronous DelayedCall instance with 1 data argument. The callback function will be invoked on a ThreadPool thread. /// /// Callback function /// Argument for callback function /// Time to callback invocation /// Newly created DelayedCall instance that can be used for later controlling of the invocation process public static DelayedCall CreateAsync(Callback cb, T data, int milliseconds) { DelayedCall dc = new DelayedCall(); PrepareDCObject(dc, milliseconds, true); dc.callback = cb; dc.data = data; return dc; } /// /// Creates and starts a new DelayedCall instance with 1 data argument. /// /// Callback function /// Argument for callback function /// Time to callback invocation /// Newly created DelayedCall instance that can be used for later controlling of the invocation process public static DelayedCall Start(Callback cb, T data, int milliseconds) { DelayedCall dc = Create(cb, data, milliseconds); dc.Start(); return dc; } /// /// Creates and starts a new asynchronous DelayedCall instance with 1 data argument. The callback function will be invoked on a ThreadPool thread. /// /// Callback function /// Argument for callback function /// Time to callback invocation /// Newly created DelayedCall instance that can be used for later controlling of the invocation process public static DelayedCall StartAsync(Callback cb, T data, int milliseconds) { DelayedCall dc = CreateAsync(cb, data, milliseconds); dc.Start(); return dc; } protected override void OnFire() { context.Post(delegate { lock (timerLock) { // Only fire the callback if the timer wasn't cancelled in this very moment if (cancelled) return; } if (callback != null) callback(data); }, null); } /// /// Resets the timeout and callback function argument to a new value and restarts the timer. /// /// New callback function argument /// New timeout value public void Reset(T data, int milliseconds) { lock (timerLock) { Cancel(); this.data = data; Milliseconds = milliseconds; Start(); } } } /// /// Implements a timer that invokes a method with 2 arguments after a user-defined timeout. /// public class DelayedCall : DelayedCall { public new delegate void Callback(T1 data1, T2 data2); private Callback callback; private T1 data1; private T2 data2; /// /// Creates a new DelayedCall instance with 2 data arguments. /// /// Callback function /// First argument for callback function /// Second argument for callback function /// Time to callback invocation /// Newly created DelayedCall instance that can be used for later controlling of the invocation process public static DelayedCall Create(Callback cb, T1 data1, T2 data2, int milliseconds) { DelayedCall dc = new DelayedCall(); PrepareDCObject(dc, milliseconds, false); dc.callback = cb; dc.data1 = data1; dc.data2 = data2; return dc; } /// /// Creates a new asynchronous DelayedCall instance with 2 data arguments. The callback function will be invoked on a ThreadPool thread. /// /// Callback function /// First argument for callback function /// Second argument for callback function /// Time to callback invocation /// Newly created DelayedCall instance that can be used for later controlling of the invocation process public static DelayedCall CreateAsync(Callback cb, T1 data1, T2 data2, int milliseconds) { DelayedCall dc = new DelayedCall(); PrepareDCObject(dc, milliseconds, true); dc.callback = cb; dc.data1 = data1; dc.data2 = data2; return dc; } /// /// Creates and starts a new DelayedCall instance with 2 data arguments. /// /// Callback function /// First argument for callback function /// Second argument for callback function /// Time to callback invocation /// Newly created DelayedCall instance that can be used for later controlling of the invocation process public static DelayedCall Start(Callback cb, T1 data1, T2 data2, int milliseconds) { DelayedCall dc = Create(cb, data1, data2, milliseconds); dc.Start(); return dc; } /// /// Creates and starts a new asynchronous DelayedCall instance with 2 data arguments. The callback function will be invoked on a ThreadPool thread. /// /// Callback function /// First argument for callback function /// Second argument for callback function /// Time to callback invocation /// Newly created DelayedCall instance that can be used for later controlling of the invocation process public static DelayedCall StartAsync(Callback cb, T1 data1, T2 data2, int milliseconds) { DelayedCall dc = CreateAsync(cb, data1, data2, milliseconds); dc.Start(); return dc; } protected override void OnFire() { context.Post(delegate { lock (timerLock) { // Only fire the callback if the timer wasn't cancelled in this very moment if (cancelled) return; } if (callback != null) callback(data1, data2); }, null); } /// /// Resets the timeout and callback function argument to a new value and restarts the timer. /// /// First new callback function argument /// Second new callback function argument /// New timeout value public void Reset(T1 data1, T2 data2, int milliseconds) { lock (timerLock) { Cancel(); this.data1 = data1; this.data2 = data2; Milliseconds = milliseconds; Start(); } } } /// /// Implements a timer that invokes a method with 3 arguments after a user-defined timeout. /// public class DelayedCall : DelayedCall { public new delegate void Callback(T1 data1, T2 data2, T3 data3); private Callback callback; private T1 data1; private T2 data2; private T3 data3; /// /// Creates a new DelayedCall instance with 3 data arguments. /// /// Callback function /// First argument for callback function /// Second argument for callback function /// Third argument for callback function /// Time to callback invocation /// Newly created DelayedCall instance that can be used for later controlling of the invocation process public static DelayedCall Create(Callback cb, T1 data1, T2 data2, T3 data3, int milliseconds) { DelayedCall dc = new DelayedCall(); PrepareDCObject(dc, milliseconds, false); dc.callback = cb; dc.data1 = data1; dc.data2 = data2; dc.data3 = data3; return dc; } /// /// Creates a new asynchronous DelayedCall instance with 3 data arguments. The callback function will be invoked on a ThreadPool thread. /// /// Callback function /// First argument for callback function /// Second argument for callback function /// Third argument for callback function /// Time to callback invocation /// Newly created DelayedCall instance that can be used for later controlling of the invocation process public static DelayedCall CreateAsync(Callback cb, T1 data1, T2 data2, T3 data3, int milliseconds) { DelayedCall dc = new DelayedCall(); PrepareDCObject(dc, milliseconds, true); dc.callback = cb; dc.data1 = data1; dc.data2 = data2; dc.data3 = data3; return dc; } /// /// Creates and starts a new DelayedCall instance with 3 data arguments. /// /// Callback function /// First argument for callback function /// Second argument for callback function /// Third argument for callback function /// Time to callback invocation /// Newly created DelayedCall instance that can be used for later controlling of the invocation process public static DelayedCall Start(Callback cb, T1 data1, T2 data2, T3 data3, int milliseconds) { DelayedCall dc = Create(cb, data1, data2, data3, milliseconds); dc.Start(); return dc; } /// /// Creates and starts a new asynchronous DelayedCall instance with 3 data arguments. The callback function will be invoked on a ThreadPool thread. /// /// Callback function /// First argument for callback function /// Second argument for callback function /// Third argument for callback function /// Time to callback invocation /// Newly created DelayedCall instance that can be used for later controlling of the invocation process public static DelayedCall StartAsync(Callback cb, T1 data1, T2 data2, T3 data3, int milliseconds) { DelayedCall dc = CreateAsync(cb, data1, data2, data3, milliseconds); dc.Start(); return dc; } protected override void OnFire() { context.Post(delegate { lock (timerLock) { // Only fire the callback if the timer wasn't cancelled in this very moment if (cancelled) return; } if (callback != null) callback(data1, data2, data3); }, null); } /// /// Resets the timeout and callback function argument to a new value and restarts the timer. /// /// First new callback function argument /// Second new callback function argument /// Third new callback function argument /// New timeout value public void Reset(T1 data1, T2 data2, T3 data3, int milliseconds) { lock (timerLock) { Cancel(); this.data1 = data1; this.data2 = data2; this.data3 = data3; Milliseconds = milliseconds; Start(); } } } }