Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

AsyncWorker - A Typesafe BackgroundWorker (and about Threading in General)

0.00/5 (No votes)
11 Apr 2010 1  
Generic methods enable typesafe Threading

Introduction

Threads are mostly used when a process lasts too long, as if one could wait for it. Especially the thread, in which the Gui runs must be kept free of expensive operations, because user-input can occur at each time. If the Gui-Thread is busy with a particular operation for longer than ca. 100ms, the user immediately has the bad experience of "skipping a beat", or of a "freezing application".

Meanwhile outdated (un)fortunately

2012 Microsoft introduced the Async/Await - Pattern as current "State of Art" of Threading.
With Async/Await the 5 Threading-Problems are solved still better and more convincing than with the approach published here ( - although this one is a very good approach ;-) ).
So please refer my Async/Await - Article.

The Sample-application

Input is a mouse-click on the colored surface, and the time, when it occurred. The output places the Label at the mouse-click-position, and displays the time of occurrence. A time-consuming evaluating of data is simulated by calling Thread.Sleep().

AsyncWorker2/PoorGui.gif

I have implemented this behavior in several variants to demonstrate different levels of complexity.

The Procedural Design

Threading implies a basic restructuring of the normal sequence. A normal sequence may evaluate data, then display it. The order of operations is directly written in code:

private void ucl_MouseDown(object sender, MouseEventArgs e) {
   //evaluate data
   System.Threading.Thread.Sleep(1000);
   var p = e.Location;
   var s=string.Format("Position {0} / {1}\nclicked at {2:T}", 
      p.X, p.Y, DateTime.Now);
   //display result
   label1.Text = s;
   label1.Location = p - label1.Size;
}

This is the lowest level of complexity, and it would be preferable - but it blocks the Gui-Thread.

Threads Never Return - The "Forwarding Design"

It's a basic principle that threading always works without return-values. A parallel running function can never return to the point, where it was called, since that point has already run through (even parallel). The idea of "waiting for the side-thread" is quite paradox, because the side-thread even was started to get rid of the need to wait.
A parallel running method only can forward its results to another method, never return.

The Forwarding Design

To rid of the code above from its blocking behavior, the expensive part must be isolated, so the process gets separated in three methods: BeforeBlocking, Blocking, AfterBlocking. The methods must be void, and one calls the next, because in the next step of development, they shall switch between threads, so there will be no return-option.

private void ucl_MouseDown(object sender, MouseEventArgs e) {
   EvaluateData(DateTime.Now, e.Location);
}

private void EvaluateData(DateTime t, Point p) {
   System.Threading.Thread.Sleep(1000);
   var s = string.Format("Position {0} / {1}\nclicked at {2:T}", p.X, p.Y, t);
   DisplayResult(s, p);
}

private void DisplayResult(string s, Point p) {
   label1.Text = s;
   label1.Location = p - label1.Size;
}

Forwarding also can be done more indirectly, by events (I will show that later). Actually Threading implies the change from procedural-based to event-based programming. As soon as the middle-part, Blocking (resp. EvaluateData), is transferred to a side-thread, the forwarding design appears as event-based, since AfterBlocking (resp. DisplayResult) is a typical callback-method.
AfterBlocking must be re-transferred to the Gui-thread, because each Control will throw an InvalidOperationException, if it is accessed from another thread than that one, which instantiated it.

Side-Note: Normally a forwarding design is deprecated, since it tends to develop in a spagghetti-manner ;). In a none-threading-scenario, one would call BeforeBlocking, Blocking, AfterBlocking one after another, e.g.:

//...
BeforeBlocking();
Blocking();
AfterBlocking();
//... 

But in a threading-scenario, that's no option, since Blocking() (or should I name it: NoLongerBlocking()?) does not really do the job, but only triggers the parallel execution of it. (And AfterBlocking() would be called before the Blocking-job is done, and cause errors, since it should be after Blocking()).

AsyncWorkerX

The forwarding design can be unblocked by very simple remodelling:

private void ucl_MouseDown(object sender, MouseEventArgs e) {
   //EvaluateData(DateTime.Now, e.Location);
   AsyncWorkerX.RunAsync(EvaluateData, DateTime.Now, e.Location);
}

private void EvaluateData(DateTime t, Point p) {
   System.Threading.Thread.Sleep(1000);
   var s = string.Format("Position {0} / {1}\nclicked at {2:T}", p.X, p.Y, t);
   //DisplayResult(s, p);
   AsyncWorkerX.NotifyGui(DisplayResult, s, p);
}

