Introduction
I fell in love with generics. They can mass produce reusable codes for me. As a token of appreciation, I created a strongly typed background worker group of classes, which is a complex absurdity of its older brother, System.ComponentModel.BackgroundWorker. I am abusing intellisense's ability to generate interface members on VS2008, so I don't have to worry about what methods I have to fulfill to become the perfect candidate for my typed background worker.
The Laborers
using System; using System.ComponentModel; using System.Threading; namespace RFCore.AsyncProcess.Worker { public interface IBackgroundWorker { bool IsBusy { get; } bool CancellationPending { get; } bool WorkerReportsProgress { get; set; } bool WorkerSupportsCancellation { get; set; } void ReportProgress(int percentProgress); void RunWorkerAsync(); void CancelAsync(); } public interface IBackgroundWorker<TArgument, TUserState> : IBackgroundWorker { void ReportProgress(int percentProgress, TUserState userState); void RunWorkerAsync(TArgument argument); } public class TypedBackgroundWorker< TArgument, TResult, TUserState> : MarshalByRefObject, IComponent, IBackgroundWorker<TArgument, TUserState> { #region Private Members private delegate void WorkerThreadStartDelegate(TArgument argument); private readonly WorkerThreadStartDelegate _threadStart; private AsyncOperation _asyncOperation; private readonly SendOrPostCallback _operationCompleted; private readonly SendOrPostCallback _progressReporter; private bool _isRunning; private bool _cancellationPending; #endregion Private Members #region Public Events public event EventHandler<TypedDoWorkEventArgs<TArgument, TResult>> DoWork; public event EventHandler<TypedProgressChangedEventArgs<TUserState>> ProgressChanged; public event EventHandler<TypedRunWorkerCompletedEventArgs<TResult>> RunWorkerCompleted; #endregion Public Events #region Constructors public TypedBackgroundWorker() { _threadStart = WorkerThreadStart; _operationCompleted = AsyncOperationCompleted; _progressReporter = ProgressReporter; } #endregion Constructors #region Protected Methods protected virtual void OnDoWork(TypedDoWorkEventArgs<TArgument, TResult> args) { if (DoWork != null) { DoWork(this, args); } } protected virtual void OnRunWorkerCompleted(TypedRunWorkerCompletedEventArgs<TResult> eArgs) { if (RunWorkerCompleted != null) { RunWorkerCompleted(this, eArgs); } } protected virtual void OnProgressChanged(TypedProgressChangedEventArgs<TUserState> eArgs) { if (ProgressChanged != null) { ProgressChanged(this, eArgs); } } #endregion Protected Methods #region Public Methods public void ReportProgress(int percentProgress) { ReportProgress(percentProgress, default(TUserState)); } public void ReportProgress(int percentProgress, TUserState userState) { if (!WorkerReportsProgress) { throw new InvalidOperationException("Background worker does not report progress."); } TypedProgressChangedEventArgs<TUserState> arg = new TypedProgressChangedEventArgs<TUserState>(percentProgress, userState); if (_asyncOperation != null) { _asyncOperation.Post(_progressReporter, arg); } else { _progressReporter(arg); } } public void RunWorkerAsync() { RunWorkerAsync(default(TArgument)); } public void RunWorkerAsync(TArgument argument) { if (_isRunning) { throw new InvalidOperationException("Background worker is already running"); } _isRunning = true; _cancellationPending = false; _asyncOperation = AsyncOperationManager.CreateOperation(null); _threadStart.BeginInvoke(argument, null, null); } public void CancelAsync() { if (!WorkerSupportsCancellation) { throw new InvalidOperationException("Typed background worker does not support cancellation."); } _cancellationPending = true; } #endregion Public Methods #region Private Methods private void WorkerThreadStart(TArgument argument) { TResult result = default(TResult); Exception error = null; bool cancelled = false; try { TypedDoWorkEventArgs<TArgument, TResult> e = new TypedDoWorkEventArgs<TArgument, TResult>(argument); OnDoWork(e); if (e.Cancel) { cancelled = true; } else { result = e.Result; } } catch (Exception exception) { error = exception; } TypedRunWorkerCompletedEventArgs<TResult> arg = new TypedRunWorkerCompletedEventArgs<TResult>(result, error, cancelled); _asyncOperation.PostOperationCompleted(_operationCompleted, arg); } private void ProgressReporter(object o) { TypedProgressChangedEventArgs<TUserState> eArgs = o as TypedProgressChangedEventArgs<TUserState>; if (eArgs != null) { ProgressReporter(eArgs); } } private void ProgressReporter(TypedProgressChangedEventArgs<TUserState> eArgs) { OnProgressChanged(eArgs); } private void AsyncOperationCompleted(object arg) { TypedRunWorkerCompletedEventArgs<TResult> eArg = arg as TypedRunWorkerCompletedEventArgs<TResult>; if (eArg == null) { return; } AsyncOperationCompleted(eArg); } private void AsyncOperationCompleted(TypedRunWorkerCompletedEventArgs<TResult> arg) { _isRunning = false; _cancellationPending = false; OnRunWorkerCompleted(arg); } #endregion Private Methods #region Public Properties public bool WorkerSupportsCancellation { get; set; } public bool WorkerReportsProgress { get; set; } public bool CancellationPending { get { return _cancellationPending; } } public bool IsBusy { get { return _isRunning; } } #endregion Public Properties #region Implementation of IDisposable /// <summary> /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// </summary> /// <filterpriority>2</filterpriority> public void Dispose() { Dispose(true); // Clean up events if (Disposed != null) { Disposed = null; } if (DoWork != null) { DoWork = null; } if (ProgressChanged != null) { ProgressChanged = null; } if (RunWorkerCompleted != null) { RunWorkerCompleted = null; } GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing) { lock (this) { if ((Site != null) && (Site.Container != null)) { Site.Container.Remove(this); } } if (Disposed != null) { Disposed(this, EventArgs.Empty); } } } #endregion #region Implementation of IComponent /// <summary> /// Gets or sets the <see cref="T:System.ComponentModel.ISite"/> associated with the <see cref="T:System.ComponentModel.IComponent"/>. /// </summary> /// <returns> /// The <see cref="T:System.ComponentModel.ISite"/> object associated with the component; or null, if the component does not have a site. /// </returns> public ISite Site { get; set; } public event EventHandler Disposed; #endregion } public class TypedDoWorkEventArgs<TArgument, TResult> : DoWorkEventArgs { public TypedDoWorkEventArgs(TArgument argument) : base(argument) { } public new TArgument Argument { get { if (base.Argument is TArgument && base.Argument != null) { return (TArgument) base.Argument; } return default(TArgument); } } public new TResult Result { get { if (base.Result is TResult && base.Result != null) { return (TResult) base.Result; } return default(TResult); } set { base.Result = value; } } } public class TypedProgressChangedEventArgs<TUserState> : ProgressChangedEventArgs { public TypedProgressChangedEventArgs(int progressPercentage, TUserState userState) : base(progressPercentage, userState) { } public new TUserState UserState { get { if (base.UserState is TUserState && base.UserState != null) { return (TUserState) base.UserState; } return default(TUserState); } } } public class TypedRunWorkerCompletedEventArgs<TResult> : EventArgs { public TypedRunWorkerCompletedEventArgs(TResult result, Exception error, bool cancelled) { Result = result; Cancelled = cancelled; Exception = error; } public TResult Result { get; private set; } public bool Cancelled { get; private set; } public Exception Exception { get; private set; } } }
The Manager
using System; using System.Collections.Generic; using RFCore.AsyncProcess.Worker; namespace RFCore.AsyncProcess { public interface IAsyncProcessCandidate<TArgument, TUserState, TResult> { void UpdateProcess(TypedProgressChangedEventArgs<TUserState> args); void ProcessCompleted(TypedRunWorkerCompletedEventArgs<TResult> args); void DoProcess(TypedDoWorkEventArgs<TArgument, TResult> args, Action<int, TUserState> reportProgress, Func<bool> getCancellationStatus); } public interface IAsyncProgressStatus<TResult> { bool Cancel { get; set; } TResult Result { get; set; } } /// <summary> /// A process manager that takes care of background processes. /// To use this process manager: /// 1. Define process name (string). /// 2. Define argument type to pass into the long process. /// 3. Define user state type to pass around during update and completion. /// 4. Define result type to be received at the end of process /// 5. Define method to run for the long process, taking: /// - AsyncProcessManager.UpdateCancellationStatus parameter to read the process status. /// - Parameter of type as described in (2) /// - Action(int, type described in (3)) delegate parameter /// 6. Define method to be called whenever an update process in (5) is delegated, taking: /// - int type parameter for process value (usually 0 - 100) /// - type as defined in (3) /// 7. Define method to be called when the long process is completed, taking: /// - bool type parameter to read whether the process is cancelled /// - Exception type parameter to read Exception thrown by the process, if any /// - type as defined in (3) to read the final user state /// - type as defined in (4) to read the result of the process. /// </summary> public class AsyncProcessManager { private readonly Dictionary<string, IBackgroundWorker> _workers = new Dictionary<string, IBackgroundWorker>(); #region Singleton stuff private static AsyncProcessManager s_Manager; public static AsyncProcessManager Instance { get { if (s_Manager == null) { s_Manager = new AsyncProcessManager(); } return s_Manager; } } private AsyncProcessManager() { } #endregion public void StartAsyncProcess<TArgument, TUserState, TResult> (string processName, TArgument processParam, IAsyncProcessCandidate<TArgument, TUserState, TResult> processCandidate) { if (string.IsNullOrEmpty(processName) || _workers.ContainsKey(processName)) { throw new Exception("Invalid process name. It may already exist or it is a null or empty string."); } TypedBackgroundWorker<TArgument, TResult, TUserState> worker = new TypedBackgroundWorker<TArgument, TResult, TUserState> { WorkerReportsProgress = true, WorkerSupportsCancellation = true }; _workers.Add(processName, worker); worker.Disposed += (sender, e) => RemoveWorker(sender as IBackgroundWorker); worker.DoWork += (sender, e) => processCandidate.DoProcess(e, ((IBackgroundWorker<TArgument, TUserState>)sender).ReportProgress, () => ((IBackgroundWorker<TArgument, TUserState>)sender).CancellationPending); worker.ProgressChanged += (sender, e) => processCandidate.UpdateProcess(e); worker.RunWorkerCompleted += (sender, e) => { processCandidate.ProcessCompleted(e); ((IDisposable)sender).Dispose(); }; worker.RunWorkerAsync(processParam); } public void StopAsyncProcess(string processName) { _workers[processName].CancelAsync(); } public void StopAllAsyncProcesses() { foreach (IBackgroundWorker worker in _workers.Values) { worker.CancelAsync(); } } public void StartAsyncProcess<TArgument, TUserState, TResult> (string processName, IAsyncProcessCandidate<TArgument, TUserState, TResult> processCandidate) { if (string.IsNullOrEmpty(processName) || _workers.ContainsKey(processName)) { throw new Exception("Invalid process name. It may already exist or it is a null or empty string."); } TypedBackgroundWorker<TArgument, TResult, TUserState> worker = new TypedBackgroundWorker<TArgument, TResult, TUserState> { WorkerReportsProgress = true }; _workers.Add(processName, worker); worker.Disposed += (sender, e) => RemoveWorker(sender as IBackgroundWorker); worker.DoWork += (sender, e) => processCandidate.DoProcess(e, ((IBackgroundWorker<TArgument, TUserState>)sender).ReportProgress, () => ((IBackgroundWorker<TArgument, TUserState>)sender).CancellationPending); worker.ProgressChanged += (sender, e) => processCandidate.UpdateProcess(e); worker.RunWorkerCompleted += (sender, e) => { processCandidate.ProcessCompleted(e); ((IDisposable)sender).Dispose(); }; worker.RunWorkerAsync(); } private void RemoveWorker(IBackgroundWorker worker) { KeyValuePair<string, IBackgroundWorker>? workerToRemove = null; foreach (var w in _workers) { if (w.Value == worker) { workerToRemove = w; break; } } if (workerToRemove.HasValue) { _workers.Remove(workerToRemove.Value.Key); } } } }
Words of Wisdom
The manager is a a singleton. The client code that wishes to run a specific process in the background shall make a call to the BeginAsyncProcess method. The idea of using the manager can be pointed out as follows:
- The client code defines the Work, Update, Complete methods to run.
- Multiple background workers run.
- As long as the process name is known, in multiple background work scenario, cancellation can be done on specific worker.
- Strongly typed result and states.
Words of Doom
It's generic. The client code must have a premonition in order to know what specific type they would expect, what intermediary state type the work should return in update process, and what specific type the background worker should accept as an argument, if any.