Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / WPF

Beginners Guide to Threading in .NET: Part 5 of n

Rate me:
Please Sign up or sign in to vote.
4.88/5 (130 votes)
10 Aug 2008CPOL13 min read 286.8K   3.5K   316   56
This article will be all about how to thread different types of UIs.

Introduction

I'm afraid to say that I am just one of those people, that unless I am doing something, I am bored. So now that I finally feel I have learnt the basics of WPF, it is time to turn my attention to other matters.

I have a long list of things that demand my attention such as WCF/WF/CLR Via C# version 2 book, but I recently went for a new job (and got it, but turned it down in the end) which required me to know a lot about threading. Whilst I consider myself to be pretty good with threading, I thought, yeah I'm OK at threading, but I could always be better. So as a result of that, I have decided to dedicate myself to writing a series of articles on threading in .NET.

This series will undoubtedly owe much to an excellent Visual Basic .NET Threading Handbook that I bought that is nicely filling the MSDN gaps for me and now you.

I suspect this topic will range from simple to medium to advanced, and it will cover a lot of stuff that will be in MSDN, but I hope to give it my own spin also. So please forgive me if it does come across a bit MSDN like.

I don't know the exact schedule, but it may end up being something like:

I guess the best way is to just crack on. One note though before we start, I will be using C# and Visual Studio 2008.

What I'm going to attempt to cover in this article will be:

This article will be all about how to thread different types of UIs.

Why Thread UIs

I guess we have all seen some pretty cool UIs and some pretty bad ones in our lives. I know when I use a UI, the first thing that makes me want to un-install something is the application being unresponsive. I have a rule, if something is unresponsive, it gets removed, no questions. It's out.

So what could these software developers that made me un-install something have done differently? Well, with a little thought and a little threading knowledge, this situation could have been avoided.

I hope that at least a few of you have read the other articles in this series. If so, it should come as no surprise to you, when I say these issues of unresponsive UIs could probably have been avoided if background tasks were run in background threads, leaving the UI to be responsive to further user interactions.

It is when we allow background work to carry on and update the UI when appropriate (say when the work is done) that a responsive UI can be constructed.

This article aims to show you a few techniques to work with to create UIs that are able to deal with a single or n-many background tasks whilst maintaining a responsive UI. I will be covering techniques for WinForms and WPF mainly, but will give you some pointers for working with Silverlight.

Threading in WinForms

In this section, I am going to show you how to use threads within a WinForms environment. This will typically be done with the BackgroundWorker component that was made available within .NET 2.0. This is by far the easiest way of creating and managing background threads within a UI. Though, it should be mentioned that it does not offer as much flexibility as creating and managing your own threads. But armed with the information from the other articles in this series, you should be able to create and manage your own threads easy enough.

The reason that the BackgroundWorker is not quite as flexible as creating your own threads is that it is designed for a particular usage pattern. The BackgroundWorker provides the following:

  • The ability to run some background work
  • The ability to run some background work based on an input parameter
  • The ability to show progress
  • The ability to report completion
  • The ability to be cancelled

It is great if this fits your needs, but for more finer detail control, you should spawn and manage your own threads. For this section of the article though, I will just be using the BackgroundWorker, as it's the most common way to create and manage background tasks in UIs these days. As I say, the other articles in this series give you all the tools you need if you find your need to something more exotic.

But for now, let's march on and look at some examples using the BackgroundWorker.

Firstly, A Bad Example

In order to understand the rest of this article, it is important to see a non-working example, so as part of the code that this article provides, I have provided a BAD example WinForms app.

When you try and run this, you will see something like:

Image 1

Now, let's look at the code that created this handled Exception. Well, it is pretty simple (we will be looking at the inner workings of the BackgroundWorker later).

