ParallelWork is an open source free helper class that lets you run multiple work in parallel threads, get success, failure and progress update on the WPF UI thread, wait for work to complete, abort all work (in case of shutdown), queue work to run after certain time, chain parallel work one after another. It’s more convenient than using .NET’s BackgroundWorker because you don’t have to declare one component per work, nor do you need to declare event handlers to receive notification and carry additional data through private variables. You can safely pass objects produced from different thread to the success callback. Moreover, you can wait for work to complete before you do certain operation and you can abort all parallel work while they are in-flight. If you are building highly responsive WPF UI where you have to carry out multiple job in parallel yet want full control over those parallel jobs completion and cancellation, then the ParallelWork library is the right solution for you.
I am using the ParallelWork library in my PlantUmlEditor project, which is a free open source UML editor built on WPF. You can see some realistic use of the ParallelWork library there. Moreover, the test project comes with 400 lines of Behavior Driven Development flavored tests, that confirms it really does what it says it does.
The source code of the library is part of the “Utilities” project in PlantUmlEditor source code hosted at Google Code.
The library comes in two flavors, one is the ParallelWork static class, which has a collection of static methods that you can call. Another is the Start class, which is a fluent wrapper over the ParallelWork class to make it more readable and aesthetically pleasing code.
ParallelWork allows you to start work immediately on separate thread or you can queue a work to start after some duration. You can start an immediate work in a new thread using the following methods:
- void StartNow(Action doWork, Action onComplete)
- void StartNow(Action doWork, Action onComplete, Action<Exception> failed)
For example,
ParallelWork.StartNow(() =>
{
workStartedAt = DateTime.Now;
Thread.Sleep(howLongWorkTakes);
},
() =>
{
workEndedAt = DateTime.Now;
});
Or you can use the fluent way Start.Work:
Start.Work(() =>
{
workStartedAt = DateTime.Now;
Thread.Sleep(howLongWorkTakes);
})
.OnComplete(() =>
{
workCompletedAt = DateTime.Now;
})
.Run();
Besides simple execution of work on a parallel thread, you can have the parallel thread produce some object and then pass it to the success callback by using these overloads:
- void StartNow<T>(Func<T> doWork, Action<T> onComplete)
- void StartNow<T>(Func<T> doWork, Action<T> onComplete, Action<Exception> fail)
For example,
ParallelWork.StartNow<Dictionary<string, string>>(
() =>
{
test = new Dictionary<string,string>();
test.Add("test", "test");
return test;
},
(result) =>
{
Assert.True(result.ContainsKey("test"));
});
Or, the fluent way:
Start<Dictionary<string, string>>.Work(() =>
{
test = new Dictionary<string, string>();
test.Add("test", "test");
return test;
})
.OnComplete((result) =>
{
Assert.True(result.ContainsKey("test"));
})
.Run();
You can also start a work to happen after some time using these methods:
- DispatcherTimer StartAfter(Action onComplete, TimeSpan duration)
- DispatcherTimer StartAfter(Action doWork,Action onComplete,TimeSpan duration)
You can use this to perform some timed operation on the UI thread, as well as perform some operation in separate thread after some time.
ParallelWork.StartAfter(
() =>
{
workStartedAt = DateTime.Now;
Thread.Sleep(howLongWorkTakes);
},
() =>
{
workCompletedAt = DateTime.Now;
},
waitDuration);
Or, the fluent way:
Start.Work(() =>
{
workStartedAt = DateTime.Now;
Thread.Sleep(howLongWorkTakes);
})
.OnComplete(() =>
{
workCompletedAt = DateTime.Now;
})
.RunAfter(waitDuration);
There are several overloads of these functions to have a exception callback for handling exceptions or get progress update from background thread while work is in progress. For example, I use it in my PlantUmlEditor to perform background update of the application.
// Check if there's a newer version of the app
Start<bool>.Work(() =>
{
return UpdateChecker.HasUpdate(Settings.Default.DownloadUrl);
})
.OnComplete((hasUpdate) =>
{
if (hasUpdate)
{
if (MessageBox.Show(Window.GetWindow(me),
"There's a newer version available.
Do you want to download and install?",
"New version available",
MessageBoxButton.YesNo,
MessageBoxImage.Information) == MessageBoxResult.Yes)
{
ParallelWork.StartNow(() => {
var tempPath = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
Settings.Default.SetupExeName);
UpdateChecker.DownloadLatestUpdate(Settings.Default.DownloadUrl, tempPath);
}, () => { },
(x) =>
{
MessageBox.Show(Window.GetWindow(me),
"Download failed. When you run next time,
it will try downloading again.",
"Download failed",
MessageBoxButton.OK,
MessageBoxImage.Warning);
});
}
}
})
.OnException((x) =>
{
MessageBox.Show(Window.GetWindow(me),
x.Message,
"Download failed",
MessageBoxButton.OK,
MessageBoxImage.Exclamation);
});
The above code shows you how to get exception callbacks on the UI thread so that you can take necessary actions on the UI. Moreover, it shows how you can chain two parallel works to happen one after another.
Sometimes you want to do some parallel work when user does some activity on the UI. For example, you might want to save file in an editor while user is typing every 10 second. In such case, you need to make sure you don’t start another parallel work every 10 seconds while a work is already queued. You need to make sure you start a new work only when there’s no other background work going on. Here’s how you can do it:
private void ContentEditor_TextChanged(object sender, EventArgs e)
{
if (!ParallelWork.IsAnyWorkRunning())
{
ParallelWork.StartAfter(SaveAndRefreshDiagram,
TimeSpan.FromSeconds(10));
}
}
If you want to shutdown your application and want to make sure no parallel work is going on, then you can call the StopAll() method.
ParallelWork.StopAll();
If you want to wait for parallel works to complete without a timeout, then you can call the WaitForAllWork(TimeSpan timeout). It will block the current thread until the all parallel work completes or the timeout period elapses.
result = ParallelWork.WaitForAllWork(TimeSpan.FromSeconds(1));
The result is true, if all parallel work completed. If it’s false, then the timeout period elapsed and all parallel work did not complete.
For details how this library is built and how it works, please read the following codeproject article:
ParallelWork: Feature rich multithreaded fluent task execution library for WPF
http://www.codeproject.com/KB/WPF/parallelwork.aspx
If you like the article, please vote for me.