After a great deal of chatter, we finally arrive at the meat of this series. We can start talking about code and some specific things that I have started to pick up. For those coming late to the party though, it might be good to briefly summarize what I’ve already blogged about. In Part 1 of this series, I talked about my recent decision to begin learning IronPython and some benefits over other options. In Part 2, I briefly introduced the project which I will be working on. This article (as well as the next two in this series) will focus on my efforts to develop a Podcast download manager using Windows Presentation Foundation (WPF) and IronPython. For those following along, the source files for the article can be downloaded here.
Some Goals and the End Product
While I describe my overall goals in Part 2, I think it is good to begin with the end in mind. So, I have decided to start this post with a few design considerations.
It will be important that my podcast client (PodCatcher) be able to download multiple files at once. While the actual download will happen in the background and will be controlled by the Background Intelligent Transfer Service (BITS) that is built into Windows, I still want to be able to interact with the download streams. I should be able to pause, resume and cancel downloads at will. I also want to know how much of a given transfer has occurred. These design goals insert a level of complexity into my program from the outset. Namely, it is necessary to use a multithreaded design.
Multithreaded applications are able to take advantage of multiple cores in newer hardware. A given data process can be split up and run in multiple threads at the same time. This type of parallel processing is able to crunch more data in a shorter time frame. Alternatively, it can be used to do multiple jobs without tying up the user interface (which is what I intend). The operating system handles the dirty job of deciding who gets time on the processor while avoiding fights and other issues.
Though multithreaded programs offer many benefits, there are also some difficulties. For example, when multiple jobs are running at the same time, it can become very difficult to communicate from one thread to another. This poses a particular problem in the case of updating the Progress Bar. WPF uses one thread for the main GUI. For various reasons, only jobs started on the GUI thread are capable of updating the user interface. Taken at face value, it appears impossible to update the status of a Progress Bar (on the GUI thread) from the Download Thread. However, we don’t need to take the scenario at face value. Much of the difficulty is circumvented by a particular class in the WPF Application framework called the Dispatcher. In this example, I will be using a class type that inherits from (and is able to communicate with the dispatcher). This class is called the BackGroundWorker.
This article will focus on the creation of a simple dialog window with a WPF ProgressBar and cancel button. For the first step, I will not be using information from BITS to drive the progress bar. Instead, I will use a simple function called CountTo100 (whose job is self explanatory) to demonstrate the BackGroundWorker and communication with the GUI thread.
Step 1: Creating Windows While Avoiding Walls
Before getting to the heart, I think it might be helpful to start with a simple HelloWorld. After all, what programming experience is complete without HelloWorld? In this case, I can demonstrate how a simple piece of XAML code (the language behind WPF) can be loaded and turned into a Window. As I am far from creative, this example has been largely stolen from IronPython in Action. A similar example can also be found in the IronPython Cookbook. Briefly, we will be using the XamlReader to load a simple window with a button and Window Title. After loading the window, the button.Click event is connected to a simple function. We will use this simple function to launch multiple instances of our ProgressDialog, each on a separate thread (which will be discussed below). Here is the relevant code:
import clr # 1
from System.IO import File
from System.Windows.Markup import XamlReader # 2
from System.Windows import Application, Windowclass TestApp(object):
xamlStream = File.OpenRead(‘HelloWorld.xaml’) # 3
self.Root = XamlReader.Load(xamlStream)
self.buttonHelloWorld = self.Root.FindName(‘buttonHelloWorld’)
self.buttonHelloWorld.Click = += self.btnHello_Click # 4
def btnHello_Click(self, sender, event): # 5
ProgDialog = ProgressDialog(app.Dispatcher)
mainWin = TestApp()
- Comment 1. This is the line of code which imports the Common Language Runtime.
- Comment 2. The XamlRader is imported from System.Windows.Markup.
- Comment 3. The Xaml code is read from a file called ‘HelloWorld.xaml’ and then loaded as a Window element in the TestApp.Root.
- Comment 4. The buttonHelloWorld.Click Event is connected to the btnHello_Click Function. This function will be used to launch the ProgressDialog.
- Comment 5. The btnHello_Click function.
The code can be run as a script and is started with the command:
A similar method is used to create the dialog box that contains the progress bar. The XAML for the form is kept in a file and then read in when it is necessary to make a new instance. Once the window is created and the various events connected, he progress bar is managed programmatically.
Step 2: The BackGroundWorker
Launching the BackgroundWorker
When the “Push Me!” button of the main window is pressed, it launches a smaller dialog box. The smaller dialog box is an instance of a special class called ProgressDialog. The ProgressDialog class contains a ProgressBar and a CancelButton, it also creates an instance of a BackGroundWorker. A reference to the application Dispatcher is included (#2) so that GUI events can be used outside of the Worker_ProgressChanged event handler. This is only done in case of an error or other issue.
def btnHello_Click(self, sender, event):
ProgDialog = ProgressDialog(app.Dispatcher) # 1
ProgDialog.RunWorkerThread(0, CountTo100) # 2
The BackGroundWorker thread is launched with the RunWorkerThread method of the dialog box. This is a special implementation that has been added to allow the BackGroundWorker to be started easily and for methods from other classes to be passed to the ProgressDialog. You might notice two arguments in being passed to the ProgDialog.RunWorkerThread in this example. The first is the number from the method will begin counting, the second is a simple function called CountTo100 (that has been defined in the TestApp_20081106.py script). In the implementation here, ProgressDialog can be used to launch any function or process via the BackGroundWorker. Consider the function, CountTo100 for a moment:
def CountTo100(sender, event):
worker = sender # 1
startValue = event.Argument for i in range(100):
Thread.Sleep(30) # 2
worker.ReportProgress(i) # 3
if worker.CancellationPending: # 4
While this method is very simple, it contains several points of interest. First, it creates a reference to the worker object in the first line of code (#1). It then briefly pauses the thread on which it runs by invoking Thread.Sleep method (#2). Last, it communicates with the BackGroundWorker (and thus the GUI) via the BackgroundWorker.ReportProgress event (#3). Any sort of quantifiable process can be run and reported in this manner. The worker.CancellationPending event allows for the for loop to be broken. This occurs if the user clicks the Cancel button while the thread is running.
How Does the BackGroundWorker Function?
The BackGroundWorker Class creates a separate dedicated thread with specific events which allow for the progress of the operation to be communicated back to the main GUI thread. The most important events include the DoWork event, the ProgressChanged event (touched on briefly above) and the RunWorkerCompleted event.
The file ProgressDialog.py contains the. As in the case of TestApp, the window is initialized by loading a xaml string and various properties are set. After this, the background worker is created, as shown in the code block below.
from System.Threading import *
from System.Windows.Threading import *
from System.Windows.Controls import ProgressBar, Buttonclass ProgressDialog(object):
def __init__(self, ThreadDispatcher):
self.Worker = BackgroundWorker() # 1
self.Worker.WorkerReportsProgress = True # 2
self.Worker.WorkerSupportsCancellation = True # 3
self.Worker.DoWork += self.Worker_DoWork
self.Worker.ProgressChanged += self.Worker_ProgressChanged
self.Worker.RunWorkerCompleted += self.Worker_RunWorkerCompleted
… def RunWorkerThread(self, argument, workHandler): # 4
self.uiCulture = Globalization.CultureInfo.CurrentUICulture
self.WorkerCallback = workHandler # 5
self.Worker.RunWorkerAsync(argument) # 6
self.Root.Show() # 7
Comment 1 shows where BackGroundWorker is created and its subsequent configuration so the ProgressChanged (#2) and WorkerSupportsCancellation (#3)
events are both enabled. The RunWorkerThread method is also shown (#4). This allows for an argument and method from another class to be passed to the BackGroundWorker. The custom method is stored as self.WorkerCallback (#5) and the BackGroundWorker is started (#6). Lastly, the ProgressDialog window is shown by the Window.Show() method (#7).
After the BackGroundWorker is started by using RunWorkerAsync, the Worker_DoWork event handler is fired.
def Worker_DoWork(self, sender, event):
Thread.CurrentThread.CurrentUICulture = self.uiCulture
self.WorkerCallback(sender, event) # 1
self.CancelButton.SetValue(Button.IsEnabledPropery, False), None)
The BackGroundWorker attempts to fire the stored method in self.WorkerCallback. If an error occurrs, a smaller Dispatcher command is invoked and the Worker_RunWorkerCompleted event handler is then fired which will close the window.
Updating the Progress Bar
The progress bar (and other updates to the GUI) are done via the Worker_ProgressChanged event handler:
def Worker_ProgressChanged(self, sender, event):
Progress = event.ProgressPercentage * 0.01 # 1
self.DownloadProgressBar.Value = Progres # 2
In this simple example, the variable Progress (#1) corresponds to the to the counter (i) in the event loop of CountTo100 (see above). It is converted from an integer to a ratio and then it is set as the value in the ProgressBar. While this example is simple, it shows the power and flexibility of the BackgroundWorker.
In this article, three different IronPython techniques are discussed. The first gave an example of loading Xaml from a file and creating a window with the XamlReader object. The second showed an example of how a BackGroundWorker may be used to run a secondary process on a background thread. The third showed how information from the background thread could be communicated to the main GUI thread via the BackGroundWorker.ProgressChanged event. In short, the BackgroundWorker class offers an elegant solution to running an operation on a thread separate from that of the GUI.
In the next article in this series, we will look at how SharpBITS can be used to download a file with the BITS service.
Acknowledgements, References and Further Reading
While putting together this entry, I am indebted to the many C# and IronPython tutorials which describe threading in the .Net framework and how to implement them. The multithreaded code for this example was translated from this C# tutorial, by Phillipp Sumi.
For additional reading about threading and background worker objects, I suggest that you check out the IronPython Cookbook. Here, is a great introduction to threading. The background worker object is discussed in greater detail here.