SYSK 381: How To Get One Callback When Multiple Async Services Calls are Completed

My current project uses Silverlight 4.0 and RIA services, and I quickly came to realize the benefits and the challenges of all DomainService calls being asynchronous.

 

Consider this scenario: a Silverlight client app needs to get some data, that requires multiple DomainService calls – it may be because upon “processing” the first resultset, the business rules require getting additional data, or, perhaps, it’s due to recursive implementation (parent/child situation). In my case, it was the latter, and getting all data in one swoop on the server wasn’t supported by the available API… or, to be more accurate, making it work this way would have required a lot more code and would have ended up with messier implementation, but, perhaps, better performance (we’ll be doing performance profiling soon and may end up going that route if performance difference is significant).

Anyway, to summarize, here is the problem I’m trying to address (diagram is simplified for clarity):

 

 

As you can see, given all the async calls, we have to add the logic to assure that the client’s callback function is only invoked when all the async calls in the façade returned, i.e. all data is there…

 

One way to do it, is by passing along the “notification context”:

 

1. Define ClientNotifyAction class (context)

 

public interface INotifyClientAction

    {

        void OperationStarted();

        void OperationCompleted();

        Dictionary<string, object> Data { get; }

        object this[string key] { get; set; }

    }

    public class NotifyClientAction<T> : SynchronizationContext, INotifyClientAction where T : new()

    {

        private long _callCount;

        private Action<T> _onDataLoaded;

        private T _result;

        private Dictionary<string, object> _data = new Dictionary<string,object>();

        public NotifyClientAction(Action<T> callback)

        {

            _onDataLoaded = callback;

            _result = new T();

        }

        public Dictionary<string, object> Data

        {

            get { return _data; }

        }

        public object this[string key]

        {

            get { return _data.ContainsKey(key) ? _data[key] : null; }

            set { _data[key] = value; }

        }

        public T Result

        {

            get { return _result; }

            set { _result = value; }

        }

        public override void OperationStarted()

        {

            base.OperationStarted();

            Interlocked.Increment(ref _callCount);

        }

        public override void OperationCompleted()

        {

            base.OperationCompleted();

            if (Interlocked.Decrement(ref _callCount) == 0)

            {

                if (_onDataLoaded != null)

                {

                    _onDataLoaded(_result);

     }

            }

        }

    }

 

2. From the DataServiceFacade.GetData1 method, create an instance of the notification context, and pass it along

 

public partial class DataServiceFacade

{

    public static void GetData1(Action<ResultType> callback)

    {

    NotifyClientAction<ResultType> ctx =

new NotifyClientAction<ResultType>(callback);

        ctx.OperationStarted();

// Now call the DataService.GetData1

// asynchronously and pass ctx as ‘UserState’

    }

    public static void GetData1CallBack(LoadOperation<Data1Type> loadOp)

    {

    INotifyClientAction ctx = loadOp.UserState as INotifyClientAction;

 // Loop processing results, call GetData2 as needed

 ctx.OperationCompleted();

    }

   private static void GetData2(..., INotifyClientAction ctx)

    {

        ctx.OperationStarted();

// Call the DataService.GetData2

// asynchronously and pass ctx as ‘UserState’

    }

    public static void GetData2CallBack(LoadOperation<Data2Type> loadOp)

    {

    INotifyClientAction ctx = loadOp.UserState as INotifyClientAction;

 // Loop processing results, call other methods as needed

 ctx.OperationCompleted();

   

    }

}

 

 

3. Remember to set ctx.Result to the data to be returned to the client in the appropriate place

 

NOTE: with RIA services (at least as of the time of writing this block), a DomainService instance can only be instantiated from the same thread (or you’ll get runtime errors)… Therefore, the code above, is not written to be thread-safe. Add locking as required, if you expect to use it in a multi-threaded execution scenario.