Coroutines with MVVM Light

In the latest round of updates to Comicster, I've been working on the logic that asks the user to save their changes before closing their current file. You will have seen a prompt just like this in your favourite single-document-interface application. The workflow goes something like this:

  • If there's no current file, or if it's not modified, go ahead and do whatever you were going to do.
  • If the current file is modified, ask the user if they want to save their changes. Give them three options: Yes, No and Cancel.
  • If they say Yes, save the current file. If it hasn't already been saved, ask them for a filename first.
  • If they say No, skip ahead and do whatever you were going to do.
  • If they cancel, don't do anything else.

We're not even up to the part where you do whatever you were going to do once the file had been closed, and you can already see that the logic is pretty complex. It's bad enough doing it in a synchronous, imperative fashion, but doing it in an asynchronous way with MVVM Light's IMessenger was fast becoming nightmarish.

I complained on Twitter that the logic was very complex, and Rob Eisenberg pointed me to this document about coroutines in his MVVM framework Caliburn. Go ahead and read the article - I'll wait.

Finished? Cool. So the idea is that you can treat a bunch of asynchronous tasks as a series of sequential, synchronous steps. Caliburn has native support for all this, but I didn't want to switch to Caliburn at this late stage. Instead, I thought I'd have a crack at knocking up my own coroutine implementation that plays nice with MVVM Light.

The Activity Interface

The first step was to define an interface that represents each logical "step" in a process. Rob uses IResult, but I chose IActivity as my type name because I like to think of these as activities in a sequential workflow. Here's the interface:

public interface IActivity
{
    void Execute(ActivityContext context);
    event EventHandler<ActivityCompletedEventArgs> Completed;
}

Pretty straight forward. You implement this activity, and make sure you call the Completed event from inside the Execute method before you finish.

The Execute method takes an instance of a class I called ActivityContext - analogous to Rob's ActionExecutionContext class. It's just a way to pass a bunch of stuff to each activity. It looks like this:

public class ActivityContext
{
    public ActivityContext(IMessenger messenger)
    {
        MessengerInstance = messenger;
    }

    public IMessenger MessengerInstance { get; private set; }
    public bool Cancel { get; set; }
    public Exception Error { get; set; }
}

You can see that I'm including an instance of MVVM Light's IMessenger in the context so that activities have an easy way of communicating with the rest of the application.

Let's also take a quick look at the ActivityCompletedEventArgs class:

public class ActivityCompletedEventArgs : CancelEventArgs
{
    public ActivityCompletedEventArgs()
    {
    }

    public ActivityCompletedEventArgs(bool cancel)
        : base(cancel)
    {
    }

    public ActivityCompletedEventArgs(Exception error)
        : this()
    {
        Error = error;
    }

    public Exception Error { get; private set; }
}

So it's just a class with a boolean Cancel property and an Error property representing any exception that the activity encountered.

Defining Activities

Now that we have a way to define activities, let's do so! Here's a nice simple one I created which shows a dialog by pumping an instance of MVVM Light's DialogMessage class into the messenger:

public class MessageBoxActivity : IActivity
{
    public MessageBoxActivity(string text, string caption, 
        MessageBoxButton button = MessageBoxButton.OK, 
        MessageBoxImage icon = MessageBoxImage.None, 
        MessageBoxResult defaultResult = MessageBoxResult.OK)
    {
        _text = text;
        _caption = caption;
        _button = button;
        _icon = icon;
        _defaultResult = defaultResult;
    }

    string _text;
    string _caption;
    MessageBoxButton _button;
    MessageBoxImage _icon;
    MessageBoxResult _defaultResult;

    void Answered(MessageBoxResult result)
    {
        this.Result = result;
        Completed(this, new ActivityCompletedEventArgs(Result == MessageBoxResult.Cancel));
    }

    public MessageBoxResult Result { get; private set; }

    public void Execute(ActivityContext context)
    {
        context.MessengerInstance.Send(new DialogMessage(_text, Answered)
        {
            Caption = _caption,
            Button = _button,
            Icon = _icon,
            DefaultResult = _defaultResult,
        });
    }

