Skip to content

Instantly share code, notes, and snippets.

@sverrirs
Created December 16, 2015 22:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sverrirs/078290e10270e48917b7 to your computer and use it in GitHub Desktop.
Save sverrirs/078290e10270e48917b7 to your computer and use it in GitHub Desktop.
Asynchronous background processor code for WPF or WinForms that behaves similar to the BackgroundWorker class in the .NET framework but leverages the Task framework.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace RectifyLib
{
/// <summary>
/// Base class for the progress argument classes used by the <see cref="AsyncBackgroundProcessor{TResult,TProgressEventArgs,TProcessArgs}"/>
/// class <see cref="AsyncBackgroundProcessor{TResult,TProgressEventArgs,TProcessArgs}.BackgroundProgress"/> event
/// </summary>
public abstract class BackgroundProgressArgs : EventArgs
{
/// <summary>
/// The total number of steps that the process will perform.
/// Must be greater than zero.
/// </summary>
public int TotalSteps { get; }
/// <summary>
/// The current number of step that the process is at.
/// Must be lower or equal to <see cref="TotalSteps"/>
/// </summary>
public int CurrentStep { get; }
protected BackgroundProgressArgs(int currentStep, int totalSteps)
{
Contract.Requires(currentStep <= totalSteps);
Contract.Requires(totalSteps > 0);
CurrentStep = currentStep;
TotalSteps = totalSteps;
}
}
/// <summary>
/// Enables a background worker-esque functionality using the .NET task framework. All progress events and completion handling are raised
/// on the main program thread and require no marshalling between background and foreground threads.
/// </summary>
/// <typeparam name="TResult">The return type of the background call</typeparam>
/// <typeparam name="TProgressEventArgs">Type of progress events that are used when <see cref="BackgroundProgress"/>
/// is raised during execution</typeparam>
/// <typeparam name="TProcessArgs">The arguments sent to the background process logic</typeparam>
public abstract class AsyncBackgroundProcessor<TResult, TProgressEventArgs, TProcessArgs> where TProgressEventArgs : BackgroundProgressArgs
{
private CancellationTokenSource _cancelSource = null;
#region Events
public event EventHandler<TProgressEventArgs> BackgroundProgress;
/// <summary>
/// Raises the <see cref="BackgroundProgress"/> event that signals a change in the current progress of the operation to any listeners.
/// </summary>
/// <param name="e">The progress args</param>
protected virtual void OnBackgroundProgress(TProgressEventArgs e)
{
BackgroundProgress?.Raise(this, e);
}
#endregion
/// <summary>
/// Runs an the process asynchronously for the given <see cref="args"/> arguments.
/// </summary>
/// <param name="args">The arguments to the background process</param>
public Task<TResult> RunAsync(TProcessArgs args)
{
// Create a new cancellation source for the task
_cancelSource?.Dispose();
_cancelSource = new CancellationTokenSource();
var cancellationToken = _cancelSource.Token;
return new TaskFactory<TResult>().StartNew(CreateAsyncProcess(args, cancellationToken), cancellationToken);
}
/// <summary>
/// Cancel an ongoing background job. If nothing is running or another cancellation is already
/// pending calling this funciton will have no effect.
/// </summary>
public void CancelAsync()
{
if (_cancelSource != null && !_cancelSource.IsCancellationRequested)
_cancelSource.Cancel();
}
/// <summary>
/// This function returns a functor that defines the background action to take. This function must be implemented by the
/// inheriting class and contains the core function that the processor should perform in the background.
/// This function is called when the <see cref="RunAsync"/> function is invoked.
/// Call <see cref="OnBackgroundProgress"/> within this method body to report progress to subscribers.
/// </summary>
/// <returns></returns>
protected abstract Func<TResult> CreateAsyncProcess(TProcessArgs args, CancellationToken cancellationToken);
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace RectifyLib.Correct
{
public class Corrector : AsyncBackgroundProcessor<CorrectionResults, CorrectorProgressArgs, FileAnalysis[]>
{
protected override Func<CorrectionResults> CreateAsyncProcess(FileAnalysis[] selectedFiles, CancellationToken cancellationToken)
{
return () => CorrectFileLocations(selectedFiles);
}
private CorrectionResults CorrectFileLocations(FileAnalysis[] selectedFiles)
{
// Some code I want to execute on a background thread...
}
}
}
using System;
using System.ComponentModel;
using System.Windows.Threading;
namespace RectifyLib
{
/// <summary>
/// See: http://stackoverflow.com/questions/18881808/raising-events-on-separate-thread
/// </summary>
public static class ThreadingExtensions
{
/// <summary>
/// Extension method which marshals events back onto the main thread for either WPF or Winforms
/// </summary>
/// <param name="multicast"></param>
/// <param name="sender"></param>
/// <param name="args"></param>
public static void Raise(this MulticastDelegate multicast, object sender, EventArgs args)
{
foreach (Delegate del in multicast.GetInvocationList())
{
// Try for WPF first
DispatcherObject dispatcherTarget = del.Target as DispatcherObject;
if (dispatcherTarget != null && !dispatcherTarget.Dispatcher.CheckAccess())
{
// WPF target which requires marshaling
dispatcherTarget.Dispatcher.BeginInvoke(del, sender, args);
}
else
{
// Maybe its WinForms?
ISynchronizeInvoke syncTarget = del.Target as ISynchronizeInvoke;
if (syncTarget != null && syncTarget.InvokeRequired)
{
// WinForms target which requires marshaling
syncTarget.BeginInvoke(del, new object[] { sender, args });
}
else
{
// Just do it.
del.DynamicInvoke(sender, args);
}
}
}
}
/// <summary>
/// Extension method which marshals actions back onto the main thread
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="action"></param>
/// <param name="args"></param>
public static void Raise<T>(this Action<T> action, T args)
{
// Try for WPF first
DispatcherObject dispatcherTarget = action.Target as DispatcherObject;
if (dispatcherTarget != null && !dispatcherTarget.Dispatcher.CheckAccess())
{
// WPF target which requires marshaling
dispatcherTarget.Dispatcher.BeginInvoke(action, args);
}
else
{
// Maybe its WinForms?
ISynchronizeInvoke syncTarget = action.Target as ISynchronizeInvoke;
if (syncTarget != null && syncTarget.InvokeRequired)
{
// WinForms target which requires marshaling
syncTarget.BeginInvoke(action, new object[] { args });
}
else
{
// Just do it.
action.DynamicInvoke(args);
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment