One enhancement that quietly slipped into the Silverlight 5 RC is the inclusion of Tasks. This is not the full-blown Task Parallel Library (TPL) to my knowledge (as it lacks the Parallel
class and extensions) but it does provide Task
, its related factories, and the functionality to chain and monitor tasks. The purpose of the library is to simplify asynchronous methods.
This post will show you an example of using the library. I'm not presenting this as a best practice, but only as a thought exercise to show you the use of the Task library in a visual way. The program computes the factorials of several numbers, but uses task continuation to ensure only one web service call is made at a time (thus simulating a sequential asynchronous workflow). You can modify the example to fire all threads simultaneously.
First, a simple service in the web project to compute the factorial:
[ServiceContract(Namespace = "http://jeremylikness.com/examples/")] [SilverlightFaultBehavior] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] public class SimpleService { [OperationContract] public long Factorial(int value) { long result = value; while (--value > 0) { result *= value; } return result; } }
Add the service reference to the Silverlight project. Next, create a simple class in the Silverlight project to hold the value and the result that implements property change notifications:
public class Factorial : INotifyPropertyChanged { private int _value; public int Value { get { return _value; } set { _value = value; RaisePropertyChange("Value"); } } private long _result; public long Result { get { return _result; } set { _result = value; RaisePropertyChange("Result"); } } protected void RaisePropertyChange(string propertyName) { var handler = PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } } public event PropertyChangedEventHandler PropertyChanged; }
The main page holds a button to fire off the calls and a list box to show the results:
<Grid x:Name="LayoutRoot" Background="White"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <ListBox Grid.Row="1" x:Name="FactorialList"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Value}"/> <TextBlock Text="!="/> <TextBlock Text="{Binding Result}"/> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <Button Click="_BtnFactorClick" x:Name="btnFactor" IsEnabled="False" Content="Factor" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10"/> </Grid>
In the code-behind for the main page, hold a list of the Factorial
objects:
private readonly List<Factorial> _factorials;
Then populate it with the numbers 1 through 20 and bind it to the list box - at this point you can run the application and see the list of numbers to be factored:
public MainPage() { InitializeComponent(); _factorials = new List<Factorial>(); for (var x = 0; x < 20; x++) { _factorials.Add(new Factorial {Value = x + 1}); } FactorialList.ItemsSource = _factorials; }
Add the code to create a channel for the service. Use this instead of the proxy client so you can take advantage of the asynchronous programming model (APM) and use the begin/end methods. The task library provides a way to conveniently convert an APM call to a task. This example simply builds the path to the service using the location of the XAP to ensure it always points to the correct URL:
var source = Application.Current.Host.Source; var uriBuilder = new UriBuilder(source.Scheme, source.Host, source.Port, "/SimpleService.svc"); var channelFactory = new ChannelFactory<SimpleServiceChannel>( new BasicHttpBinding(), new EndpointAddress(uriBuilder.Uri)); var channel = channelFactory.CreateChannel();
At the top of the code-behind for the main page, add a list of tasks that return a long
(the return type of the service):
private readonly List<Task<long>> _tasks;
Create a method to return a task. The task factory launches the task immediately. In this example, you want one task to complete before firing the next. Therefore, the task factory call is wrapped in another task to kick it off. The task that is created directly will not launch until you explicitly invoke it. This method will return a task that is primed to return the result of the service call when it is invoked:
private Task<long> _CreateFactorialTask(int idx, SimpleService channel) { return new Task<long>(() => Task<long>.Factory.FromAsync( channel.BeginFactorial, channel.EndFactorial, _factorials[idx].Value, null).Result); }
If you want the threads to all fire simultaneously (to the extent the thread pool allows) simply remove the Task
wrapper and return the result of the factory call directly.
Notice that the factory takes the form of "begin method" then "end method" followed by the parameters that are passed in. The last parameter is a state object that you don't need for this example. When the results return, they aren't guaranteed to return on the UI thread, so create a method to safely dispatch them when needed:
private void _SafeInvoke(Action action) { if (Dispatcher.CheckAccess()) { action(); } else { Dispatcher.BeginInvoke(action); } }
Now you can create the task list. Place this code after creating the factorial list. All the code does is create a list of task objects, and marks each one to continue by updating the result and then launching the next task in line. An additional method ensures the very last result is obtained, and then the button to kick off the process is enabled:
_tasks = new List<Task<long>>(); for (var count = 0; count < _factorials.Count; count++) { _tasks.Add(_CreateFactorialTask(count, channel)); if (count <= 0) continue; var localCount = count; _tasks[localCount - 1].ContinueWith( taskCompleted => { _SafeInvoke( () => _factorials[localCount - 1].Result = taskCompleted.Result); _tasks[localCount].Start(); return _tasks[localCount]; }); } _tasks[_tasks.Count - 1].ContinueWith( taskCompleted => _SafeInvoke(() => _factorials[_factorials.Count - 1].Result = taskCompleted.Result)); btnFactor.IsEnabled = true;
If you know why the count is copied to the localCount
variable, good job! If not, no worries. The tasks are being created using lambda expressions with anonymous methods, so the locally scoped variable is used to capture the value of the variable. This is referred to as a "closure" and you can learn more about it here.
Now you can run the application and the tasks will be queued up, but not running. The last step is to kick off the workflow. All that is required is to start the first task. Because the tasks were linked, each task will complete, update the results, then kick off a new task that will generate the next web service call. When you click the button, you should see the factorials updating in order.
private void _BtnFactorClick(object sender, RoutedEventArgs e) { btnFactor.IsEnabled = false; _tasks[0].Start(); }
This example just scratched the surface of what is possible. You can convert event-based tasks using a task completion source, orchestrate groups of tasks to launch in parallel and wait for them to finish, and even nest tasks inside of other tasks. This is a very powerful and welcome addition to the Silverlight tool box!
Download the source here.