Wednesday, September 1, 2010

Silverlight: Performing Multiple Asynchronous Calls

If you've spent any time with Silverlight, you've probably used it with a Web Service (ASMX or WCF, either one). And the first thing that jumps out at you is the fact that Silverlight does not and in fact cannot generate synchronous methods to call the web methods. In stead of a method called GetData that returns a value, you have a GetDataAsync method and a GetDataCompleted handler, to which the return value will be in the EventArg parameter.

This is not a bad thing. I don't particularly want a plugin blocking the browser's UI thread with a long-running synchronous call. In fact, I've read (somewhere, can't find the reference) that this is the reason that MS didn't allow Silverlight to do synchronous calls.

However, there are times when you need to do multiple calls, and then have something happen when they are all done. You can't trust that calls will finish in the order that you start them in, because these calls can run simultaneously. There are a few options to do this. I'll show you two examples. Article continues after the jump.

First, here's the code for a simple example WCF service that we'll be using.

using System.ServiceModel;
using System.ServiceModel.Activation;

namespace AsyncExample.Web
{
    [ServiceContract(Namespace = "")]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    public class ExampleService
    {
        [OperationContract]
        public string GetSomeData() { return "Some Data"; }

        [OperationContract]
        public string GetSomeMoreData() { return "Some More Data"; }
    }
}


One way is to "chain" your calls together:

using System;
using System.Windows;
using System.Windows.Controls;
using AsyncExample.ExampleService;

namespace AsyncExample
{
    public partial class ChainingExample : UserControl
    {
        private ExampleServiceClient serviceClient;
        private string someData, someMoreData;

        public ChainingExample()
        {
            InitializeComponent();
            serviceClient = new ExampleServiceClient();
        }

        private void button1_Click(object sender, RoutedEventArgs e)
        {
            serviceClient.GetSomeDataCompleted += new EventHandler<GetSomeDataCompletedEventArgs>(serviceClient_GetSomeDataCompleted);
            serviceClient.GetSomeMoreDataCompleted += new EventHandler<GetSomeMoreDataCompletedEventArgs>(serviceClient_GetSomeMoreDataCompleted);
            serviceClient.GetSomeDataAsync();
        }

        private void serviceClient_GetSomeDataCompleted(object sender, GetSomeDataCompletedEventArgs e)
        {
            someData = e.Result;
            serviceClient.GetSomeMoreDataAsync();
        }

        private void serviceClient_GetSomeMoreDataCompleted(object sender, GetSomeMoreDataCompletedEventArgs e)
        {
            someMoreData = e.Result;
            DisplayData();
        }

        private void DisplayData()
        {
            string message = "Got all data.\n{0}\n{1}";
            MessageBox.Show(string.Format(message, someData, someMoreData));
        }
    }
}

This isn't my favorite method, but it's ok for a small number of calls. The reason I don't care for it is because flow doesn't seem intuitive or natural to me. Also, if you add more calls, you have to make changes to both the service call behind and ahead of the ones you add.

This option has a strength which is also its weakness: it mimics a synchronous pattern. Call 2 is not started until Call 1 is finished. If you have things that must happen in a specific order, this is is a great way to ensure that. However, if you have long running calls (or more than a few short calls) that could be made simultaneously, you hurt performance by using this pattern.

My favorite way of doing this is to create a "Facts" class that contains the service proxy, public properties for the Result values of the methods, and an event.

using System;
using AsyncExample.ExampleService;

namespace AsyncExample
{
    public delegate void GetAllDataCompletedHandler(object sender, EventArgs e);

    public class ExampleServiceFacts
    {
        public event GetAllDataCompletedHandler GetAllDataCompleted;
        protected virtual void OnGetAllDataCompleted(EventArgs e)
        {
            GetAllDataCompleted(this, e);
        }

        public string SomeData { get; set; }
        public string SomeMoreData { get; set; }

        private object locker = new object();
        private ExampleServiceClient serviceClient;
        private int serviceCallsCompletedCount;
        private const int totalServiceCalls = 2;

        public ExampleServiceFacts()
        {
            serviceClient = new ExampleServiceClient();
            serviceClient.GetSomeDataCompleted += new EventHandler<GetSomeDataCompletedEventArgs>(serviceClient_GetSomeDataCompleted);
            serviceClient.GetSomeMoreDataCompleted += new EventHandler<GetSomeMoreDataCompletedEventArgs>(serviceClient_GetSomeMoreDataCompleted);
        }

        public void GetAllDataAsync()
        {
            serviceCallsCompletedCount = 0;
            serviceClient.GetSomeDataAsync();
            serviceClient.GetSomeMoreDataAsync();
        }

        public void ServiceCallCompleted()
        {
            lock (locker)
                ++serviceCallsCompletedCount;
            if (serviceCallsCompletedCount >= totalServiceCalls)
                OnGetAllDataCompleted(new EventArgs());
        }

        private void serviceClient_GetSomeDataCompleted(object sender, GetSomeDataCompletedEventArgs e)
        {
            SomeData = e.Result;
            ServiceCallCompleted();
        }

        private void serviceClient_GetSomeMoreDataCompleted(object sender, GetSomeMoreDataCompletedEventArgs e)
        {
            SomeMoreData = e.Result;
            ServiceCallCompleted();
        }
    }
}

With this class, you make one single call (GetAllDataAsync). Internally, this will kick off all your service calls. As each one completes, it iterates a counter (in a threadsafe manner, that's why we lock the section there) and checks to see if it was the last one finished. If not, it does nothing. If it was the last method to finish, it triggers the Completed event that, hopefully, you have subscribed to in your client code:

using System.Windows;
using System.Windows.Controls;

namespace AsyncExample
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, RoutedEventArgs e)
        {
            ExampleServiceFacts facts = new ExampleServiceFacts();
            string message = "Got all data.\n{0}\n{1}";
            facts.GetAllDataCompleted += (snd, ea)
                => MessageBox.Show(string.Format(message, facts.SomeData, facts.SomeMoreData));
            facts.GetAllDataAsync();
        }
    }
}

Quick note, I've used a Lambda here as the handler for the GetAllDataCompleted event instead of a named method. The idea is the same though.

3 comments:

  1. Hi Curtis,

    Thanks so much for posting this so long ago! It's helping me quite a lot in my transition to Silverlight.

    I'm having a small problem though and wondered if you've experienced it too: It seems as though my main XAML page isn't correctly subscribing to the event.

    In other words, when the code arrives at the OnGetAllDataCompleted method, GetAllDataCompleted is always null.

    If you had any suggestions on this, I'd be very grateful.

    Michael

    ReplyDelete
  2. I've since discovered that when the callback from the web service occurs (which is structure differently from yours), the object variables (including the method assigned to GetAllDataCompleted) are gone.

    Before pulling the data from the web service, the GetAllDataCompleted event is hooked up correctly to the Lambda expression.

    I'm still puzzling about why this is the case!

    ReplyDelete
  3. I figured it out finally... on the main XAML page, I had not passed the correct instance of the class to HtmlPage.RegisterScriptableObject().

    All's well now... Thanks again for your post!

    ReplyDelete

Speak your mind.