private void DisplayResult(string s, Point p) {
   label1.Text = s;
   label1.Location = p - label1.Size;
}

AsyncWorkerX is a static class, publishing the generic (extension-)methods RunAsync() and NotifyGui(), each five times overloaded. They accept method-delegates with up to four arguments, together with the required arguments.
The types of delegate and arguments are inferred by type-inference, which makes the use very flexible and easy, without losing type-safeness.
This is the main advantage of the introduced threading-design.

BackgroundWorker

BackgroundWorker can't reproduce the above shown behavior at all. Note: With each mouse-click, a new thread starts, and if you quickly click three times, you get three threads running, which overlap each other.
BackgroundWorker would throw its "IsBusy"- (InvalidOperation-) Exception.

struct Data2Thread {
   public DateTime Time;
   public Point Pt;
}
struct Data2Gui {
   public string Text;
   public Point Pt;
}

private void ucl_MouseDown(object sender, MouseEventArgs e) {
   if(backgroundWorker1.IsBusy) {
      MessageBox.Show("backgroundWorker1.IsBusy");
      return;
   }
   Data2Thread d = new Data2Thread() { Time = DateTime.Now, Pt = e.Location };
   backgroundWorker1.RunWorkerAsync(d);
}

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) {
   Data2Thread d = (Data2Thread)e.Argument;           
   System.Threading.Thread.Sleep(1000);
   Data2Gui d2g = new Data2Gui() {
      Pt = d.Pt,
      Text = string.Format(
         "Position {0} / {1}\nclicked at {2:T}", d.Pt.X, d.Pt.Y, d.Time)
   };
   e.Result = d2g;
}

private void backgroundWorker1_RunWorkerCompleted(
      object sender, RunWorkerCompletedEventArgs e) {
   Data2Gui d2g = (Data2Gui)e.Result;
   label1.Text = d2g.Text;
   label1.Location = d2g.Pt - label1.Size;
}

A big progress in complexity appears, since BackgroundWorker cannot transfer multiple arguments between the threads. You must create special data-structures, where you can wrap in the arguments and the results, and you must cast and unwrap them to use them in the other thread.

That's a very unintuitive approach, and the original forwarding design cannot be recognized at all, since BackgroundWorkers events dictate method-names and arguments.

A Richer GUI

Actually, I would like to leave the static methods as they are, but BackgroundWorker supports two nice features, which I also wanted to top: Cancellation and reporting progress.
So I change the sample-application in order to create a richer Gui: There is a cancel-Button and a ProgressBar, both only visible, if the parallel process runs.
Moreover the MouseDown-event gets temporarily unsubscribed, to suppress the option of requesting more than one parallel process at a time.

AsyncWorker2/RichGui.gif

Threading-principle "UpdateGui"

Immediately, the need of a method "UpdateGui" appears, to keep the commands together, which change the Gui-appearance in dependence, whether the parallel process is running or not. Otherwise, you may disable a Button in the MouseDown(), re-enable it, when the result is to display, and forget it in case of cancellation.

BackgroundWorker versus AsyncWorker

public partial class uclBgwPrgbar : UserControl {

   struct Data2Thread {
      public DateTime Time;
      public Point Pt;
   }
   struct Data2Gui {
      public string Text;
      public Point Pt;
   }

   public uclBgwPrgbar() {
      InitializeComponent(); 
      UpdateGui();
   }

   private void UpdateGui() {
      btCancel.Visible = progressBar1.Visible = backgroundWorker1.IsBusy;
      if(backgroundWorker1.IsBusy) {
         this.MouseDown -= ucl_MouseDown;
         progressBar1.Value = 0;
      } else { this.MouseDown += ucl_MouseDown; }
   }

   void ucl_MouseDown(object sender, MouseEventArgs e) {
      //pack arguments
      Data2Thread d = new Data2Thread() { Time = DateTime.Now, Pt = e.Location };
      backgroundWorker1.RunWorkerAsync(d);
      UpdateGui();
   }