C#
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace Threading.UI.Winforms
{
    public partial class BackgroundWorkerBadExample : Form
    {
        public BackgroundWorkerBadExample()
        {
            InitializeComponent();
        }

        private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
        {
            try
            {
                for (int i = 0; i < (int)e.Argument; i++)
                {
                    txtResults.Text += string.Format(
                        "processing {0}\r\n", i.ToString());
                }
            }
            catch (InvalidOperationException oex)
            {
                MessageBox.Show(oex.Message);
            }

        }

        private void backgroundWorker1_RunWorkerCompleted(object sender, 
            RunWorkerCompletedEventArgs e)
        {
            MessageBox.Show("Completed background task");
        }

        private void btnGo_Click(object sender, EventArgs e)
        {
            backgroundWorker1.RunWorkerAsync(100);
        }
    }
}

The important part to note here is the backgroundWorker1_DoWork() method. Notice that we catch an InvalidOperationException. The reason for this is that in .NET Windows programming, there is one cardinal rule, and that is all controls must be accessed using the thread that created them. In this example, we are not doing anything to marshal our background thread's work to be done on the UI thread, thus we get the InvalidOperationException.

Luckily, we can fix this in a number of ways, which I will describe below. But before I show you how to fix this, let me just talk about how to work with the BackgroundWorker; it's really very very easy.

The BackgroundWorker can be wired up using a few parameter changes and a few events.

The following table outlines how to do various things with the BackgroundWorker:

TaskWhat needs setting
Report ProgressWorkerReportProgress = True, and wire up the ProgressChangedEvent
Support CancellingWorkerSupportsCancellation = True
Running without ParamNone
Running with ParamNone

You will see more by examining the attached code.

Now Some Better Options

So what I want to show you now are some options to marshal the background work to the UI thread. I have included three options:

Option 1: Use BeginInvoke (works with all versions of .NET)

C#
try
{
    for (int i = 0; i < (int)e.Argument; i++)
    {
        if (this.InvokeRequired)
        {
            this.Invoke(new EventHandler(delegate
            {
                txtResults.Text += string.Format(
                    "processing {0}\r\n", i.ToString());
            }));
        }
        else
            txtResults.Text += string.Format(
                "processing {0}\r\n", i.ToString());
    }
}
catch (InvalidOperationException oex)
{
    MessageBox.Show(oex.Message);
}

This is probably the oldest way to marshal to the UI thread, but it is also the most explicit, and really shows what is going on, and I think aids readability.

Option 2: Use SynchonizationContext (works with .NET 2.0 and above)

C#
private SynchronizationContext context;
.....
.....
//set up the SynchronizationContext
context = SynchronizationContext.Current;
if (context == null)
{
    context = new SynchronizationContext();
}
.....
.....
try
{
    for (int i = 0; i < (int)e.Argument; i++)
    {
        context.Send(new SendOrPostCallback(delegate(object state)
        {
            txtResults.Text += string.Format(
                      "processing {0}\r\n", i.ToString());

        }), null);
    }
}
catch (InvalidOperationException oex)
{
    MessageBox.Show(oex.Message);
}

This version uses a .NET 2.0 available object called the SynchronizationContext, which is an object that allows us to marshal to the UI thread using the Send() method. Internally, SynchronizationContext is really nothing more than a wrapper for some anonymous delegates. Want proof, fire up Reflector and have a look. There is also a great CP article here by Leslie Sanford, which talks in detail about the SynchronizationContext, should you wish to know more.

Option 3: Use lambdas (works with .NET 3.0 and above)

Now we could also go completely mad, and replace the use of the anonymous delegate with a lambda, which would give us something like:

C#
private SynchronizationContext context;
.....
.....
//set up the SynchronizationContext
context = SynchronizationContext.Current;
if (context == null)
{
    context = new SynchronizationContext();
}
.....
.....
try
{
    for (int i = 0; i < (int)e.Argument; i++)
    {
        context.Send(new SendOrPostCallback((s) =>
            txtResults.Text += string.Format(
                          "processing {0}\r\n", i.ToString())
        ), null);
    }
}
catch (InvalidOperationException oex)
{
    MessageBox.Show(oex.Message);
}

I guess it really depends on how happy you are with lambdas. I think they are OK for small tasks, but believe me, I have seen them used in overload, and it is not pretty. The next article will be very lambda intensive, as Task Parallel Library (TPL) seems to use loads of lambdas.

When you run either of these options in the demo code, you will get a very simple form that shows something like:

Image 2

What About Reporting Progress

You may, of course, want to report progress completed when using the BackgroundWorker; luckily, this is also a snap. This snippet of code shows the most important parts of setting up the BackgroundWorker to report progress:

C#
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace Threading.UI.Winforms
{
    public partial class BackgroundWorkerReportingProgress : Form
    {
        private int factor = 0;
        private SynchronizationContext context;

        public BackgroundWorkerReportingProgress()
        {
            InitializeComponent();

            //set up the SynchronizationContext
            context = SynchronizationContext.Current;
            if (context == null)
            {
                context = new SynchronizationContext();
            }
        }

        private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
        {

            BackgroundWorker worker = sender as BackgroundWorker;
            try
            {
                for (int i = 0; i < (int)e.Argument; i++)
                {
                    if (worker.CancellationPending)
                    {
                        e.Cancel = true;
                        return;
                    }

                    context.Send(new SendOrPostCallback( (s) =>
                        txtResults.Text += string.Format(
                        "processing {0}\r\n", i.ToString())
                    ), null);

                    //report progress
                    Thread.Sleep(1000);
                    worker.ReportProgress((100 / factor) * i + 1);

                }
            }
            catch (InvalidOperationException oex)
            {
                MessageBox.Show(oex.Message);
            }
        }


        private void btnGo_Click(object sender, EventArgs e)
        {
            factor = 100;
            backgroundWorker1.RunWorkerAsync(factor);
        }

        private void backgroundWorker1_ProgressChanged(object sender, 
            ProgressChangedEventArgs e)
        {
            progressBar1.Value = e.ProgressPercentage;
        }

        private void backgroundWorker1_RunWorkerCompleted(object sender, 
            RunWorkerCompletedEventArgs e)
        {
            MessageBox.Show("Completed background task");
        }

        private void btnCancel_Click(object sender, EventArgs e)
        {
            backgroundWorker1.CancelAsync();
        }
    }
}

It's very simple; we just wire up the BackgroundWorker.ProgressChanged event handler (backgroundWorker1_ProgressChanged in this case) and set the progress within the BackgroundWorker.DoWork event handler.

And to cancel the operation, we can simply call the BackgroundWorker.CancelAsync() method.

I have attached a small demo project, which when run will look like the following:

Image 3

Threading in WPF

WPF is new to .NET 3.0, and I don't know how many of you are using this (me, I love it). The thing to note is that it still produces code that is .NET, and although a WPF app may look different from a WinForms app, some of the underlying plumbing is the same. Threading is one area where the underlying idea is the same as WinForms.

Recall "the one cardinal rule, and that is all controls must be accessed using the thread that created them". Well, this is the same in WPF. The only difference is that we must use a WPF object known as the Dispatcher, which provides services for managing the queue of work items for a thread.

I have created a BackgroundWorker which is totally fine to use in WPF. I have seen some folk look for days for something in WPF only to realise that they can simply use some of the same ideas from WinForms.

Anyway, here is an example in WPF using the BackgroundWorker. I will not be showing the XAML as it is not important to the example.

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Windows.Threading;

namespace Threading.UI.WPF
{
    /// <summary>
    /// Interaction logic for BackGroundWorker.xaml
    /// </summary>
    public partial class BackGroundWorkerWindow : Window
    {
        private BackgroundWorker worker = new BackgroundWorker();

        public BackGroundWorkerWindow()
        {
            InitializeComponent();

            //Do some work with the Background Worker that 
            //needs to update the UI. 
            //In this example we are using the System.Action delegate.
            //Which encapsulates a a method that takes no params and 
            //returns no value.

            //Action is a new in .NET 3.5
            worker.DoWork += (s, e) =>
            {
                try
                {
                    for (int i = 0; i < (int)e.Argument; i++)
                    {
                        if (!txtResults.CheckAccess())
                        {
                            Dispatcher.Invoke(DispatcherPriority.Send,
                            (Action)delegate
                            {
                                txtResults.Text += string.Format(
                                    "processing {0}\r\n", i.ToString());
                            });
                        }
                        else
                            txtResults.Text += string.Format(
                                "processing {0}\r\n", i.ToString());
                    }
                }
                catch (InvalidOperationException oex)
                {
                    MessageBox.Show(oex.Message);
                }
            };

        }

        private void btnGo_Click(object sender, RoutedEventArgs e)
        {
            worker.RunWorkerAsync(100);
        }
    }
}

This is very similar to the WinForms example I showed earlier, but this time we must use a WPFism, which is the Dispatcher. Let's look at that in a bit more detail; the important part is this section. The things of note here are the use of CheckAccess() - this can be thought of as the equivalent to the WinForms InvokeRequired. The other thing of note is the use of the cast to a System.Action; this encapsulates a parameterless/returnless method. Other than these subtle differences, these code snippets look the same in my humble opinion.

WPF

C#
if (!txtResults.CheckAccess())
{
    Dispatcher.Invoke(DispatcherPriority.Send,
    (Action)delegate
    {
        txtResults.Text += string.Format(
            "processing {0}\r\n", i.ToString());
    });
}
else
    txtResults.Text += string.Format(
        "processing {0}\r\n", i.ToString());

If we now compare that with the first option I gave you when working with WinForms:

WinForms

C#
if (this.InvokeRequired)
{
    this.Invoke(new EventHandler(delegate
    {
        txtResults.Text += string.Format(
            "processing {0}\r\n", i.ToString());
    }));
}
else
    txtResults.Text += string.Format(
        "processing {0}\r\n", i.ToString());

I also wanted to show you how to use a ThreadPool in WPF (this would be similar in WinForms, just lose the WPF specific stuff, like Dispatcher/CheckAccess()).

ThreadPool Usage

Attached is a small example that uses a ThreadPool (which I discussed in detail in part4 of this series). I have included two options, which are as follows:

Option 1: Use Lambdas

This example uses lambdas:

C#
try
{
    for (int i = 0; i < 10; i++)
    {
        //CheckAccess(), which is rather strangely marked [Browsable(false)]
        //checks to see if an invoke is required
        //and where i respresents the State passed to the 
        //WaitCallback        
        if (!txtResults.CheckAccess())
        {
            //use a lambda, which represents the WaitCallback
            //required by the ThreadPool.QueueUserWorkItem() method
            ThreadPool.QueueUserWorkItem(waitCB =>
            {
                int state = (int)waitCB;

                Dispatcher.BeginInvoke(DispatcherPriority.Normal,
                    ((Action)delegate
                    {
                        txtResults.Text += string.Format(
                            "processing {0}\r\n", state.ToString());
                    }));
            }, i);
        }
        else
            txtResults.Text += string.Format(
                "processing {0}\r\n", i.ToString());
    }
}
catch (InvalidOperationException oex)
{
    MessageBox.Show(oex.Message);
}

The important part here is the way that the state is obtained for the WaitCallback. The State parameter for a WaitCallback is usually an object, and WaitCallbacks are normally done as shown below in option 2. By using lambdas, we are able to shorten the process some what. Where the waitCB is the actual state object, which is an Object, so must be cast to the correct type.

Option 2: Use More Explicit Syntax

C#
try
{
    for (int i = 0; i < 10; i++)
    {
        ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadProc), i);
    }
}
catch (InvalidOperationException oex)
{
    MessageBox.Show(oex.Message);
}
....
....
....
// This is called by the ThreadPool when the queued QueueUserWorkItem
// is run. This is slightly longer syntax than dealing with the Lambda/
// System.Action combo. But it is perhaps more readable and easier to
// follow/debug
private void ThreadProc(Object stateInfo)
{
    //get the state object
    int state = (int)stateInfo;

    //CheckAccess(), which is rather strangely marked [Browsable(false)]
    //checks to see if an invoke is required
    if (!txtResults.CheckAccess())
    {
        Dispatcher.BeginInvoke(DispatcherPriority.Normal,
            ((Action)delegate
            {
                txtResults.Text += string.Format(
                    "processing {0}\r\n", state.ToString());
            }));
    }
    else
        txtResults.Text += string.Format(
            "processing {0}\r\n", state.ToString());
}

