Exploring the NetCF Web Crawler sample (Visual Studio .NET 2003) - Part II: Updating the UI

A couple of weeks ago, I discussed link tracking in the NetCF Web Crawler sample from Visual Studio .NET 2003.  Today, I would like to use the Web Crawler sample to discuss updating an application's user interface from a worker thread.

When updating user interface elements, it is very important to perform the tasks while running in the context of the thread that created the controls.  One side effect of attempting to modify controls from a different thread are application hangs.  This is most often seen when attempting to update a control (ex: modify it's Text property) in a asynchronous method callback.  While callback methods typically live in the class that owns the controls, they are most often called by a thread that was created by another piece of code (the NetCF HTTP client class library, for example).  When UI updates are attempted from this callback, application hangs may occur.

The way to avoid these side effects is to marshal the callback calls to your user interface's thread context.  This can be achived using the Invoke method of the System.Windows.Forms.Control class.  The Web Crawler example uses this technique to provide runtime status to the user.

NOTE: The code in this post has been edited for clarity and to reduce size.  For full sample code, please consult the files that were installed with Visual Studio .NET 2003 (\Program Files\Microsoft Visual Studio .NET 2003\CompactFrameworkSDK\v1.0.5000\Windows CE\Samples\VC#\Pocket PC\WebCrawler).

Events and event handlers
The Web Crawler's Crawler class supports a number of events for which that clients can register handlers.  To keep my example as clear as possible, I am going to focus on a very simple case -- the PageFoundEvent. 

The WebCrawler.Crawler object exposes a public EventHandler field for the PageHandledEvent as shown below:

public EventHandler PageFoundEvent;

The client (WebCrawler.MainForm) registers it's event hander:

// from startButton_Click
this.crawler = new Crawler(startingPage,
                           noProxy);
this.crawler.PageFoundEvent += new EventHandler(this.HandlePageFoundEvent);

The event handler implementation also lives in the client (WebCrawler.MainForm):

private void HandlePageFoundEvent(object sender, EventArgs e)
{
    if(CustomInvokeRequired())
    {
        this.linkCount.Invoke(new EventHandler(this.HandlePageFoundEvent));
        return;
    }
           
    try
    {
        this.linkCount.Text = string.Format("{0}", Int32.Parse(this.linkCount.Text)+1);
    }
    catch(FormatException)
    { }
}

The HandlePageFoundEvent method first checks to see if the call was made in the context of the user interface thread by calling CustomInvokeRequired() (more on this soon).  If not (returns true), it calls the linkCount object's Invoke method to marshal the call to the user interface thread.  The HandlePageFoundEvent method is then re-called on the user interface thread.  CustomInvokeRequired() is called (returns false) and the method updates the linkCount control's Text property.

Determining if thread marshaling is required
I mentioned earlier that I would go into more detail on the web crawler's CustomInvokeRequired method (WebCrawler.MainForm).  At startup, the web crawler's MainForm object stores it's thread in a private member variable:

private readonly System.Threading.Thread FormThread = System.Threading.Thread.CurrentThread;

This member is marked as readonly so that it cannot be modified.

When called, the CustomInvokeRequired method checks to see if the current thread is the same as the FormThread (the thread that owns the user interface):

if(this.FormThread.Equals(System.Threading.Thread.CurrentThread))

If the current thread is the user interface thread (this.FormThread.Equals(Thread.CurrentThread)), there is no marshaling required and CustomInvokeRequired returns false.  Otherwise, true is returned indicating that thread marshaling is needed.

Using this technique, the web crawler's worker thread can keep the user informed, and not cause any undesirable side effects.

Until next time,
-- DK

Disclaimer(s):
This posting is provided "AS IS" with no warranties, and confers no rights.