Learning IronPython - Part 6 - From Rudimentary to Functional

 | December 7, 2008 6:54 pm

In the last article of this series, my download manager based on the Background Intelligent Transfer Service (BITS) in Microsoft Windows had reached a functional (though far from aesthetically pleasing) state. Making use of a multithreaded model and interfacing with BITS through a C# wrapper, the download manager has been designed to integrate into my IronPython "learning project," a PodCast plug-in for Windows Media Player.

Figure 1 - Right. User interface of the fully functional IronPython Download Manager. Major features include the ability to run downloads in the background without affecting foreground application performance and the ability to pause and resume downloads.

With all the major pieces in place, this article will focus on some of the smaller details which transform a rudimentary download manager into a functional one. The files for this article can be found here.

Widgets, Controls and Functionality

While my rudimentary download manager was "good enough" to get the job done, it lacked both elegance and style. The screen shot at right shows the new download manager, and as is apparent, now it just lacks style.

As compared to the previous incarnation, there are a number of changes. Some of the more major ones include:

  • All active downloads have been moved from separate windows into a single list.
  • Two separate views have been added. One for active downloads, which includes information about the download source, the name of the file, and its download progress. The second includes information about the downloaded file.
  • Downloads can now be paused and re-started.

These changes combine to make the program both more functional and easier to use. They were also very easy to implement, requiring only simple changes to the existing code. To understand how, it is necessary to delve into some of the bowels of Windows Presentation Foundation (WPF) and XAML, which was used to build the user interface.

Extensible Application Markup Language (XAML)

Just like Python, XAML is a computer programming language in its own right and is incredibly powerful. This was something I underestimated when I started out on my IronPython Adventure. XAML is used to define the widgets and controls of the user interface as well as their position. However, it can also be used to define their basic behavior and properties. But that is not all! XAML also includes a vector graphics language (similar to SVG) and typesetting capabilities (similar to PDF). Thus using XAML, the user-interface, the graphics, and the text can all be programmed in the same language. In one particularly impressive example of XAML, a Microsoft Developer created a text editor with spell check and copy/paste in less than 20 lines of XAML code.

There are also more practical benefits to XAML. Perhaps one of the greatest is the ability to separate the function of a program (sometimes called the model) from the UI of the program (called the view). This makes it easier to introduce a separate designer/programmer based workflow as well as maintain the individual classes that comprise the code.

Windows, Controls and UserControls

XAML was specifically designed so that one control is capable of hosting the contents of another. Given the markup based nature of the language, it treats text, graphics, and even other UI widgets in a very similar manner. As a result, the technique used to add text to a download button is very similar to that used to add a graphic.

This made it trivially easy to change the manner in which my ProgressDialog window is treated. Consider the two code examples below. The first example shows the XAML markup used to create the ProgressDialog as a separate window. The second example shows the code used to create the ProgressDialog as an item in the main window.

Code Example 1 : ProgressDialog as a separate window.


<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Progress Dialog" Width="300" SizeToContent="Height">
    <Grid>
        <StackPanel Margin="10">
            <ProgressBar Name="DownloadProgressBar" Width="236"
            Height="21" Minimum="0" Maximum="1" Margin="10"
            HorizontalContentAlignment="Stretch" />
            <StackPanel Orientation="Horizontal" FlowDirection="RightToLeft">
                <Button x:Name="CancelButton" Width="84"
                Height="22">Cancel</Button>
            </StackPanel>
        </StackPanel>
    </Grid>
</Window>

Code Example 2 : ProgressDialog as a control on the main download window.


<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="Auto" Height="67">
    <Grid>
        <StackPanel Margin="10">
            <ProgressBar Name="DownloadProgressBar" Width="236"
            Height="21" Minimum="0" Maximum="1" Margin="10"
            HorizontalContentAlignment="Stretch" />
            <StackPanel Orientation="Horizontal" FlowDirection="RightToLeft">
                <Button x:Name="CancelButton" Width="84"
                Height="22">Cancel</Button>
            </StackPanel>
        </StackPanel>
    </Grid>
</UserControl>

As can be seen, there is only one minor modification to the XAML. The Window tag in the root node of the first example has been changed to a UserControl tag in the second. This one simple addition allows for the behavior of the control to completely change. It can now be added to other controls, such as a ListBox control, with all of its functionality intact. The Python code associated with the ProgressDialog class largely does not change. In fact, I was able to simplify it by removing the code specific to showing and closing the now unneeded window.