Although this syntax is longer than the lambda example, it is obviously more explicit. I think it is a judgment call; if you are happy working with lambdas, go for it.

Threading in Silverlight

This section assumes you have the Silverlight 2.0 BETA installed.

"Silverlight 2 brings support for threading to the browser. You can either directly start new threads using System.Threading.Thread and System.Threading.ThreadPool, or you can use the higher-level (and recommended) System.ComponentModel.BackgroundWorker type. The latter encapsulates the concept of executing work in the background (using a thread from the thread-pool) and updating the UI based on progress and/or completion of that work, which means that you can safely update the UI from the related events.

A lesser-known type that we introduced in beta 1 is System.Windows.Threading.Dispatcher. This type lets you execute work on the UI thread - something that's useful when you directly want to update the UI from a background thread. Since Silverlight always has a single UI-thread, there is only a single dispatcher instance per Silverlight application. This instance is accessible via any DependencyObject or ScriptObject instance's Dispatcher property. Once you have a reference to a dispatcher, you can use its BeginInvoke method to dispatch your work. In Silverlight, we added an overload which takes an Action, which means you don't need to add a cast or anything to help the compiler infer what type of delegate you want to pass.

Please note that you may not be able to find the dispatcher property via intellisense. It's marked as an advanced property, so you either need to update your VS settings to display advanced members, or you just need to ignore intellisense and assume your code will in fact compile regardless of what intellisense implies. The same goes for CheckAccess, which is actually marked as a member that should never be displayed. The main reason these members aren't always visible is because they shouldn't be as common as the other members on a DependencyObject. As I mentioned before, you'll probably want to use a BackgroundWorker most of the time instead.

Gotchas

There are a couple of things to be aware of. The first is that we try to guard against cross-thread invocations when this would potentially be unsafe. For example, we don't allow you to call into the HTML DOM or a JavaScript function from a background thread. The reason for this is that both assume to be invoked on the UI thread. Breaking this assumption can lead to unexpected behavior, including browser crashes.

The other thing to be aware of is creating deadlocks. Silverlight comes with primitives such as Monitor (encapsulated via the lock construct in C#) and ManualResetEvent which make it trivial to create a deadlock. A deadlock will cause most browsers to hang completely. While technically this isn't very different from some JavaScript, it's often easier to accidentally create a deadlock than an infinite loop of code. For example, I've seen several people try to create a synchronous version of HttpWebRequest by letting the current thread wait for a ManualResetEvent to be notified by the response callback. HttpWebRequests, however, execute their callbacks on the UI thread, which means you have a deadlock right there. While ideally you avoid blocking the UI thread entirely, you should at least consider specifying timeouts when you use a synchronization object. For example, instead of the lock construct in C# (Monitor.Enter/Exit), consider using Monitor.TryEnter/Exit, passing in a reasonable timeout, and instead of using ManualResetEvent's parameterless WaitOne, consider using one of the overloads."

http://www.wilcob.com/Wilco/Silverlight/threading-in-silverlight.aspx

What this all means is that we are able to do something like this to marshal threads to the UI thread in Silverlight. In this example, I am creating a new thread and using a lambda to marshal to the correct UI thread. The second option uses anonymous delegates; both are fine.

C#
var myThread = new Thread(() =>
{
    //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    // OPTION 1 : Use lambda
    //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    txtResults.Dispatcher.BeginInvoke(() =>
        txtResults.Text = "Updated from a non-UI thread.");

    //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    // OPTION 2 : Use anonymous delegate
    //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    //txtResults.Dispatcher.BeginInvoke(delegate
    //{
    //    txtResults.Text = "Updated from a non-UI thread.";
    //});
});
myThread.Start(); 

We're Done