   private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) {
      //unpack arguments
      Data2Thread d2t = (Data2Thread)e.Argument;           
      var p = d2t.Pt;
      var t = d2t.Time;
      //evaluate data
      for(var i = 0; i < 300; i++) {
         System.Threading.Thread.Sleep(10);
         backgroundWorker1.ReportProgress(i / 3);  //reports 300 "progresses"
         if(backgroundWorker1.CancellationPending) {
            e.Cancel = true;
            return;
         }
      }
      //pack results
      Data2Gui d2g = new Data2Gui() {
         Pt = p,
         Text = string.Format("Position {0} / {1}\nclicked at {2:T}", p.X, p.Y, t)
      };
      e.Result = d2g;
   }

   private void backgroundWorker1_RunWorkerCompleted(
         object sender, RunWorkerCompletedEventArgs e) {
      UpdateGui();
      if(e.Cancelled) {
         label1.Text = "Cancelled"; 
         return;
      } 
      Data2Gui d2g = (Data2Gui)e.Result;  //unpack results
      label1.Text = d2g.Text;
      label1.Location = d2g.Pt - label1.Size;
   }

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

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

The same thing is done using the AsyncWorker-Class:

public partial class uclAsyncWorker : UserControl {
   
   private AsyncWorker _Worker = new AsyncWorker();
   
   public uclAsyncWorker() {
      InitializeComponent();
      _Worker.IsRunningChanged += (s, e) => UpdateGui();
      UpdateGui();
   }
   
   private void UpdateGui() {
      btCancel.Visible = progressBar1.Visible = _Worker.IsRunning;
      if(_Worker.IsRunning) {
         this.MouseDown -= ucl_MouseDown;
         progressBar1.Value = 0;
      } else { this.MouseDown += ucl_MouseDown; }
   }
   
   void ucl_MouseDown(object sender, MouseEventArgs e) {
      _Worker.RunAsync(EvaluateData, DateTime.Now, e.Location);
   }
   
   private void EvaluateData(DateTime t, Point p) {
      for(var i = 0; i < 300; i++) {
         System.Threading.Thread.Sleep(10);
         //reports a progress only if the previous one was more than 0.3s ago
         //ReportProgress()==false indicates the process is cancelled.
         if(!_Worker.ReportProgress(() => progressBar1.Value = i / 3))return;
      }
      var s = string.Format("Position {0} / {1}\nclicked at {2:T}", p.X, p.Y, t);
      _Worker.NotifyGui(DisplayResult, s, p);
   }
   
   private void DisplayResult(string s, Point p) {
      label1.Text = s;
      label1.Location = p - label1.Size;
   }
   
   private void btCancel_Click(object sender, EventArgs e) {
      _Worker.Cancel();
      label1.Text = "Cancelled";
   }
}

Note the simpler design: There is only one event, IsRunningChanged(), designed to give the opportunity, to change Gui-Elements, depending on wether the AsyncWorker runs.

ReportProgress() and NotifyGui() work both in a very similar manner as the static AsyncWorkerX-methods, mentioned before. That means: ReportProgress() is as flexible as NotifyGui() - you can execute whatever you want as Progress-Report - update Labels, change Colors, log Data to a Textbox....
The difference between ReportProgress() and NotifyGui() lies in AsyncWorkers additional property int ReportInterval (means milliseconds).
If you call ReportProgress() more than once within that interval, the reporting will be omitted.
This feature is required in fast loops - otherwise you may sabotage your performance by reporting much to often (eg. the BackgroundWorker-sample-code reports a "progress" 100 times per second!).

Also note the bool return-value (of both): Returning false indicates that cancellation was requested. If so, you should immediately cancel the parallel processing and return. AsyncWorker tells only one time, that the thread should be left - next time you try to report a progress, or notify the Gui, AsyncWorker will throw an exception.

Note the shift of the concept of cancellation:

BackgroundWorker always reaches RunWorkerCompleted(), the method, where the result should be displayed. Consequently, there must be handled three very different things:

  • re-enable Gui-elements, which were disabled while the process was running
  • notify occurrence of cancellation, if necessary
  • otherwise, display the result

AsyncWorker also raises its event, IsRunningChanged(), but in case of cancellation you are enforced, really to cancel, so that the DisplayResult-part no longer is reached.

Therefore, in DisplayResult() you care about displaying results, in IsRunningChanged() care about adapting Gui-elements, and notifying cancellation, I recommend to implement there, where you request it: namely in btCancel_Click() (alternatively you can use IsRunningChanged(), and check the bool property AsyncWorker.CancelRequested).

Databinding to AsyncWorker.IsRunning

Combining AsyncWorker.IsRunning with the public event EventHandler IsRunningChanged() enables the option of Databinding. In case of a simple Gui, it can help to reduce complexity.

public partial class uclDataBoundWorker : UserControl {

   private AsyncWorker _Worker = new AsyncWorker();

   public uclDataBoundWorker() {
      InitializeComponent();
      progressBar1.DataBindings.Add("Visible", _Worker, "IsRunning");
      btCancel.DataBindings.Add("Visible", _Worker, "IsRunning");
   }

   protected override void OnMouseDown(MouseEventArgs e) {
      if(!_Worker.IsRunning) _Worker.RunAsync(EvaluateData, DateTime.Now, e.Location);
      base.OnMouseDown(e);
   }
   
   //...

You see: the method "UpdateGui" could be removed.

Dangerous BackgroundWorker-Behavior

Although the MSDN itself [ ^ ] demands unconditional to call aDelegate.EndInvoke(), whenever a side-thread was started with aDelegate.BeginInvoke(), you will find the following code, when you disassemble the BackgroundWorker-class:

public void RunWorkerAsync(object argument)
{
    if (this.isRunning)
    {
        throw new InvalidOperationException(SR.GetString
		("BackgroundWorker_WorkerAlreadyRunning"));
    }
    this.isRunning = true;
    this.cancellationPending = false;
    this.asyncOperation = AsyncOperationManager.CreateOperation(null);
    this.threadStart.BeginInvoke(argument, null, null);
}

And "this.threadStart" is simply a private class-variable of type WorkerThreadStartDelegate, which is defined as:

private delegate void WorkerThreadStartDelegate(object argument);

Since:

this.threadStart.BeginInvoke(argument, null, null);

does not save the returned IAsyncResult, nor passes an AsyncCallback to the BeginInvoke()-call, I see no option for the BackgroundWorker, to call this.threadStart.EndInvoke(IAsyncResult ar) properly.

A few days I was puzzled, and I thought: "Maybe calling aDelegate.EndInvoke() is not required, if the target-method is void, and doesn't return a value to be evaluated by .EndInvoke()". But then I recognized a serious problem with that: If no debugger is attached, exceptions will be lost, when they occur in a side-thread.

Usually, while developing a software, there is always a debugger attached, which will catch and assert those side-thread-exceptions properly. That means consequently, you will never encounter the loss of exceptions, until you have deployed your software, and an error occurs while your customer uses it. Since the exception is lost, the software will continue running in an undefined state, and the following malfunctions can cause great damage, like corrupt sensitive data. And the error-logging only will log the following exception, not the real reason.

The following code enables to try various thrown exceptions:

Imports System.Windows.Forms
Imports System.ComponentModel

Partial Public Class uclExceptions : Inherits UserControl

   Private _ThrowEx As Action(Of String) = AddressOf ThrowEx

   Private Sub ThrowEx(ByVal text As String)
      Throw New Exception(text)
   End Sub

   Private Sub Button_Click(ByVal sender As Object, ByVal e As EventArgs) _
	Handles btBackgroundworker.Click, _
         btAsyncworker.Click, btWithEndInvoke.Click, _
		btWithoutEndInvoke.Click, btSynchron.Click
      Select Case True
         Case sender Is btSynchron
            ThrowEx("thrown in main-thread")
         Case sender Is btBackgroundworker
            BackgroundWorker1.RunWorkerAsync()
         Case sender Is btAsyncworker
            _ThrowEx.RunAsync("thrown by Asyncworker")
         Case sender Is btWithEndInvoke
            _ThrowEx.BeginInvoke("thrown with EndInvoke", _
		AddressOf _ThrowEx.EndInvoke, Nothing)
         Case sender Is btWithoutEndInvoke
            _ThrowEx.BeginInvoke("thrown without EndInvoke", Nothing, Nothing)
      End Select
   End Sub

   Private Sub backgroundWorker1_DoWork(ByVal sender As Object, _
		ByVal e As DoWorkEventArgs)
      ThrowEx("thrown by  backgroundWorker1")
   End Sub

End Class
using System;
using System.Windows.Forms;
using System.ComponentModel;

namespace AsyncWorkerCs {
   public partial class uclExceptions : UserControl {

      private Action<string> _ThrowEx = delegate(string text) 
	{ throw new Exception(text); };

      public uclExceptions() { InitializeComponent(); }

      private void btBackgroundworker_Click(object sender, EventArgs e) {
         backgroundWorker1.RunWorkerAsync();
      }
      private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) {
         _ThrowEx("thrown by  backgroundWorker1");
      }
      private void btWithoutEndInvoke_Click(object sender, EventArgs e) {
         _ThrowEx.BeginInvoke("thrown without EndInvoke", null, null);
      }
      private void btWithEndInvoke_Click(object sender, EventArgs e) {
         _ThrowEx.BeginInvoke("thrown with EndInvoke", _ThrowEx.EndInvoke, null);
      }
      private void btAsyncworker_Click(object sender, EventArgs e) {
         _ThrowEx.RunAsync("thrown by Asyncworker");
      }
      private void btSynchron_Click(object sender, EventArgs e) {
         _ThrowEx("thrown in main-thread");
      }
   }
}

