Created
October 8, 2018 05:19
-
-
Save scionwest/388f3d5e00310e3b578bf8a967bc3afc to your computer and use it in GitHub Desktop.
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.Threading; | |
using System.Threading.Tasks; | |
namespace MudDesigner.Engine.Core | |
{ | |
/// <summary> | |
/// <para> | |
/// The Engine Timer allows for starting a timer that will execute a callback at a given interval. | |
/// </para> | |
/// <para> | |
/// The timer may fire: | |
/// - infinitely at the given interval | |
/// - fire once | |
/// - fire _n_ number of times. | |
/// </para> | |
/// <para> | |
/// The Engine Timer will stop its self when it is disposed of. | |
/// </para> | |
/// <para> | |
/// The Timer requires you to provide it an instance that will have an operation performed against it. | |
/// The callback will be given the generic instance at each interval fired. | |
/// </para> | |
/// <para> | |
/// In the following example, the timer is given an instance of an IPlayer. | |
/// It starts the timer off with a 30 second delay before firing the callback for the first time. | |
/// It tells the timer to fire every 60 seconds with 0 as the number of times to fire. When 0 is provided, it will run infinitely. | |
/// Lastly, it is given a callback, which will save the player every 60 seconds. | |
/// <code> | |
/// var timer = new EngineTimer<IPlayer>(new DefaultPlayer()); | |
/// timer.StartAsync(30000, 6000, 0, (player, timer) => player.Save()); | |
/// </code> | |
/// </para> | |
/// </summary> | |
/// <typeparam name="TState">The type that will be provided when the timer callback is invoked.</typeparam> | |
public sealed class EngineTimer<TState> : CancellationTokenSource, IDisposable where TState : class | |
{ | |
/// <summary> | |
/// How many times we have fired the timer thus far. | |
/// </summary> | |
long fireCount = 0; | |
/// <summary> | |
/// Initializes a new instance of the <see cref="EngineTimer{T}"/> class. | |
/// </summary> | |
/// <param name="callback">The callback.</param> | |
/// <param name="state">The state.</param> | |
public EngineTimer(TState state) | |
{ | |
this.StateData = state ?? throw new ArgumentNullException(nameof(state), "EngineTimer constructor requires a non-null argument."); | |
} | |
/// <summary> | |
/// Gets the object that was provided to the timer when it was instanced. | |
/// This object will be provided to the callback at each interval when fired. | |
/// </summary> | |
public TState StateData { get; private set; } | |
/// <summary> | |
/// Gets a value indicating whether the engine timer is currently running. | |
/// </summary> | |
public bool IsRunning { get; private set; } | |
/// <summary> | |
/// <para> | |
/// Starts the timer, firing a synchronous callback at each interval specified until <code>numberOfFires</code> has been reached. | |
/// If <code>numberOfFires</code> is 0, then the callback will be called indefinitely until the timer is manually stopped. | |
/// </para> | |
/// <para> | |
/// The following example shows how to start a timer, providing it a callback. | |
/// </para> | |
/// <code><![CDATA[ | |
/// var timer = new EngineTimer<IPlayer>(new DefaultPlayer()); | |
/// double startDelay = TimeSpan.FromSeconds(30).TotalMilliseconds; | |
/// double interval = TimeSpan.FromMinutes(10).TotalMilliseconds; | |
/// int numberOfFires = 0; | |
/// | |
/// timer.StartAsync( | |
/// startDelay, | |
/// interval, | |
/// numberOfFires, | |
/// async (player, timer) => await player.Save()); | |
/// ]]></code> | |
/// </summary> | |
/// <param name="startDelay"> | |
/// <para> | |
/// The <code>startDelay</code> is used to specify how much time must pass before the timer can invoke the callback for the first time. | |
/// If 0 is provided, then the callback will be invoked immediately upon starting the timer. | |
/// </para> | |
/// <para> | |
/// The <code>startDelay</code> is measured in milliseconds. | |
/// </para> | |
/// </param> | |
/// <param name="interval">The interval in milliseconds.</param> | |
/// <param name="numberOfFires">Specifies the number of times to invoke the timer callback when the interval is reached. Set to 0 for infinite.</param> | |
public void StartAsync(double startDelay, double interval, int numberOfFires, Func<TState, EngineTimer<TState>, Task> callback) | |
{ | |
this.StartTimer( | |
timerDelegate: (task, state) => RunTimerAsync(task, state, interval, numberOfFires), | |
data: new EngineTimerState<TState>(callback, this.StateData), | |
isAsync: true, | |
startDelay: startDelay); | |
} | |
/// <summary> | |
/// Stops the timer for this instance. | |
/// Stopping the timer will not dispose of the EngineTimer, allowing you to restart the timer if you need to. | |
/// </summary> | |
public void Stop() | |
{ | |
if (!this.IsCancellationRequested) | |
{ | |
this.Cancel(); | |
} | |
this.IsRunning = false; | |
} | |
/// <summary> | |
/// Sets the timers current state. | |
/// </summary> | |
/// <param name="state">The state.</param> | |
public void SetState(TState state) | |
{ | |
this.StateData = state; | |
} | |
/// <summary> | |
/// Stops the timer and releases the unmanaged resources used by the <see cref="T:System.Threading.CancellationTokenSource" /> class and optionally releases the managed resources. | |
/// </summary> | |
/// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param> | |
protected override void Dispose(bool disposing) | |
{ | |
if (disposing) | |
{ | |
this.Stop(); | |
} | |
base.Dispose(disposing); | |
} | |
private void StartTimer(Func<Task, EngineTimerState<TState>, Task> timerDelegate, EngineTimerState<TState> data, bool isAsync, double startDelay) | |
{ | |
this.IsRunning = true; | |
Task | |
.Delay(TimeSpan.FromMilliseconds(startDelay), this.Token) | |
.ContinueWith( | |
(Func<Task, object, Task>)timerDelegate, | |
data, | |
this.Token, | |
TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnRanToCompletion, | |
TaskScheduler.Default); | |
} | |
private Task RunTimerAsync(Task task, EngineTimerState<TState> state, double interval, int numberOfFires) | |
{ | |
return this.Run(task, () => state.TimerCallback(state.CurrentState, this), interval, numberOfFires); | |
} | |
private async Task Run(Task task, Func<Task> callback, double interval, int numberOfFires) | |
{ | |
while (!this.IsCancellationRequested) | |
{ | |
// Only increment if we are supposed to. | |
if (numberOfFires > 0) | |
{ | |
this.fireCount++; | |
} | |
await callback(); | |
PerformTimerCancellationCheck(interval, numberOfFires); | |
await Task.Delay(TimeSpan.FromMilliseconds(interval), this.Token).ConfigureAwait(false); | |
} | |
} | |
private void PerformTimerCancellationCheck(double interval, int numberOfFires) | |
{ | |
// If we have reached our fire count, stop. If set to 0 then we fire until manually stopped. | |
if (numberOfFires > 0 && this.fireCount >= numberOfFires) | |
{ | |
this.Stop(); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment