Synchronizing UI Operations with Asynchronous Dependencies
When working with asynchronous data access such as using a Silverlight client to access data from a server side service, you will inevitably run into the situation where you have two or more calls which have results that need to be processed in a synchronous manner. A simple example is populating a list box from server side data and then making a second call to get the value that needs to be selected. You could wait until the list is populated before making the second call, but that would be a waste.
I built a little helper object that allows you to define Action delegates that will get executed in a specific order once all pending operations have completed. Below is a snippet of how it can be used.
private ILoginService loginService; private ILoginView loginView; private OperationSync localeOpSync = new OperationSync(LocaleOps.GetLocales, LocaleOps.GetDefaultLocale); private void PopulateLocales() { loginService.GetLocales(HandleGetLocalesResult); loginService.GetDefaultLocale(HandleGetDefaultLocaleResult); } private void HandleGetDefaultLocaleResult(string result) { localeOpSync.OpCompleted(LocaleOps.GetDefaultLocale, GetSetSelectedLocaleAction(result)); } private void HandleGetLocalesResult(string[] result) { localeOpSync.OpCompleted(LocaleOps.GetLocales, GetSetLocaleSelectionAction(result)); } private Action GetSetSelectedLocaleAction(string result) { return () => loginView.SetSelectedLocale(result); } private Action GetSetLocaleSelectionAction(string[] result) { return ()=> loginView.SetLocaleSelections(result); }
To show how it works I’ve included a subset of the unit tests (with extension methods and other goodies stripped out). The setup for all the specs is at the bottom of the code snippet.
[TestFixture] public class when_not_completing_all_ops : behaves_like_setting_up_operation_actions { [Test] public void should_not_execute_actions() { OpSync.OpCompleted(OpKey.One, Action1); OpSync.OpCompleted(OpKey.Two, Action2); CollectionAssert.IsEmpty(ActionResults); } } [TestFixture] public class when_completing_all_ops_in_key_order : behaves_like_setting_up_operation_actions { [Test] public void should_execute_actions_in_key_order() { OpSync.OpCompleted(OpKey.One, Action1); OpSync.OpCompleted(OpKey.Two, Action2); OpSync.OpCompleted(OpKey.Three, Action3); Assert_That_ActionResults_Fire_In_Correct_Order(); } } [TestFixture] public class when_completing_all_ops_not_in_key_order : behaves_like_setting_up_operation_actions { [Test] public void should_execute_actions_in_key_order() { OpSync.OpCompleted(OpKey.Two, Action2); OpSync.OpCompleted(OpKey.Three, Action3); OpSync.OpCompleted(OpKey.One, Action1); Assert_That_ActionResults_Fire_In_Correct_Order(); } } public abstract class behaves_like_setting_up_operation_actions { protected OperationSync OpSync { get; set; } protected Action Action1 { get; set; } protected Action Action2 { get; set; } protected Action Action3 { get; set; } protected List<OpKey> ActionResults { get; set; } [SetUp] public void Before_Each_Spec() { OpSync = new OperationSync(OpKey.One, OpKey.Two, OpKey.Three); Action1 = () => ActionResults.Add(OpKey.One); Action2 = () => ActionResults.Add(OpKey.Two); Action3 = () => ActionResults.Add(OpKey.Three); ActionResults = new List<OpKey>(); } protected void Assert_That_ActionResults_Fire_In_Correct_Order() { Assert.That(ActionResults.Count, Is.EqualTo(3)); Assert.That(ActionResults[0], Is.EqualTo(OpKey.One)); Assert.That(ActionResults[1], Is.EqualTo(OpKey.Two)); Assert.That(ActionResults[2], Is.EqualTo(OpKey.Three)); } protected enum OpKey { One, Two, Three } }
The keys can be anything. I have just been creating a private enum in the class where I need them. Here’s the OperationSync class that handles it all:
public class OperationSync { private static readonly object syncRoot = new object(); private readonly object[] keys; private readonly Dictionary<object, bool> keyKeepActionAfterExecute = new Dictionary<object, bool>(); private readonly Dictionary<object, Action> keyActions = new Dictionary<object, Action>(); public OperationSync(params object[] operationKeys) { keys = operationKeys; foreach (var o in operationKeys) { keyActions.Add(o, null); } } public void OpCompleted(object key, Action action) { OpCompleted(key, action, false); } public void OpCompleted(object key, Action action, bool keepActionAfterExecute) { lock (syncRoot) { if (!keyActions.ContainsKey(key)) throw new UIException("Key '{0}' not found."); keyActions[key] = action; keyKeepActionAfterExecute[key] = keepActionAfterExecute; if (AllOpsComplete()) ProcessActions(); } } private void ProcessActions() { ExecuteEachAction(); ClearActionsIfIndicated(); } private void ExecuteEachAction() { foreach (var key in keys) { keyActions[key](); } } private void ClearActionsIfIndicated() { foreach (var key in keys) { if (!keyKeepActionAfterExecute[key]) keyActions[key] = null; } } private bool AllOpsComplete() { return keyActions.All(pair => pair.Value != null); } }
There’s probably a fancy smancy AJAX pattern name for this sort of thing. Let me know if you come across one.