Introduction
In the current public alpha of Silverlight 1.1, there is no dispatcher like the wonderful BackgroundWorker
for WinForms developers (since 2.0) and using the Thread
in Silverlight is so not easy. For a “WinFormer”, this is a huge drawback. Without BackgroundWorker
, you can only use or create controls in the Silverlight main thread (except if you like cross thread exception...). In WinForms, the logic is to manipulate controls in the main thread and do logic and mathematical computing in another “Logic” thread. Every specific period, a refresh is made to coordinate things. The BackgroundWorker
is specifically devoted to this kind of task. Let's start making our own Silverilght BackgroundWorker
.
First thing: A timer
The idea is to create a Storyboard by code with an animation duration of 100 milliseconds (you can change the value). Each time the Storyboard is completed, you have to analyse the associated thread currently running and generate events, if needed. I do not use an HtmlTimer
because the class is deprecated.
public BackgroundWorker(Panel host)
{
if (host == null)
throw new ArgumentNullException("host", "Host must be valid");
this._host = host;
this.ConfigureStoryboard();
}
The ConfigureStoryboard
method creates the Timer:
private void ConfigureStoryboard()
{
if (!BackgroundWorker._hostsToStoryboards.ContainsKey(this._host))
{
string xamlCode = string.Format(BackgroundWorker._tickTimerStoryboardXaml,
Guid.NewGuid().ToString());
Storyboard storyboard = XamlReader.Load(xamlCode) as Storyboard;
this._host.Resources.Add(storyboard);
StoryboardData data = new StoryboardData() { Storyboard=storyboard,
IsStarted = false, NumberOfHostManaged=0 };
BackgroundWorker._hostsToStoryboards.Add(this._host, data);
}
bool isStarted = BackgroundWorker._hostsToStoryboards[this._host].IsStarted;
BackgroundWorker._hostsToStoryboards[this._host].Storyboard.Duration =
new TimeSpan(0, 0, 0, 0, this._refreshInterval);
if (isStarted)
this.StartTicks();
}
The hostsToStoryboards
field is a Dictionary
witch associates a Host to a Storyboard. This helps to have only one timer run by the host (and not one by the BackgroundWorker
). The tickTimerStoryboardXaml
field contains a serialized XAML Storyboard:
private static string _tickTimerStoryboardXaml =
@"<Storyboard xmlns:x=""http://schemas.microsoft.com/winfx" +
@"/2006/xaml""x:Name=""TickTimer_{0}""></Storyboard>";
The StoryboardData
class informs if a Storyboard is started, and the number of threads currently listened to by the Storyboard. The RefreshInterval
property is simply the Timer
period:
BackgroundWorker._hostsToStoryboards[this._host].Storyboard.Duration =
new TimeSpan(0, 0, 0, 0, this._refreshInterval);
Each time the Storyboard is completed, the Logical Thread is analysed:
private void BackgroundTickOccurs(object sender, EventArgs e)
{
if (this._progressChangedEventArgs != null)
{
if (this.ProgressChanged != null)
this.ProgressChanged(this, this._progressChangedEventArgs);
this._progressChangedEventArgs = null;
}
if (this._completed && !this._completedLaunched)
{
BackgroundWorker._hostsToStoryboards[this._host].NumberOfHostManaged--;
if (this.RunWorkerCompleted != null)
this.RunWorkerCompleted(this, this._runWorkerCompletedEventArgs);
this._isBusy = false;
this._completedLaunched = true;
}
if (BackgroundWorker._hostsToStoryboards[this._host].NumberOfHostManaged <= 0)
{
BackgroundWorker._hostsToStoryboards[this._host].Storyboard.Stop();
BackgroundWorker._hostsToStoryboards[this._host].Storyboard.Seek(
new TimeSpan(0, 0, 0, 0, 0));
}
BackgroundWorker._hostsToStoryboards[this._host].NumberOfHostManaged--;
if (!this._completedLaunched)
this.StartTicks();
}
If the Logic Thread has instantiated a ProgressChangedEventArgs
object, I generate a ProgressChanged
event. If the thread is completed and the Completed
event is not launched, I generate a RunWorkerCompleted
event. If the storyboard has no Thread to analyse, it is stopped.
The logic thread
The InternalJob
method is the ThreadStart
delegate of the thread. This method is called asynchronously from the main thread.
private void InternalJob()
{
this._doWorkEventArgs = new DoWorkEventArgs(this._argument);
this._runWorkerCompletedEventArgs = new RunWorkerCompletedEventArgs(null);
try
{
if (this.DoWork != null)
this.DoWork(this, this._doWorkEventArgs);
this._runWorkerCompletedEventArgs.Result = this._doWorkEventArgs.Result;
}
catch (Exception e)
{
this._runWorkerCompletedEventArgs.Error = e;
}
this._completed = true;
}
The DoWork
linked method is called inside a try-catch
block. If the method completes well, the runWorkerCompletedEvent
takes the return result of the doWorkEventArgs
passed to the DoWork
method. If an exception is caught, the runWorkerCompletedEvent
takes the exception.
Calling the Worker
First instantiate the Worker, passing the host for the timer:
this._worker = new BackgroundWorker(this.Host);
Then, link your event methods:
this._worker.DoWork += new DoWorkEventHandler(_worker_DoWork);
this._worker.ProgressChanged +=
new ProgressChangedEventHandler(_worker_ProgressChanged);
...
And to finish, start the Worker:
this._worker.RunWorkerAsync();
The sample
I made a simple sample, where a BackgroundWorker
will animate an X shape in a canvas. When you click on a link, a shape is added on the client area.
Apologizes
The article may be difficult to read: I speak very bad English and I apologize for this. I hope this code will help you, like it helped me. Don’t hesitate to send me comments! (Yes, in English if you can :) ).