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: 15 Oct, 2024 10:53 PM History

Trackbacks