Introduction
The BackgroundWorker
class is used to manage work that is done in a background thread and report progress to the UI, avoiding cross-threading problems. Implementing all BackgroundWorker
logic is a bit dirty and does take place in your code.
In this article, I present a wrapper class which handles some work you usually do. It's useful for situations when you need execution of a set of independent tasks.
An example might be sending mail to several recipients or processing a set of images.
Using the Code
The BWHelper
class aggregates the actions that should be done. It handles BackgroundWorker.DoWork
and checks whether the user decided to use parallel or sequential execution. Then it executes all Action
s, checking before each execution if cancellation was asked.
The class also computes the percentage done and the probable time left (based on the average execution time of completed tasks).
public class BWHelper
{
private IEnumerable<Action> toDo;
private DateTime startTime;
private bool isParallel = false;
private BackgroundWorker worker;
private ValueMonitor<int> percentageProgress = new ValueMonitor<int>(0);
private ValueMonitor<TimeSpan> timeLeft = new ValueMonitor<TimeSpan>(TimeSpan.MaxValue);
public void SetActionsTodo( IEnumerable<Action> toDoActions)
{
toDo = toDoActions;
}
public bool IsParallel
{
get { return isParallel; }
set { isParallel = value; }
}
public IValueMonitor<TimeSpan> TimeLeft { get { return timeLeft; } }
public BWHelper(BackgroundWorker aWorker)
{
worker = aWorker;
worker.WorkerReportsProgress = true;
worker.WorkerSupportsCancellation = true;
percentageProgress.ValueChanged +=
new ValueChangedDelegate<int>(percentageProgress_ValueChanged);
worker.DoWork += new DoWorkEventHandler(worker_DoWork);
}
public BWHelper(IEnumerable<Action> actionsToDo, BackgroundWorker aWorker):this(aWorker)
{
toDo = actionsToDo;
}
public int Total
{
get
{
if (toDo == null) return 0;
return toDo.Count();
}
}
private void percentageProgress_ValueChanged(int oldValue, int newValue)
{
worker.ReportProgress(newValue);
}
private void worker_DoWork(object sender, DoWorkEventArgs e)
{
if (toDo == null)
throw new InvalidOperationException("You must provide actions to execute");
int total = toDo.Count();
startTime = DateTime.Now;
int current = 0;
if (isParallel == false)
{
foreach (var next in toDo)
{
next();
current++;
if (worker.CancellationPending == true) return;
percentageProgress.Value = (int)((double)current / (double)total * 100.0);
double passedMs = (DateTime.Now - startTime).TotalMilliseconds;
double oneUnitMs = passedMs / current;
double leftMs = (total - current) * oneUnitMs;
timeLeft.Value = TimeSpan.FromMilliseconds(leftMs);
}
}
else
{
Parallel.For(0, total - 1,
(index, loopstate) =>
{
toDo.ElementAt(index)();
if (worker.CancellationPending == true) loopstate.Stop();
Interlocked.Increment(ref current);
percentageProgress.Value = (int)((double)current / (double)total * 100.0);
double passedMs = (DateTime.Now - startTime).TotalMilliseconds;
double oneUnitMs = passedMs / current;
double leftMs = (total - current) * oneUnitMs;
timeLeft.Value = TimeSpan.FromMilliseconds(leftMs);
}
);
}
}
}
The ValueMonitor
class is intensively used for int
and TimeSpan
value types. Note that because TimeLeft
change is signaled by means of the ValueMonitor.ValueChanged
event rather than doing this using BackgroundWorker
, you should implement the cross-threading logic yourself. In the example project, for the sake of simplicity and because I'm lazy, the CheckForIllegelCrossThreadCalls
property is set to true
.
The example project is a Windows Forms application which executes "useful" tasks, each taking 200 ms. It also shows the possible time left. You also may choose either parallel or sequential execution.
When the Start button is hit, the following code is executed:
List<Action> actions = new List<Action>();
for (int i = 0; i< 100; i++)
actions.Add( () => Thread.Sleep(200) );
helper.SetActionsTodo(actions);
helper.IsParallel = checkBoxUseParallel.Checked;
backgroundWorker.RunWorkerAsync();
The things we gain from BWHelper
are:
- Percentage counting is concentrated in one place. You also don't have to worry about the
ReportProgress
method. - Average time left is measured. You may handle the
BWHelper.TimeLeft.ValueChanged
event to show this in the UI. - Easy switch between sequential and parallel actions execution.
- Cancellation logic is also not your task.
- The properties
WorkerReportsProgress
and WorkerSupportsCancellation
are set to true
in the constructor.
While we've got these advantages, it should be noted what this class can't provide:
BWHelper
is not suitable when your Actions depend on results of each other. - Remember, that if some of your actions take too long, the cancellation may also take a long time.
History
- 22nd January, 2012 - First published