I'm starting a occasional series of postings on top things every
Silverlight
and WPF
developer should know. Originally, I was going to make this a
top-10 style list, but I'll keep that for my presentations, and
instead expand on it here, with one topic per post.
This one is about threading in Silverlight.
Ok, that subject sounds like a lot, but it's all pretty closely
related.
Both Silverlight and WPF have the concept of a UI thread.
Whenever you tie that thread up, you make your application
unresponsive. In Silverlight, if you're on a page with other
plug-ins, you make all the plug-ins unresponsive. (It's actually
per-process, but most modern browsers separate processes by
tabs)
Async Network Calls
In order to keep from tying up the UI thread, you have to
perform long-running processes on another thread. Often, but not
always, those processes are network calls. In Silverlight, all
network calls are async with no provision to handle them any other
way. This lets you off the hook threading-wise, but requires that
you understand how to chain those calls, and how to deal with
libraries that have IAsyncResult callbacks that return on different
threads.
TestServiceClient client = new TestServiceClient();
client.DoSomethingSlowCompleted += (s, ea) =>
{
// do something with the results here
Debug.WriteLine(ea.Result);
};
client.DoSomethingSlowAsync();
Background Worker
For other processes, the BackgroundWorker is a great, simple, way to do
some background work while reporting progress to the UI. It takes
all the dispatching work away, so you can feel free to manipulate
the UI from the ProgressChanged and RunWorkerCompleted events. Do
not touch the UI from the DoWork event, as that is actually running
on a separate thread.
private BackgroundWorker _worker = new BackgroundWorker();
private void RunLongProcess()
{
_worker.WorkerReportsProgress = true;
ProgressBar.Minimum = 0;
ProgressBar.Maximum = 100;
_worker.DoWork += (s, e) =>
{
for (int i = 0; i < 100; i++)
{
// simulate long-running work
System.Threading.Thread.Sleep(500);
((BackgroundWorker)s).ReportProgress(i+1);
}
};
_worker.ProgressChanged += (s, e) =>
{
// this is on the UI thread, so you can
// update UI from here.
ProgressBar.Value = e.ProgressPercentage;
};
_worker.RunWorkerCompleted += (s, e) =>
{
// clean up after your stuff, yes, you can touch UI here.
};
_worker.RunWorkerAsync();
}
Of course, knowing how to set up event handlers using lambda
expressions is always helpful :)
Dispatcher
Sometimes, you spin off a thread of your own, or you have to
deal with some of the crufty stacks where the callbacks come back
on a different thread from the one that created the call. In those
cases, you need to use a dispatcher to make a call back to the UI
thread.
If the code is on a page, you can call the dispatcher property
and pass it a function to execute (yes, more lambda in this
example).
private void UpdateUI()
{
Dispatcher.BeginInvoke(() =>
{
ProgressBar.Value = 50;
});
}
But, if the code is in some sort of non-UI class, it gets more
complex. You can do it this way, using a dummy control (like we did
in Silverlight 2):
TextBlock _dispatcherObject = new TextBlock();
private void UpdateUINasty() // avoid this approach unless it's all you've got
{
_dispatcherObject.Dispatcher.BeginInvoke(() =>
{
SomePage.ProgressBar.Value = 50;
});
}
This example assumes the object itself was created on the UI
thread. The better way is to use the Deployment object:
private void UpdateUINotSoNasty()
{
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
SomePage.ProgressBar.Value = 50;
});
}
That's better. Definitely the recommended approach for recent
versions of Silverlight, including Windows Phone 7.
Timers
What about when you need to run something on a timer? The normal
timer classes require manual dispatching to do any UI updates.
However, Silverlight and WPF both have the DispatcherTimer class that handles all that
for you.
private void StartTimer()
{
DispatcherTimer timer = new DispatcherTimer();
timer.Tick += (s, e) =>
{
// do some very quick work here
// update the UI
StatusText.Text = DateTime.Now.Second.ToString();
};
timer.Interval = TimeSpan.FromSeconds(1);
timer.Start();
}
Of course, realize that what you're doing here is
interrupting the UI thread, not really running
anything on a separate thread. It's not suitable for anything
long-running and cpu-intensive, but rather something where you need
to execute on a regular interval. Clock UI updates are a perfect
example.
Threads
In general, I discourage folks from spinning off real threads.
Very few people understand how to efficiently work with threads, so
even if you're a threading genius, the person coming in after you
probably isn't. In most cases, the Background Worker will provide
you with what you need.
Silverlight doesn't yet have it, but the Parallel Tasking support built into .NET 4 can
really help you out should you really need to do more than two
things at once. Definitely check it out.
Conclusion
Threading in Silverlight and WPF is a little trickier than some
other technologies, because you have to worry about the UI thread.
My intention here was to both inform you that this trickiness
exists, and then show some ways of dealing with it.