    public event EventHandler<ActivityCompletedEventArgs> Completed = delegate { };
}

So when the activity is executed, it sends a new DialogMessage off to the application. When the application has done what it needs to do with the dialog, the callback method (Answered) is called, and that method fires the Completed event.

Executing a Workflow

Now that we have an activity defined we need some way of executing it from our ViewModels. Caliburn takes a really neat approach using a method that returns a bunch of activities, one after the other. What Rob's article doesn't show is the engine that iterates over the activities and executes them. I haven't looked at his code, but here's how I've implemented mine:

public ActivityContext Interact(IEnumerable<IActivity> activities)
{
    var context = new ActivityContext(MessengerInstance);

    var enumerator = activities.GetEnumerator();

    bool done = !enumerator.MoveNext();
    if (done) return context;

    while (!done && !context.Cancel && context.Error == null)
    {
        var activity = enumerator.Current;
        activity.Completed += (sender, e) =>
        {
            context.Cancel = e.Cancel;
            context.Error = e.Error;
            done = !enumerator.MoveNext();
        };
        activity.Execute(context);
    }
    return context;
}

This method walks over each activity in the IEnumerable<IActivity> parameter and executes it. If its Completed event says that it was cancelled or that an error occurred, it stops the whole process. I have a couple of overloads of this method which I'll demonstrate later.

Putting It All Together

So now we're back to our old problem of prompting the user to save their file before they open a different one. Let's have a look at my New method, which is called whenever the user tries to open a collection:

void Open(string fileName)
{
    this.Interact(
        Saver(askFirst: true),
        Opener(fileName));
}

As you can see, I'm calling my Interact method and passing it the result of two different methods. One of them saves the current collection (but asks first), and the other opens a new one. Let's have a look at those methods. First, Saver:

IEnumerable<IActivity> Saver(bool askFirst, bool saveAs = false)
{
    // if there is no collection opened, there's nothing
    // to do
    var collection = _content as CollectionViewModel;
    if (collection == null) yield break;

    // if the collection isn't modified and they didn't
    // click "Save As", there's nothing to do
    if (!collection.IsModified && !saveAs) yield break;

    var fileName = collection.FileName;
    if (askFirst)
    {
        // ask the user if they want to save
        var mbox = new MessageBoxActivity("Save changes to " + Path.GetFileName(fileName) + "?", "Save Changes", MessageBoxButton.YesNoCancel, MessageBoxImage.Question, MessageBoxResult.Yes);
        yield return mbox;

        // If they clicked No or Cancel, we have nothing else to do
        if (mbox.Result != MessageBoxResult.Yes) yield break;
    }

    // if the user clicked "Save As" or the collection has never been
    // saved, we need to ask for a filename
    if (saveAs || collection.IsNew || collection.FileName == null)
    {
        var sfd = new SaveFileDialogActivity(_settings, fileName);
        yield return sfd;

        if (sfd.Result != true) yield break;
        fileName = sfd.FileName;
    }

    // One way or another we now have a filename. Time to save the
    // file!
    yield return new CollectionSaver(fileName, collection.Collection);
    collection.FileName = fileName;
    collection.IsModified = false;
}

There's a lot of code in there, but hopefully you can read it from top to bottom and get an idea of what it's doing.

So when that's all finished (and assuming the user didn't hit Cancel along the way), Opener gets called:

IEnumerable<IActivity> Opener(string fileName)
{
    if (fileName == null)
    {
        var ofd = new OpenFileDialogActivity(_settings);
        yield return ofd;

        if (ofd.Result != true) yield break;

        fileName = ofd.FileName;
    }

    if (!File.Exists(fileName))
    {
        yield return new MessageBoxActivity(String.Format("File {0} does not exist", fileName), "Cannot Open File");
        yield break;
    }

    // time to load the file. Our CollectionLoader class has
    // a Collection property which will be populated if the
    // load succeeded
    var loader = CollectionLoader.Load(reader, fileName);
    yield return loader;

    if (loader.Collection != null)
    {
        this.Content = _collectionViewModelFactory(loader.Collection, fileName);
    }
}

