Background Worker Threads

IronPython & Windows Forms, Part IX

Windows Forms

Note

This is part of a series of tutorials on using IronPython with Windows Forms.

 

 

The BackgroundWorker Class

Recently we tried to add an 'activity indicator' (throbber) to a long running process in our Windows Forms application. Unfortunately we ran into difficulties.

The main problem was that we were using the wrong event to detect when a control lost the focus. You might think that LostFocus was the obvious choice [1]. In fact this is a low level event only used when updating UICues. The correct event to use is Leave.

LostFocus is raised when the user clicks on the exit button, but Leave isn't. We spent part of today fixing all the places we used GotFocus and LostFocus and replacing them with Enter and Leave. Luckily it wasn't too many. Smile

Using the BackgroundWorker, suggested by Andriy in a comment, the code is quite nice [2].

You provide the BackgroundWorker with your long running process as an event handler. It has a method to detect if one is already running, and raises an event when it has finished.

A common idiom in our code is to have our own event hooks. Rather than tightly coupling our objects together, they can raise events.

An approximation of the code structure we used is shown below. This is also a good [3] example of how to use the BackgroundWorker.

This code shows an event hook class [4], which provide the LongRunningStart and LongRunningEnd events which enable and disable the activity indicator: the throbber.

This is automatically triggered when the textbox Leave event is raised. (But I've omitted all the boiler-plate in setting up the form and textbox of course.)

import clr
clr.AddReference('System')
clr.AddReference('System.Windows.Forms')

from System.ComponentModel import BackgroundWorker
from System.Windows.Forms import Form, TextBox

class EventHook(object):
    def __init__(self):
        self.__handlers = []

    def __iadd__(self, handler):
        self.__handlers.append(handler)
        return self

    def __isub__(self, handler):
        self.__handlers.remove(handler)
        return self

    def fire(self, *args, **keywargs):
        for handler in self.__handlers:
            handler(*args, **keywargs)


class LongRunning(object):
    def __init__(self):
        self._worker = BackgroundWorker()
        #
        self.LongRunningStart = EventHook()
        self.LongRunningEnd = EventHook()
        self._worker.DoWork += lambda _, __: self.__longRunningProcess()
        self._worker.RunWorkerCompleted += lambda _, __: self.LongRunningEnd.fire()

    def LongRunningProcess(self):
        # This can be called directly if you need a
        # synchronous call as well.
        # The long running process will block the GUI from
        # updating though.
        self.LongRunningStart.fire()
        self.__longRunningProcess()
        self.LongRunningEnd.fire()

    def LongRunningProcessAsync(self):
        # Just drop out if one is already running
        if not self._worker.IsBusy:
            # This starts __longRunningProcess on a background thread
            self.LongRunningStart.fire()
            self._worker.RunWorkerAsync()

    def __longRunningProcess(self):
        # Do *lots* of stuff :-)


class MainForm(Form):

    def __init__(self):
        self.longRunning = LongRunning()
        self.longRunning.LongRunningStart += self.enableThrobber
        self.longRunning.LongRunningEnd += self.disableThrobber

        self.textBox = TextBox
        self.Controls.Add(self.textBox)
        self.textBox.Leave += lambda _, __: self.longRunning.LongRunningProcessAsync()

    def enableThrobber(self):
        # do something

    def disableThrobber(self):
        # do something

To check if the BackgroundWorker is in the middle of running, we use the IsBusy Property.

To tell it what to do when started, we add our long running process to the DoWork Event, this is kicked off on a separate thread: so be careful !

This is actually launched by the RunWorkerAsync Method.

When our process (bad choice of word, hey) has finished, the RunWorkerCompleted Event is raised.

Notice that if we wrap our .NET event handlers in a lambda we don't need the sender and event arguments.

lambda _, __: self.LongRunningEnd.fire()

You can use the event argument sent to DoWork handlers to pass arguments when RunWorkerAsync is called, but this isn't shown.

Note

Thanks to Davy Mitchell for pointing out a couple of errors in the code example which have now been fixed.

[1]We certainly did...
[2]Well, at least in my opinion.
[3]Well, I hope so...
[4]Written from memory, not copied directly from Resolver. Let's hope my boss believes me. You register and unregister event handlers using the normal .NET idiom, minus the sender and event arguments.

For buying techie books, science fiction, computer hardware or the latest gadgets: visit The Voidspace Amazon Store.

Hosted by Webfaction

Return to Top

Page rendered with rest2web the Site Builder

Last edited Fri Nov 27 18:32:35 2009.

Counter...