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);
}
 
In this example, there are two asynchronous service methods which get called. Their results are passed to handler methods where they are processed by Actions, but those Actions don’t execute until both OpComplete methods have fired (actually the last OpComplete triggers the execution of all the Actions. It doesn’t matter which call returns first. Their associated Actions will execute in the order defined by the keys in the creation of the OperationSync object. There are a million different ways to put this together with lambdas and anonymous methods, but I tried to bust it up here so it would be easier to follow.

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.

Technorati Tags: ,,,
Connecting ActiveRecord to SQL Server