MVVM Light Activities and Exceptions

As part of my contrib project to add support for coroutines to MVVM Light, I've been thinking about how to handle exceptions.

Let's assume we have some code like this in an ActivityCommand handler method:

IEnumerable<IActivity> GetUnicorns()
{
    yield return new BusyIndicator(true); // show that we're busy

    yield return new UnicornDownloader(); // get the unicorns!

    yield return new BusyIndicator(false); // we're no longer busy
}

Great! We use a BusyIndicator activity to somehow notify the user that we're busy (heck, maybe it's just changing the cursor to an hourglass), then we kick off a UnicornDownloader activity to fetch the unicorns we needed. When it completes, we tell the user that we're not busy anymore.

However, there's a problem with this code. Unicorns aren't real! They're fantasy creatures! The UnicornDownloader activity is going to throw an AnimalNotFoundException!

That means one of two things will happen, depending on how well-written our UnicornDownloader activity is:

  1. The activity will raise the Completed event with the ActivityCompletedEventArgs.Error property set to an AnimalNotFoundException instance, and the processor will stop running the sequence.

  2. The activity will simply throw an exception, which we're not handling, causing the application to die.

Either way, the third activity in the sequence (letting the user know that we're not busy anymore) will never be executed. We needed a way to convey the exception information back to the calling method.

So what to do? Caliburn handles this situation in a unique way: It looks for a method on your ViewModel flagged as a "rescue" method and passes the execption to that. MVVM Light is, at its core, a little more explicit, and doesn't depend on special methods or anything. It's supposed to be a very lightweight, simple approach to MVVM.

What We Didn't Do

I considered a few different approaches to solving this problem:

Throw the Exception (Not an Option)

The first idea was to simply throw any exception from the ActivityProcessor.Process method and leave it up to the caller to handle. This wasn't an option, because the code below doesn't actually compile. You can't yield return from inside a try or catch block!

IEnumerable<IActivity> GetUnicorns()
{
    yield return new BusyIndicator(true); // show that we're busy

    try
    {
        yield return new UnicornDownloader(); // get the unicorns!
    }
    catch(AnimalNotFoundException ex)
    {
        // log the exception, maybe notify the user
    }
    finally
    {
        yield return new BusyIndicator(false); // we're no longer busy
    }
}

Continue Execution

Secondly, I considered changing the IActivity interface to look like this:

public interface IActivity
{
    Exception Error { get; set; }  // <-- add this

    void Execute(ActivityContext context);

    event EventHandler<ActivityCompletedEventArgs> Completed;
}

So when the ActivityProcessor sees an exception (whether it was unexpectedly thrown by the activity or returned as part of the Completed event) it could set the Error property on the activity and continue execution. The GetUnicorns method would now look more like this:

IEnumerable<IActivity> GetUnicorns()
{
    yield return new BusyIndicator(true); // show that we're busy

    var loader = new UnicornDownloader();
    yield return loader; // get the unicorns!

    if (loader.Error != null)
    {
        // log the exception, maybe notify the user
    }

    yield return new BusyIndicator(false); // we're no longer busy
}

This is still a good solution if you know your activity might raise an exception and you think you can handle it gracefully. I would recommend this approach in that case, but it doesn't mean that we needed a new property on the IActivity interface.

Use a Special Activity

This option occurred to me later: A special "exception handling" activity. When you yield it to the processor, it doesn't get executed - it just gets saved away and executed if an exception is encountered. Our unicorn-fetching code in this case might look something like this:

IEnumerable<IActivity> GetUnicorns()
{
    // this only gets executed if an exception is
    // thrown by one of the subsequent activities
    yield return new ExceptionHandler(context =>
        {
            // log the exception, maybe notify the user.
            // then return the "clean-up" activity:
            return new BusyIndicator(false);
        });

    yield return new BusyIndicator(true); // show that we're busy

    yield return new UnicornDownloader(); // get the unicorns!

    yield return new BusyIndicator(false); // we're no longer busy
}

So in this case we have a special ExceptionHandler activity that takes a Func<ActivityContext, IActivity> as its constructor parameter. If an exception is raised or returned by any subsequent activity, the ExceptionHandler activity is executed to get one last activity that it can use to clean up.

Yielding a second ExceptionHandler activity would, I suppose, overwrite the first one, effectively changing the exception handling logic mid-sequence.

This is actually a pretty cool idea, but I'm not a fan of "special case" code like this, so I didn't end up trying it.

The Final Solution

The direction I ended up taking was to simply add an optional third parameter to the ActivityCommand constructor so that the developer can specify a "completed" action. Something like this:

GetUnicornsCommand = processor.CreateCommand(GetUnicorns, CanGetUnicorns, GetUnicornsCompleted);

So then our GetUnicorns method remains the same, but we have a new method called GetUnicornsCompleted which looks like this:

void GetUnicornsCompleted(ActivityContext context)
{
    if (context.Error != null)
    {
        // log the error, maybe notify the user

        // clean up
        new BusyIndicator(false).Execute(context);
    }
}

So the parameter type would be Action<ActivityContext> - that way the method would be able to interrogate the context to determine whether an error had occurred, or if the process had been cancelled. It also gives that method the ability to execute activities on demand, by passing the context directly into the activity's Execute method.

At the same time, I've also added a new, read-only property to ActivityContext called LastActivity. You can use that to determine what last thing that happened before the exception was raised.

If you need to differentiate between exceptions returned as part of an activity's Completed event versus those that were thrown unexpectedly and unhandled, you can test the type of the context.Error property. If an exception is returned by an activity it will be wrapped in an ActivityException instance, with the InnerException property set to the original exception and the Activity property set to the activity that returned the error. Thanks to Joshua McKinney for this idea!

.net wpf mvvm coroutines
Posted by: Matt Hamilton
Last revised: 15 Oct, 2024 08:56 PM History