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:
The activity will raise the
Completed
event with theActivityCompletedEventArgs.Error
property set to anAnimalNotFoundException
instance, and the processor will stop running the sequence.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. Right now there is no 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, "dumb" approach to MVVM.
Right now there are a few ways I can approach this problem:
Use a Special Method
This is the direction I'm leaning right now. 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.
This is a pretty tidy solution. The only drawback that I can see is that there's nothing in the context to tell you which activity ran last. That's something I could add without too many problems, though, I think.
Throw the Exception (Not an Option)
Update This is not an option, because the above code doesn't actually compile. You can't yield return
from inside a try
or catch
block! You're a smart coder! Help me out! Leave me a comment!
Firstly, I could change the ActivityProcessor
so it throws any exception returned as part of an activity's Completed
event. Then you would have to change your GetUnicorns
method to look more like this:
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
}
}
This is a bit more work on the part of the developer, but it's probably more predictable in that exceptions are never "swallowed" by the ActivityProcessor
.
Continue Execution
Secondly, I could change 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
}
Again, the developer would have to know that an activity might throw (or return) an exception, but at least he'd find out about it in a way that would let the execution continue if he so desired.
Ignore It
One approach is simply to leave it be. If you're writing your own activities, you should know whether they might throw an exception, and write the exception handling code into the activity. If you think the process should continue when an exception is raised, store the exception in a property (similar to the "continue execution" option above) and let the caller deal with it as they see fit.
Use a Special Activity
This option just occurred to me: 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.
Your Thoughts?
So, should we just throw the exception no matter what? Or should we continue execution and let the developer know via an Error
property on the IActivity
interface? Let me know which option you prefer, or if you have a better idea!
Last revised: 05 Jan, 2011 09:28 PM History