I have written a class library for creating timer-based bacground operations in .NET projects. The idea is to be able to create and manage (start/stop/resume/cancel) recurring background operations and also to be able to poll and/or react to changes in the background operation's state.
The library consists mainly of two things:
RecurringOperation model/class
- Encapsulates and represents a single action/method which is executed in specified intervals.
- Represents the state of the recurring operation, providing properties and events to bind to.
- Supports XAML and MVVM pattern to bind UI controls to the state of the recurring operation.
RecurringOperationManager singleton class
- Responsible for managing all RecurringOperation objects inside an application (starting/stopping/resuming).
- Responsible for executing recurring operations in a thread safe way and makes sure that a single recurring operation does not "queue up" if the execution of a recurring operation takes longer than the specified interval for that operation.
I'd like your opinion on
- The overall code quality and readability (any glaring bugs or mistakes)
- The architecture and implemenation of the RecurringOperation and RecurringOperationManager classes
In order to get the general idea about the usage of the library, please take a look at the readme on the github page at https://github.com/TDMR87/Recurop
To see the library being used, take a look at this github page. The project creates a stopwatch program in WPF using this library. https://github.com/TDMR87/RecuropDemo
The implementation of the RecurringOperation class looks like this:
public class RecurringOperation : INotifyPropertyChanged
{
public RecurringOperation(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new InvalidOperationException(Constants.UnnamedOperationException);
_name = name;
IsExecuting = false;
IsRecurring = false;
IsNotRecurring = true;
IsPaused = false;
IsIdle = true;
IsCancelled = false;
IsNotCancelled = true;
CallbackLock = new object();
Status = RecurringOperationStatus.Idle;
CanBeStarted = true;
}
private readonly string _name;
/// <summary>
/// Indicates whether the recurring operation can be started.
/// </summary>
public bool CanBeStarted
{
get => canBeStarted;
internal set
{
canBeStarted = value;
OnPropertyChanged();
}
}
private bool canBeStarted;
/// <summary>
/// Indicates whether the recurring background operation is
/// currently executing it's specified action.
/// </summary>
public bool IsExecuting
{
get => isExecuting;
internal set
{
isExecuting = value;
OnPropertyChanged();
}
}
private bool isExecuting;
/// <summary>
/// Indicates whether the recurring background operation is
/// currently in recurring state (and not cancelled, for example).
/// </summary>
public bool IsRecurring
{
get => isRecurring;
internal set
{
isRecurring = value;
OnPropertyChanged();
}
}
private bool isRecurring;
/// <summary>
/// Indicates whether the recurring background operation is
/// currently in recurring state.
/// </summary>
public bool IsNotRecurring
{
get => isNotRecurring;
internal set
{
isNotRecurring = value;
OnPropertyChanged();
}
}
private bool isNotRecurring;
/// <summary>
/// Indicates whether the recurring background operation is
/// currently in cancelled state.
/// </summary>
public bool IsCancelled
{
get => isCancelled;
internal set
{
isCancelled = value;
OnPropertyChanged();
}
}
private bool isCancelled;
/// <summary>
/// Indicates whether the recurring background operation is
/// currently not in cancelled state.
/// </summary>
public bool IsNotCancelled
{
get => isNotCancelled;
internal set
{
isNotCancelled = value;
OnPropertyChanged();
}
}
private bool isNotCancelled;
/// <summary>
/// Indicates whether the recurring operation is currently paused.
/// </summary>
public bool IsPaused
{
get => isPaused;
internal set
{
isPaused = value;
OnPropertyChanged();
}
}
private bool isPaused;
/// <summary>
/// Indicates whether the recurring operation is idle. An idle state means that
/// the operation is not yet started or it has been aborted.
/// </summary>
public bool IsIdle
{
get => isIdle;
set
{
isIdle = value;
OnPropertyChanged();
}
}
private bool isIdle;
/// <summary>
/// The start time of the latest background operation execution.
/// </summary>
public DateTime LastRunStart
{
get => lastRunStart;
internal set
{
lastRunStart = value;
OnPropertyChanged();
}
}
private DateTime lastRunStart;
/// <summary>
/// The start time of the latest background operation execution.
/// </summary>
public DateTime LastRunFinish
{
get => lastRunFinish;
internal set
{
lastRunFinish = value;
OnPropertyChanged();
}
}
private DateTime lastRunFinish;
public RecurringOperationStatus Status
{
get => status;
internal set
{
status = value;
OnStatusChanged();
OnPropertyChanged();
}
}
private RecurringOperationStatus status;
/// <summary>
/// A lock object used to prevent overlapping threads from modifying
/// the properties of the Background Operation object. Use with lock-statement.
/// </summary>
internal object CallbackLock { get; }
/// <summary>
/// When the Background Operation's action delegate throws an exception,
/// the exception is accessible through this property. The next exception
/// in the action delegate will override any previous value in this property.
/// </summary>
public Exception Exception
{
get => exception;
internal set
{
exception = value;
OnOperationFaulted();
}
}
private Exception exception;
/// <summary>
/// The event handler is triggered whenever the status of
/// the background operation changes.
/// </summary>
public event Action StatusChanged;
/// <summary>
/// The event handler is triggered when the
/// Background Operation's action delegate throws an exception.
/// </summary>
public event Action<Exception> OperationFaulted;
/// <summary>
/// The event handler is triggered when properties of the Background Operation
/// change value.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
protected void OnStatusChanged()
{
StatusChanged?.Invoke();
}
protected void OnOperationFaulted()
{
OperationFaulted?.Invoke(Exception);
}
/// <summary>
/// Returns the identifying name of this background operation.
/// </summary>
/// <returns></returns>
public string GetName()
{
if (string.IsNullOrWhiteSpace(_name))
{
throw new InvalidOperationException(Constants.UninitializedOperationException);
}
return _name;
}
/// <summary>
/// Returns the identifying name of this background operation.
/// </summary>
/// <returns></returns>
public override string ToString()
{
if (string.IsNullOrWhiteSpace(_name))
{
throw new InvalidOperationException(Constants.UninitializedOperationException);
}
return _name;
}
}
And the RecurringOperationManager class looks like this:
public sealed class RecurringOperations
{
private static RecurringOperations instance = null;
private static readonly object instanceLock = new object();
private readonly List<Operation> recurringOperations;
/// <summary>
/// Private singleton constructor.
/// </summary>
private RecurringOperations()
{
recurringOperations = new List<Operation>();
}
/// <summary>
/// Public singleton instance.
/// </summary>
public static RecurringOperations Manager
{
get
{
lock (instanceLock)
{
if (instance == null) instance = new RecurringOperations();
return instance;
}
}
}
/// <summary>
/// Resumes the execution of the specified recurring operation
/// if currently in Paused state. Resuming a cancelled operation
/// throws an exception.
/// </summary>
/// <param name="operation"></param>
/// <param name="interval"></param>
/// <param name="action"></param>
/// <param name="startImmediately"></param>
public void ResumeRecurring(RecurringOperation operation, bool startImmediately = false)
{
// If an uninitialized background operation object was given as an argument
if (operation == null || string.IsNullOrWhiteSpace(operation.GetName()))
throw new InvalidOperationException(Constants.UninitializedOperationException);
// If cancelled
if (operation.Status == RecurringOperationStatus.Cancelled)
{
var exception = new InvalidOperationException(Constants.CancelledOperationException);
operation.Exception = exception;
throw exception;
}
// Search the Manager's collection of operations for an operation
var recurringOperation =
recurringOperations.Find(op => op.Name.Equals(operation.GetName()));
// If the operation was found and is not recurring
if (recurringOperation != null)
{
// Set background operation status
operation.Status = RecurringOperationStatus.Idle;
operation.IsIdle = true;
operation.IsRecurring = true;
operation.IsNotRecurring = false;
operation.IsPaused = false;
operation.IsExecuting = false;
// Re-start the operation
recurringOperation.Timer.Change(
startImmediately ? TimeSpan.Zero : recurringOperation.Interval, recurringOperation.Interval);
}
}
/// <summary>
/// Creates and/or starts a recurring operation.
/// If an operation has already been started,
/// an exception will be thrown.
/// </summary>
/// <param name="backgroundOperation"></param>
/// <param name="interval"></param>
/// <param name="action"></param>
/// <param name="startImmediately"></param>
public void StartRecurring(RecurringOperation backgroundOperation, TimeSpan interval, Action action, bool startImmediately = false)
{
// If an uninitialized background operation object was given as an argument
if (backgroundOperation == null || string.IsNullOrWhiteSpace(backgroundOperation.GetName()))
throw new InvalidOperationException(Constants.UninitializedOperationException);
// Search the Manager's collection of operations for an operation
// that corresponds with the background operation
var recurringOperation =
recurringOperations.Find(op => op.Name.Equals(backgroundOperation.GetName()));
// If operation has already been registered
if (recurringOperation != null)
{
throw new InvalidOperationException($"{Constants.RecurringOperationException}. " +
$"Operation name '{recurringOperation.Name}'.");
}
// Create a new recurring operation object
var operation = new Operation
{
Name = backgroundOperation.GetName(),
Interval = interval
};
// This is a local function here..
// ..Create a callback function for the timer
void TimerCallback(object state)
{
// To indicate if a thread has entered
// a block of code.
bool lockAcquired = false;
try
{
// Try to acquire a lock.
// Sets the value of the lockAcquired, even if the method throws an exception,
// so the value of the variable is a reliable way to test whether the lock has to be released.
Monitor.TryEnter(backgroundOperation.CallbackLock, ref lockAcquired);
// If lock acquired
if (lockAcquired)
{
// Set background operation status
backgroundOperation.LastRunStart = DateTime.Now;
backgroundOperation.IsExecuting = true;
backgroundOperation.IsIdle = false;
backgroundOperation.Status = RecurringOperationStatus.Executing;
try
{
action();
}
catch (Exception ex)
{
// Set reference to the catched exception in the background operations exception property
backgroundOperation.Exception = ex;
}
finally
{
// Set last run finish time
backgroundOperation.LastRunFinish = DateTime.Now;
// Set background operation state
backgroundOperation.IsExecuting = false;
backgroundOperation.IsIdle = true;
// If not cancelled
if (backgroundOperation.Status != RecurringOperationStatus.Cancelled)
{
// Set status to idle
backgroundOperation.Status = RecurringOperationStatus.Idle;
}
}
}
}
finally
{
// If a thread acquired the lock
if (lockAcquired)
{
// Release the lock
Monitor.Exit(backgroundOperation.CallbackLock);
}
}
} // End local funtion
// Create a timer that calls the action in specified intervals
// and save the timer in the recurring operations Timer property
operation.Timer = new Timer(
new TimerCallback(TimerCallback), null, startImmediately ? TimeSpan.Zero : interval, interval);
// Add the recurring operation to the Manager's collection
recurringOperations.Add(operation);
// Set status of the client-side background operation
backgroundOperation.Status = RecurringOperationStatus.Idle;
backgroundOperation.IsRecurring = true;
backgroundOperation.IsNotRecurring = false;
backgroundOperation.IsPaused = false;
backgroundOperation.IsIdle = false;
backgroundOperation.CanBeStarted = false;
}
/// <summary>
/// Pauses the recurring execution of the operation.
/// Execution can be continued with a call to ResumeRecurring().
/// </summary>
/// <param name="backgroundOperation"></param>
public void PauseRecurring(RecurringOperation backgroundOperation)
{
// Search the Manager's collection of operations for an operation
// that corresponds with the background operation
var privateOperation =
recurringOperations.Find(
operation => operation.Name.Equals(backgroundOperation.GetName()));
// If found
if (privateOperation != null)
{
// Pause the timer
privateOperation.Timer.Change(Timeout.Infinite, Timeout.Infinite);
// Set the status of the client-side background operation
backgroundOperation.Status = RecurringOperationStatus.Idle;
backgroundOperation.IsRecurring = false;
backgroundOperation.IsNotRecurring = true;
backgroundOperation.IsPaused = true;
backgroundOperation.IsIdle = true;
backgroundOperation.IsExecuting = false;
}
}
/// <summary>
/// Cancels the specified recurring operation.
/// Cancelled operations cannot be resumed, they must be restarted
/// with a call to StartRecurring.
/// </summary>
/// <param name="backgroundOperation"></param>
public void Cancel(RecurringOperation backgroundOperation)
{
// Search the Manager's collection of operations for an operation
// that corresponds with the background operation
var privateOperation =
recurringOperations.Find(
operation => operation.Name.Equals(backgroundOperation.GetName()));
// If found
if (privateOperation != null)
{
// Dispose the timer and the operation object
privateOperation.Timer.Dispose();
recurringOperations.Remove(privateOperation);
// Set the status of the client-side background operation
backgroundOperation.Status = RecurringOperationStatus.Cancelled;
backgroundOperation.IsRecurring = false;
backgroundOperation.IsNotRecurring = true;
backgroundOperation.IsPaused = false;
backgroundOperation.IsExecuting = false;
backgroundOperation.IsIdle = true;
backgroundOperation.IsCancelled = true;
backgroundOperation.IsNotCancelled = false;
backgroundOperation.CanBeStarted = true;
}
}
/// <summary>
/// Private operation data structure.
/// </summary>
private class Operation
{
internal string Name { get; set; }
internal Timer Timer { get; set; }
internal TimeSpan Interval { get; set; }
}
}
System.Threading.Tasks.Task
? Asynchronous Programming. Looks like inventing yet another wheel. :) In general I mean not a replacement your class with .NET one but it may power the solution making it 10x times smaller/simpler/faster. Also learn something aboutCancellationTokenSource
in addition to above link. \$\endgroup\$async
approach to the development. \$\endgroup\$async/await
+TAP features. Btw, the solution has not a bubble-sort-complexity. Afm, to rewrite it correctly with TAP can take a long time, the hour that I have isn't enough. Maybe later. For now I suggest you to edit the post adding links and some information from comments there. \$\endgroup\$