Well, that's all I wanted to say this time. I hope you liked the article, and that it helps you produce more responsive UIs. Could I just ask, if you liked this article, could you please vote for it? I thank you very much.

Possibly Next Time

If I have enough time/patience/energy, next time, we will be looking at the future of threading, which is the Task Parallel Library (TPL), which is a BETA at the moment. It is very complicated, but looks pretty interesting. We shall see if time is on my side.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)

- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence

Both of these at Sussex University UK.

Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2016
  • Codeproject MVP 2016
  • Microsoft C# MVP 2015
  • Codeproject MVP 2015
  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
GeneralMy vote of 2 Pin
smton2-Aug-13 3:32
smton2-Aug-13 3:32 
GeneralMy vote of 5 Pin
delibey22-Feb-12 12:18
delibey22-Feb-12 12:18 
GeneralMy vote of 5 Pin
itsik3717-May-11 20:21
itsik3717-May-11 20:21 
GeneralMy vote of 5 Pin
alain_dionne29-Mar-11 7:02
professionalalain_dionne29-Mar-11 7:02 
GeneralEr... I have a question Pin
joe5yellow25-Jan-11 21:14
joe5yellow25-Jan-11 21:14 
GeneralThanks for your great article Pin
AmirLevel27-Sep-10 2:38
AmirLevel27-Sep-10 2:38 
GeneralThxxxxx but i have ques Pin
Ashraf ELHakim28-Jun-10 4:00
Ashraf ELHakim28-Jun-10 4:00 
Generalthanks Pin
ali_reza_zareian2-Sep-09 7:06
ali_reza_zareian2-Sep-09 7:06 
GeneralRe: thanks Pin
Sacha Barber2-Sep-09 9:54
Sacha Barber2-Sep-09 9:54 
QuestionHow to access form controls while using threads Pin
Rajeshgut16-Jul-09 23:26
Rajeshgut16-Jul-09 23:26 
AnswerRe: How to access form controls while using threads Pin
R. vd Kooij11-Jan-10 22:37
R. vd Kooij11-Jan-10 22:37 
QuestionHow can I use threading in a network application [modified] Pin
samMaster26-Jun-09 2:22
samMaster26-Jun-09 2:22 
GeneralThank You Pin
George_Botros18-Jun-09 2:39
George_Botros18-Jun-09 2:39 
GeneralRe: Thank You Pin
Sacha Barber18-Jun-09 2:53
Sacha Barber18-Jun-09 2:53 
GeneralInherit the backgroundworker Pin
uffejz16-Jan-09 3:42
uffejz16-Jan-09 3:42 
GeneralRe: Inherit the backgroundworker Pin
Sacha Barber16-Jan-09 22:37
Sacha Barber16-Jan-09 22:37 
GeneralRe: Inherit the backgroundworker Pin
uffejz17-Jan-09 0:44
uffejz17-Jan-09 0:44 
GeneralRe: Inherit the backgroundworker Pin
Sacha Barber17-Jan-09 1:29
Sacha Barber17-Jan-09 1:29 
GeneralRe: Inherit the backgroundworker Pin
uffejz17-Jan-09 1:55
uffejz17-Jan-09 1:55 
GeneralRe: Inherit the backgroundworker Pin
Sacha Barber17-Jan-09 2:30
Sacha Barber17-Jan-09 2:30 
GeneralRe: Inherit the backgroundworker Pin
uffejz17-Jan-09 3:09
uffejz17-Jan-09 3:09 
GeneralRe: Inherit the backgroundworker Pin
Sacha Barber17-Jan-09 4:21
Sacha Barber17-Jan-09 4:21 
GeneralRe: Inherit the backgroundworker Pin
uffejz18-Jan-09 0:58
uffejz18-Jan-09 0:58 
GeneralRe: Inherit the backgroundworker Pin
Sacha Barber18-Jan-09 3:34
Sacha Barber18-Jan-09 3:34 
GeneralRe: Inherit the backgroundworker Pin
Paul B.24-Jan-09 9:16
Paul B.24-Jan-09 9:16 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.