After converting the ProgressDialog to a UserControl, I added a ListBox control to the program's main form to show the individual download ProgressDialogs. No other changes were made to the XAML of the main form. The XAML for the revised main form is shown below.


<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"
SizeToContent="Height" Height="Auto"
MinWidth="450" MinHeight="106">
    <Grid Height="Auto" Width="Auto">
        <Separator Margin="0,0,0,69" Height="10" VerticalAlignment="Bottom" />
        <ListBox x:Name="listboxDownloads" Margin="0,0,0,85" />
        <TextBox x:Name="txtDownloadUrl" Margin="6.67,0,110,38" Height="25"
        VerticalAlignment="Bottom" />
        <Button x:Name="buttonDownloadFile" Margin="0,0,5,38"
        HorizontalAlignment="Right" Width="98.5" Height="25"
        VerticalAlignment="Bottom">Download File</Button>
        <TextBlock Margin="6.67,0,0,7" HorizontalAlignment="Left"
        Width="110.055" Height="25" VerticalAlignment="Bottom">Download Directory
        </TextBlock>
        <TextBox x:Name="txtDownloadDirectory" Margin="123,0,5,7"
        Height="25" VerticalAlignment="Bottom" />
    </Grid>
</Window>

Individual items are added to the ListBox with a single line of code in the btnDownloadFile_Click event handler of the main script (TstApp_20081203.py):


    def btnDownloadFile_Click(self, sender event):
        …
        self.listboxDownloads.Items.Add(RssDownload.Root)

Bells and Whistles

By changing the behavior of the ProgressDialog, I have taken a step toward unifying the UI of my BITS based Download Manager and that of Firefox, which is serving as my "inspiration." Both windows now prominently feature a central area which shows the number of downloads and their progress. However, there are a number of features included in the FireFox download manager which have not yet been added to my BITS version.

Figure 2. User interface for the Firefox Download Manager.

These include the ability to pause and resume downloads, as well as information about the download source and the file name. It is fortunate that these additional features are also quite easy to add to the ProgressDialog class of the download manager.

Download Details

XAML includes a simple widget, called a TextBlock, which can display either formatted or unformatted text. The information necessary to populate these widgets be passed to the ProgressDialog using the RunWorkerThread method. So, the only code modifications necessary are in the RunWorkerThread, Worker_DoWork and Worker_RunWorkerCompleted methods of the ProgressDialog class. An additional button can also be added to toggle between a paused and active state.

In the code block below is shown the new RunWorkerThread method. The original RunWorkerThread method received three inputs: the function to be run on the background thread (workHandler), a reference to the BitsJob (rssJob), and an initial argument (always set to zero) for the Worker.RunWorkerAsync() method. The new function, in contrast, receives five inputs. The three original as well as a string that points to the DownloadUrl and to the file destination (FileUrl).


    def RunWorkerThread(self, workHandler, rssJob, DownloadUrl, FileUrl, argument):

        if self.autoIncrementInterval > -1:
        self.ProgressTimer.Interval =
        … TimeSpan.FromMilliseconds(self.autoIncrementInterval)
        self.ProgressTimer.Start()

        self.RemoteFileUrl = urlparse.urlparse(DownloadUrl) # 1
        self.LocalFileUrl = FileUrl

        self.uiCulture = Globalization.CultureInfo.CurrentUICulture
        self.WorkerCallback = workHandler # 2
        self.rssJob = rssJob

        self.txtDownloadSource.Text = self.RemoteFileUrl[1] # 3
        self.txtDownloadName.Text = os.path.basename(self.RemoteFileUrl[2])
        self.txtDownloadProgress.Text = self.txtDownloadName.Text

        self.Worker.RunWorkerAsync(argument) # 4