Other Uses

You might have noticed above that my Interact method (which I'm sure will get a name-change in the near future) returns the context once it's done. You can make use of that if you have very simple steps in your process. For example, when the user wants to create a new file, we need to ask them if they want to save the current one, but then just go ahead and make a new collection. Creating a new collection is a one-liner, so you can do it like this:

void New()
{
    var context = Interact(Saver(askFirst: true));
    if (context.Cancel || context.Error != null) return;

    Content = _collectionViewModelFactory(new Collection(), "Untitled");
}

So that's calling Interact to do the prompting/saving, and then checking the return value to see if everything went smoothly before creating a new file.

Wrapping Up

Coroutines allow you to take a set of activities that might depend on a user's input or a long-running task and execute them in a sequential manner. It's the best of both worlds! I'd like to see MVVM Light incorporate something like this - perhaps in the form of an ActivityCommand (for want of a better name) whose Execute method could take the form of a method that returns an IEnumerable<IActivity> rather than one with no return type.

.net mvvm wpf coroutines
Posted by: Matt Hamilton
Last revised: 21 May, 2013 10:31 PM History

Trackbacks

Comments

Paul
Paul
07 Jan, 2011 09:47 AM

Hi Matt

great article. imho I don't understand why 'every' silverlight developer isn't using co-routines for sync/async co-ordination (or Caliburn Micro for that matter). It's definitely the most elegant solution we have until we get c#5 async in silverlight .

I haven't run your code but on quick glance is this a problem?

while (!done && !context.Cancel && context.Error == null) { var activity = enumerator.Current; activity.Completed += (sender, e) => { context.Cancel = e.Cancel; context.Error = e.Error; done = !enumerator.MoveNext(); }; activity.Execute(context); }

If the Execute method is async it'll return immediately and get called multiple times until the async operation fires Completed?

Caliburn actually hooks/unhooks the Completed event to what in your code would be the Interact method itself which simply calls Execute and doesn't do anything more until the event handler fires and calls it back.

I'm probably missing something and yours is fine but on first glance this didn't look right...

regards

07 Jan, 2011 10:48 AM

Hi Paul,

You're not missing anything - the code as written is problematic and not the same as the code in its current form. Grab the source from BitBucket and take a look - I ended up using a recursive method that returns immediately but calls itself when the each activity completes.

Thanks for your comment!

Jordan
Jordan
17 Feb, 2011 01:50 PM

Hi, I like it. May stop me moving to Caliburn as coroutines seem to be the best way to chain async operations. My only issue is you examples only show a DialogMessage type of action. I'm new to MVVM Light so not sure of which messages to use. In the case of a real world use of this would be to call a remove web service within the Activity and call the Action on completion, what message type would best be used in the .Send()?

17 Feb, 2011 10:42 PM

Hi Jordan,

You're right - I haven't given any concrete examples of calling a service asynchronously in an action. The easiest way, given a service that doesn't provide an async method to which you can pass a callback, is to wrap the call in a Task and do it all asyncronously:

public override void Execute(ActivityContext context)
{
    Task.Factory.StartNew(() =>
        {
            try
            {
                // call your service
                RaiseCompleted();
            }
            catch (Exception ex)
            {
                RaiseCompleted(ex);
            }
        });
}

Since the RaiseCompleted method uses the Dispatcher to raise the event back on the primary thread, you can call it from inside your task like that. You might provide some sort of public Result property on your activity that the caller can inspect to see what the service returned.

18 Feb, 2011 08:04 AM

My brain oh-so-slowly catches up... Some time after reading this I think I now grok it - and I really like it.

I've done plenty of work with asynchronous commands but never had to chain calls together; or at least, never tried to structure a chain of asynchronous methods together like this - probably because of the inherent complexity - I would've always tried to work round it.

Now that I see how coroutines work, you've definitely helped to remove the trepidation - thanks :)

[Slow and steady wins the race]

Your Comments

Used for your gravatar. Not required. Will not be public.
Posting code? Indent it by four spaces to make it look nice. Learn more about Markdown.

Preview