Logging side-thread-exceptions

Each sample-application contains a placeholder, where a global error-logging can be implemented, as a last safety-net, to get exception-infos from side-threads too, and to prevent the application from keeping on running in an undefined state:

Imports Microsoft.VisualBasic.ApplicationServices
Imports WinFormApp = System.Windows.Forms.Application

Namespace My

   Partial Friend Class MyApplication

      Private Sub App_Startup(ByVal sender As Object, ByVal e As StartupEventArgs) _
	Handles Me.Startup
         AddHandler AppDomain.CurrentDomain.UnhandledException, _
		AddressOf SideThread_Exception
         AddHandler WinFormApp.ThreadException, AddressOf MainThread_Exception
      End Sub

      Private Shared Sub MainThread_Exception(ByVal sender As Object, _
		ByVal e As System.Threading.ThreadExceptionEventArgs)
         MessageBox.Show("Log unhandled main-thread-Exception here" & _
		vbLf & e.Exception.ToString())
         WinFormApp.Exit()
      End Sub

      Private Shared Sub SideThread_Exception(ByVal sender As Object, _
		ByVal e As System.UnhandledExceptionEventArgs)
         MessageBox.Show("Log unhandled side-thread-Exception here" & _
		vbLf + e.ExceptionObject.ToString())
         WinFormApp.Exit()
      End Sub

   End Class

End Namespace
using System;
using System.Windows.Forms;

namespace AsyncWorkerCs {

   public static class Program {

      [STAThread]
      static void Main() {
         Application.EnableVisualStyles();
         AppDomain.CurrentDomain.UnhandledException += SideThread_Exception;
         Application.ThreadException += MainThread_Exception;
         Application.SetCompatibleTextRenderingDefault(false);
         Application.Run(new frmMain());
      }

      static void MainThread_Exception(object sender, 
		System.Threading.ThreadExceptionEventArgs e) {
         MessageBox.Show("Log unhandled main-thread-Exception here\n" + 
		e.Exception.ToString());
         Application.Exit();
      }

      static void SideThread_Exception(object sender, UnhandledExceptionEventArgs e) {
         MessageBox.Show("Log unhandled side-thread-Exception here\n" + 
		e.ExceptionObject.ToString());
         Application.Exit();
      }
   }
}

To play around with that, compile the application and start it directly, from outside the IDE.

Another Topic: Events from a Side-thread

Just look at the sample below: The class CountDown has two events, and the way they are raised in the Gui-Thread is IMO "spectacular unspectacular":

public sealed class CountDown : IDisposable {

   public class TickEventargs : EventArgs {
      public readonly int Counter;
      public TickEventargs(int counter) { this.Counter = counter; }
   }

   public event EventHandler<tickeventargs> Tick = delegate { };
   public event EventHandler IsRunningChanged = delegate { };

   private int _Counter;
   private System.Threading.Timer _Timer;
   private bool _IsRunning = false;

   public CountDown() {
      _Timer = new System.Threading.Timer(Timer_Tick, null, Timeout.Infinite, 1000);
   }

   private void Timer_Tick(object state) {
      Tick.RaiseGui(this, new TickEventargs(_Counter)); //raise event
      if(_Counter == 0) IsRunning = false;
      else _Counter -= 1;
   }

   public bool IsRunning {
      get { return _IsRunning; }
      set {
         if(_IsRunning == value) return;
         _IsRunning = value;
         if(_IsRunning) _Timer.Change(0, 1000);
         else _Timer.Change(Timeout.Infinite, 1000);
         IsRunningChanged.RaiseGui(this);                  //raise event
      }
   }

   public void Start(int initValue) {
      _Counter = initValue;
      IsRunning = true;
   }

   public void Dispose() { _Timer.Dispose(); }
}</tickeventargs>

(.RaiseGui is the third Extension-method, published by AsyncWorkerX, not available to VB.NET.)

In VB.NET, you can raise events to the Gui-Thread as well, but you need to design them more according to the Microsoft-design-guidelines for events, that means: publish an Protected Sub OnXYEvent(), which raises the event. Calling that Sub can easily be transferred to the Gui-Thread as any other method:

Public Class TickEventargs
   Inherits EventArgs
   Public ReadOnly Counter As Integer
   Public Sub New(ByVal counter As Integer)
      Me.Counter = counter
   End Sub
End Class

Public Event Tick As EventHandler(Of TickEventargs)
Protected Sub OnTick(ByVal e As TickEventargs)
   RaiseEvent Tick(Me, e)
End Sub

Private Sub Timer_Tick(ByVal state As Object)
   'raise Tick-event in Gui-thread
   AsyncWorkerX.NotifyGui(AddressOf OnTick, New TickEventargs(_Counter))
   If _Counter = 0 Then
      IsRunning = False
   Else
      _Counter -= 1
   End If
End Sub

'...

It's not as elegant as in C#, but elegant enough, I think.

How AsyncWorker Works

NotifyGui() and RunAsync() work very differently, although they are designed with the same signature.

RunAsync()

RunAsync() raises IsRunningChanged(), before it executes Delegate.BeginInvoke(). Delegate.BeginInvoke() takes a callback as parameter, which is called from the side-thread, before it runs out. Within that callback Delegate.EndInvoke() is executed, which is important for several reasons - please refer to Asynchronous Method Invocation [ . ].
Finally raise IsRunningChanged(), but transferred to the Gui-thread, since the callback still runs in the side-thread.
I'll show RunAsync() in two overloads, one for methods without arguments, the other for methods with two arguments:

public void RunAsync(ActionasyncAction) {
   asyncAction.BeginInvoke( GetCallback( asyncAction.EndInvoke), null);
}
public void RunAsync<T1, T2>(Action<T1, T2> asyncAction, T1 arg1, T2 arg2) {
   asyncAction.BeginInvoke(arg1, arg2, GetCallback( asyncAction.EndInvoke), null);
}

GetCallback() does some settings, and returns an AsyncCallback-Delegate, which combines the .EndInvoke-call with ToggleIsRunning(), to set IsRunning back to false:

private AsyncCallback GetCallback(AsyncCallback endInvoke) {
   if(IsRunning) throw this.BugException("I'm already running");
   _CancelRequested = _CancelNotified = false;
   ToggleIsRunning();
   _ReportNext = Environment.TickCount;
   return (AsyncCallback)Delegate.Combine(endInvoke, _ToggleIsRunningCallback);   
}

NotifyGui()

NotifyGui(), on the other hand, simply forwards syncAction and its arguments to TryInvokeGui():

/// <summary>
/// if cancelled, returns false. if called once more, throws BugException: 
/// "after cancellation 
/// you should notify Gui no longer.". Use the return-value to avoid that.
/// </summary>
public bool NotifyGui(Action syncAction) { return TryInvokeGui(syncAction, false); }

public bool NotifyGui<T1, T2>(Action<T1, T2> syncAction, T1 arg1, T2 arg2) {
   return TryInvokeGui(syncAction, false, arg1, arg2);
}

TryInvokeGui() covers reporting progress as well as notifying the GUI straight forward. An exception will remind you, if you call the Gui after cancellation, otherwise it forwards to InvokeGui().
InvokeGui() cancels, if Application.OpenForms[0] is not available, or if action.Target is a Control, but meanwhile disposed. See TryInvokeGui() and InvokeGui() together:

private bool TryInvokeGui(Delegate action, bool reportProgress, 
	params object[] args) {
   if(_CancelRequested) {
      if(_CancelNotified) throw this.BugException(
         "after cancellation you should notify Gui no longer.");
      _CancelNotified = true;
      return false;
   }
   if(reportProgress) {
      var ticks = Environment.TickCount;
      if(ticks < _ReportNext) return true;
      InvokeGui(action, args);
      //set the next reportingtime, aligned to  ReportInterval 
      _ReportNext = ticks + ReportInterval - (ticks - _ReportNext) % ReportInterval;
   } else InvokeGui(action, args);
   return true;
}
private void InvokeGui(Delegate action, params object[] args) {
   var  target = action.Target as Control; 
   if(Application.OpenForms.Count == 0 || 
	(target != null && target.IsDisposed)) Cancel();
   else Application.OpenForms[0].BeginInvoke(action, args);
}

The trick in general is to use Application.OpenForms[0] as the invoking Control, which globally enables the option to transfer method-calls to the Gui-thread.
This includes independent classes, where no Control is available (like the CountDown-class, shown before).

History

  • 04-2010: Add section about dangerous BackgroundWorker-behavior changes in code of AsyncWorker and the demo-apps

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here