The revised function, in addition to launching the background worker using RunWorkerAsync (#4), also adds information to the various TextBlocks. Information about the site from which it is downloading and the file name is spliced from the DownloadUrl using the Python function, urlparse (#1). Urlparse returns a 6-tuple which includes information about the type of url (file, http, ftp, etc), the site domain, and the download file name. This information is then added to the corresponding TextBlock (#3).

Pause/Resume the Download Job

The BitsJob pointer (rssJob) which is passed to the ProgressDialog contains the BitsJob.Suspend() and BitsJob.Resume() methods. These two methods contain the functionality necessary to pause and resume the download. A new button, buttonChangeStatus, which has been added to the right of the progress bar provides a simple implementation so that the user can call these events as necessary from the GUI. The event handler code is shown below.


    def buttonChangeStatus_Click(self, sender, event):

        if self.Paused == False: # 1
        self.Paused = True # 2
        self.rssJob.Suspend() # 3
        self.buttonChangeStatus.Content = self.StartDownloadIcon # 4
        self.buttonChangeStatus.ToolTip = 'Resume Download' # 5
        return

        if self.Paused == True: # 6
        self.Paused = False
        self.rssJob.Resume() # 7
        self.buttonChangeStatus.Content = self.PauseDownloadIcon
        self.buttonChangeStatus.ToolTip = 'Pause Download'
        return

Upon first running, the subroutine checks the current state of the BitsJob. If it is active (#1), then it first changes the ProgressDialog state to self.Paused = True and suspends the BitsJob (#2). It then loads a custom icon (#4) and changes the ToolTip text to indicate that clicking on the button will resume the download (#5). If the state is paused, a similar series of events occurs (#6). The job is resumed (#7) and the button text and icon are changed to indicate that clicking them will pause the download.

Wrapping Up

Once the BitsJob has finished transferring, Worker_RunWorkerCompleted is called.


    def Worker_RunWorkerCompleted(self, sender, event):

        self.ProgressTimer.Stop()
        self.DownloadProgressBar.Value = self.DownloadProgressBar.Maximum
        self.CancelButton.IsEnabled = False

        # Complete the Download Job
        self.rssJob.Complete() # 1

        # Set the Controls to Completed State
        self.CancelButton.Visibility = Visibility.Collapsed # 2
        self.buttonChangeStatus.Visibility = Visibility.Collapsed
        self.DownloadProgressBar.Visibility = Visibility.Collapsed

        if self.Cancelled == False: # 3
            TimeComplete = DateTime.Now # 4
            TimeCompleteString = TimeComplete.ToString() �
            self.txtDownloadProgress.Text = 'Download Complete'
            self.txtDateTime.Visibility = Visibility.Visible # 5
            self.txtDateTime.Text = TimeComplete.ToString()

            # Set the File Icon # 6
            FileIcon = RetrieveIcon.GetLargeIcon(self.LocalFileUrl)
            FileBmp = FileIcon.ToBitmap()
            strm = MemoryStream()
            FileBmp.Save(strm, System.Drawing.Imaging.ImageFormat.Png)

            bmpImage = BitmapImage()

            bmpImage.BeginInit()
            strm.Seek(0, SeekOrigin.Begin)
            bmpImage.StreamSource = strm
            bmpImage.EndInit()
            self.imgIcon.Source = bmpImage
            self.imgIcon.Height = 48
            self.imgIcon.Width = 48

        else:
            self.txtDownloadProgress.Text = 'Download Cancelled' # 7

First, the event handler completes the BitsJob (# 1) and hides the Progressbar, Cancel Button and Pause/Resume Button (#2) by setting their Visibility property to Collapsed. In contrast to Windows Forms, WPF makes use of three visibility option: Visibility.Visible which displays the element, Visibility.Hidden which hides the element but still reserves space in the layout, and Visibility.Collapsed which neither shows the element nor reserves space in the layout.

If the download completes without being cancelled (#3), then the current time is added to the txtDateTime TextBlock (#4, #5) and the txtDownloadProgress TextBlock is set to 'Download Complete (#4). Last, the icon for the file type is retrieved and displayed in the Picture element, imgIcon (#6). If the user presses the cancel button, the ProgressDialog.Cancelled property is set to True and the text in txtDownloadProgress is set to "Download Cancelled."

Conclusion

After adding the ability to Pause/Resume the download and additional elements which display the download details, the rudimentary download manager described in the last article becomes a useful and functional program. This completes an important component of my Podcast Plugin. More importantly, since beginning this project, I have learned a great deal about IronPython, WPF and the .Net framework. The next article in this series will offer a brief summary of some of those lessons.

Similar Posts:

4 Responses to “Learning IronPython - Part 6 - From Rudimentary to Functional”

[...] Art and Photography « Learning IronPython - Part 6 - From Rudimentary to Functional [...]

[...] finishing the download manager of my podcast aggregator (PodCatcher), I took a few days off to work on other things. After a brief [...]

[...] to comfortable ground and I will be returning to my Podcast aggregator. Previously, I created a download manager to handle the work of retrieving files; now it’s time to start tackling another major module: [...]

[...] to comfortable ground and I will be returning to my Podcast aggregator. Previously, I created a download manager to handle the work of retrieving files; now it’s time to start tackling another major module: [...]