Я написал библиотеку классов для создания фоновых операций на основе таймера в проектах .NET. Идея состоит в том, чтобы иметь возможность создавать и управлять (запускать / останавливать / возобновлять / отменять) повторяющиеся фоновые операции, а также иметь возможность опрашивать и / или реагировать на изменения в состоянии фоновой операции.
Библиотека состоит в основном из двух вещей:
RecurringOperation модель / класс
- Инкапсулирует и представляет одно действие / метод, который выполняется через определенные промежутки времени.
- Представляет состояние повторяющейся операции, предоставляя свойства и события для привязки.
- Поддерживает шаблон XAML и MVVM для привязки элементов управления пользовательского интерфейса к состоянию повторяющейся операции.
RecurringOperationManager одноэлементный класс
- Отвечает за управление всеми объектами RecurringOperation внутри приложения (запуск / остановка / отмена).
- Отвечает за выполнение повторяющихся операций потокобезопасным способом и следит за тем, чтобы одна повторяющаяся операция не «ставилась в очередь», если выполнение повторяющейся операции занимает больше времени, чем указанный интервал для этой операции.
Я хотел бы узнать ваше мнение о
- Общее качество и удобочитаемость кода (любые явные баги или ошибки)
- Архитектура и реализация RecurringOperation а также RecurringOperationManager классы
Чтобы получить общее представление об использовании библиотеки, ознакомьтесь с файлом readme на странице github по адресу
https://github.com/TDMR87/Recurop
Чтобы увидеть используемую библиотеку, взгляните на эту страницу github. Проект создает программу секундомера в WPF с использованием этой библиотеки.
https://github.com/TDMR87/RecuropDemo
Реализация RecurringOperation класс выглядит так:
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;
}
}
А класс RecurringOperationManager выглядит так:
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; }
}
}