Learning IronPython – Part 5 – A Rudimentary Download Manager
Rob Oakes | December 1, 2008 10:42 pmNow that the background and semi-theoretical parts about BITS, multithreading, and downloads have been dealt with; it's time to start wrapping up ends and build a useable program. In this post, I will be looking at how to connect my multithreaded BackgroundWorker Object and SharpBITS to a functioning user interface that notifies the user of the download's progress. For those wishing to follow along, the source files and assemblies for this article can be found here.
The Download Manager
In the first iteration of the download manager, the main form was nothing more than a glorified "Hello World." While its only button practically begged to be pushed, it didn't do anything more complicated than launch a BackgroundWorker and the CountTo100 function.
A generic download manager, however, needs to be slightly more functional, which requires more than a single button. The user interface needs to accept input from the user, including the source of the download as well as a destination folder. As a result, the UI (by necessity) requires more controls. The figure below shows the modified user interface and the code block below provides source XAML of the Download Manager main form. It now includes two text boxes, one for the download URL and the other for the destination directory. The button has been resized and the text changed to better describe its function.

Figure 1. Download manager user interface. The top text box is used for the download url and the bottom text box is used to specify the download directory.
As can be seen in the source XAML below, I have also added a number of StackPanel objects so that the new TextBoxes and Button stay organized.
<Window xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' xmlns:x:'http://schemas.microsoft.com/winfx/2006/xaml' Title="IronPython - Download Manager" Width="450.864" SizeToContent="Height" Height="106" MaxWidth="451" MinWidth="450.864" MinHeight="106"> <StackPanel Height="Auto" Width="Auto" Orientation="Vertical"> <StackPanel Height="Auto" Width="Auto" Orientation="Horizontal"> <TextBox x:Name="txtDownloadUrl" Height="22.725" Width="323.604" /> <Button x:Name="buttonDownloadFile" Height="22.725" Width="103.626"> Download File</Button> </StackPanel> <Separator Height="12.726" Width="Auto" /> <StackPanel Orientation="Horizontal"> <TextBlock Height="20.907" Width="112.716" > Download Directory</TextBlock> <TextBox x:Name="txtDownloadDirectory" Height="22.725" Width="314.514" /> </StackPanel> </StackPanel> </Window>
In the __init__ function of the main script (TestApp_20081126), the TextBoxes (#2) are created in the same way as the Button (#1).
class TestApp(object):
def __init__(self):
xamlStream = File.OpenRead('DownloadManager.xaml')
self.Root = XamlReader.Load(xamlStream)
self.buttonDownloadFile =
... self.Root.FindName('buttonDownloadFile') # 1
self.txtDownloadUrl =�
... self.Root.FindName('txtDownloadUrl') # 2
self.txtDownloadDirectory =
... self.Root.FindName('txtDownloadDirectory')
…
The default download directory can then be set by passing a string to the TextBox.Text property of the destination textbox at runtime. In this example, I have hardcoded the download default directory to be the Users Downloads Directory.
self.txtDownloadDirectory.Text = ... r'C:\Users\Username\Downloads'
When there is a valid url is in the source text box, pressing the "Download File" button launches a second dialog which indicates the progress of the background operation. The ProgressBar.Maximum is set to the size of the file, and the location of the ProgressBar represents the number of bytes transferred . As in Part 3 of this series, both the download job and the updates to the user interface proceed via a separate thread (managed with the BackgroundWorker Class). In this implementation, it is possible to interrupt the background thread and cancel the download by pressing the "Cancel" button.

Figure 2. Dialog Box which shows the download progress by using a BackgroundWorker.
Managing Downloads
As described in Part 4, the BITS service is controlled through the BitsManager class created when the main script (TestApp_20081126.py) is loaded (following the import statements).
import clr
import os
import system
…
clr.AddReferenceToFile('SharpBITS.Base.dll')
import SharpBits.Base
…
app = Application()
DownloadManager = SharpBits.Base.BitsManager()
def CheckDirectory(FolderPath): FolderCheck = FolderPath � if os.path.exists(FolderCheck): # 1 print `Folder Path Exists' else: os.mkdir(FolderCheck) # 2
The code is straightforward. It makes use of the os.path.exists method to determine if the FolderPath is valid (#1). If not, it creates the folder (#2), using os.mkdir(DirectoryName).
def DownloadFile(DownloadUrl, DestUrl):
# Check if the download directory exists
CheckDir = os.path.dirname(DestUrl)
CheckDirectory(CheckDir)
# Create the download job
rssJob = DownloadManager.CreateJob("rssEnclosureDownload",
... SharpBits.Base.JobType.Download) # 1
rssJob.AddFile(DownloadUrl, DestUrl) # 2
rssJob.Priority = SharpBits.Base.JobPriority.ForeGround # 3
return rssJob # 4
def UpdateFileDialog(sender, event, rssJob): worker = sender # 1 # Update the BackgroundWorker/UI of Download Progress while rssJob.State != SharpBits.Base.JobState.Transferred: # 2 worker.ReportProgress(rssJob.Progress.BytesTransferred) Thread.Sleep(150) if worker.CancellationPending: # 3 rssJob.Cancel() # 4 break # 5
UpdateFileDialog first creates an instance of the worker by referencing the function sender (#1). It then queries the state of the rssJob (#2). If the job reports its status as anything other than "Transferred," the worker then fires the BackgroundWorker.ReportsProgress and provides the total bytes transferred. If the BackgroundWorker.CancellationPending is true, then the function cancels the BitsJob and exits.
Initiating the Download
As in the case of the multithreaded hello world example, the download is started by pressing the button on the main form.
def btnDownloadFile_Click(self, sender, event): DownloadUrl = self.txtDownloadUrl.Text # 1 # Create the File and Destination Url FileName = os.path.basename(DownloadUrl) FileUrl = self.txtDownloadDirectory.Text + '\\' + FileName # Return the txtDownloadUrl TextBox to Blank self.txtDownloadUrl.Text = None # Create the Background Thread and Download Job ProgressWindow = ProgressDialog(app.Dispatcher) # 2 rssJob = DownloadFile(DownloadUrl, FileUrl) # 3 # Start the Download Job rssJob.Resume() # 4 # Begin Updating the ProgressDialog ProgressWindow.RunWorkerThread(updateFileDialog,� ... rssJob, 0) # 5
First, the download source Url (DownloadUrl) is taken from the program's main form (#1) and the file destination url is created. If no valid url is present, the program will quit and return an error.
Next, the DownloadUrl TextBox is set to a null value. The BackgroundWorker and ProgressDialog are then created (#2). The BitsJob is created and started (#4). Finally, the ProgressWindow is opened and the BackgroundWorker is started by using the custom ProgressDialog.RunWorkerThread, which was discussed more in depth in Part 3.
The ProgressDialog Class
As you might have already guessed, BITS is already multithreaded and handles thread to thread communication via the BitsJob and BitsManager interfaces. As a result, the BackgroundWorker launched with the ProgressDialog.RunWorkerThread does not directly handle the work of downloading the file. Instead, its primary function is to update the user interface. As a result, it works in very similar fashion to the CountTo100 example previously described via the BackgroundWorker.DoWork, ProgressChanged and RunWorkerCompleted events.
The code block below shows the most important changes to the ProgressDialog class. These include a pointer to the BitsJob and a function which sets the ProgressBar.Maximum value. The BitsJob.Complete() function has also been added to the Worker_RunWorkerCompleted() event handler.
class ProgressDialog(object): def __init__(self, ThreadDispatcher): … self.rssJob = None # 1 … def RunWorkerThread(self, workHandler, rssJob, argument): … self.WorkerCallback = workHandler # 2 self.rssJob = rssJob # 3 self.Worker.RunWorkerAsync(argument) … def Worker_DoWork(self, sender, event): … try: … self.WorkerCallback(sender, event, self.rssJob) #4 self.SetMaxValue() except: … def Worker_ProgressChanged(self, sender, event): DownloadProgress = event.ProgressPercentage self.DownloadProgressBar.Value = DownloadProgress if self.DownloadProgressBar.Maximum == 1: # 5 self.SetMaxValue() if DownloadProgress > self.DownloadProgressBar.Maximum: self.SetMaxValue() … def Worker_RunWorkerCompleted(self, sender, event): … self.rssJob.Complete() # 6
First, an empty reference called self.rssJob is created (#1). It will receive a pointer to the current BitsJob when the RunWorkerThread method is called (#3). In the Worker_ProgressChanged event, the thread checks the ProgressBar.Maximum value to ensure that it is correct (#5). This is done via the function, SetMaxValue(). After the download is reported complete, the background thread then runs the BitsJob.Complete() method to release the files to disk.
Conclusion
IronPython is an excellent language in which to glue the functions of multiple components together in an elegant manner. In this post, I looked at how to construct a GUI for the Windows BITS service. I was able to easily modify the BackgroundWorker class and create a custom dialog. Then, using the Worker_ProgressChanged event, it is possible to update the GUI without causing the entire program to lock while the download proceeds.
In the next article in this series, I will begin wrapping up work on the Download Manager. First, I will create a single window and list current downloads in a single list. After that, I will create a custom class that responds to the download state and allows for individual jobs to be paused and resumed.
Similar Posts:
- Learning IronPython – Part 4 – BITS and Pieces
- Learning IronPython - Part 6 - From Rudimentary to Functional
- Installation of PyQt on Windows
- Installation of PyQt on Mac OS X
- Learning IronPython – Part 3 – A Beautiful Start
Categories: IronPython, Programming
Comments Off























![Blackwater: The Rise of the World’s Most Powerful Mercenary Army [Revised and Updated]](http://ecx.images-amazon.com/images/I/41ZopVuqGsL._SL160_.jpg)
No Responses to “Learning IronPython – Part 5 – A Rudimentary Download Manager”