Many times developing windows applications, you’ll need to perform some operations in background.
The problem you’ll face sooner or later is that those operations need to inform User Interface (UI) about their progress and completion.
UI doesn’t like to be informed about anything from a different thread: you’ll get nasty “Cross-thread operation not valid” exception from WinForms controls, if you try.
Let’s take a look at the sample Presenter code:
public void Start()
{
TaskStatus taskStatus = this._backupService.CreateTask();
taskStatus.Completed += BackupFinished;
this._backupService.Start(taskStatus);
}
TaskStatus contains single event Completed.
What’ll do is that we’ll subscribe to this event to display some information on the View:
public void BackupFinished(object sender, EventArgs e)
{
// If the operation is done on different thread,
// you'll get "Cross-thread operation not valid"
// exception from WinForms controls here.
this.View.ShowMessage("Finished!");
}
So, what are the options:
Lets examine the last concept as SynchronizationContext is not a well-know-class in the .NET world.
Generally speaking this class is useful for synchronizing calls from worker thread to UI thread.
It has a static Current property that gets the synchronization context for the current thread or null if there is no UI thread (e.g. in Console application)
This is the TaskStatus class that utilizes SynchronizationContext.Current if it is not null:
public class TaskStatus
{
private readonly SynchronizationContext _context;
public event EventHandler Completed = delegate { };
public TaskStatus()
{
_context = SynchronizationContext.Current;
}
internal void OnCompleted()
{
Synchronize(x => this.Completed(this, EventArgs.Empty));
}
private void Synchronize(SendOrPostCallback callback)
{
if (_context != null)
_context.Post(callback, null);
else
callback(null);
}
};
Now lets see some tests.
First we’ll check if the event is executed:
[Test]
public void Completed_RaisesCompleted()
{
using(SyncContextHelper.No())
{
bool wasFired = false;
TaskStatus status = new TaskStatus();
status.Completed += (sender, args) => { wasFired = true; };
status.OnCompleted();
Assert.IsTrue(wasFired);
}
}
The following test shows that in WindowsForms application, although operation is executed on different thread, Completed event is routed back (using windows message queue) to the UI thread:
[Test]
public void Completed_WithSyncContext_IsExecutedOnSameThread()
{
using (SyncContextHelper.WinForms())
{
int completedOnThread = -1;
int thisThread = Thread.CurrentThread.GetHashCode();
TaskStatus status = new TaskStatus();
status.Completed += (sender, args) =>
{
completedOnThread =
Thread.CurrentThread.GetHashCode();
};
Scenario.ExecuteOnSeparateThread(status.OnCompleted);
// process messages send from background thread
// (like Completed event)
Application.DoEvents();
Assert.AreEqual(thisThread, completedOnThread);
}
}
When there is no SynchronizationContext (SynchronizationContext.Current == null) Completed event is executed on the different thread:
[Test]
public void Completed_InMultiThreadedScenario_IsExecuedOnDifferentThread()
{
using(SyncContextHelper.No())
{
int completedOnThread = -1;
int thisThread = Thread.CurrentThread.GetHashCode();
TaskStatus status = new TaskStatus();
status.Completed += (sender, args) =>
{
completedOnThread =
Thread.CurrentThread.GetHashCode();
};
Scenario.ExecuteOnSeparateThread(status.OnCompleted);
Assert.AreNotEqual(thisThread, completedOnThread);
}
}
Finally unit test helper classes:
public class SyncContextHelper : IDisposable
{
private readonly SynchronizationContext _previous;
private SyncContextHelper(SynchronizationContext context)
{
_previous = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(context);
}
public static SyncContextHelper WinForms()
{
return new SyncContextHelper(
new WindowsFormsSynchronizationContext());
}
public static SyncContextHelper No()
{
return new SyncContextHelper(null);
}
public void Dispose()
{
SynchronizationContext.SetSynchronizationContext(_previous);
}
};