Tamir Dresher

Catching exceptions from Caliburn.Micro ActionMessage invocation

27 Jan 2015

This post was originally published on 27 Jan 2015 at http://blogs.microsoft.co.il/iblogger/2015/01/27/catching-exceptions-from-caliburn-micro-actionmessage-invocation/

Caliburn.Micro provides many extensions to make our life easier. One of them is the ability to handle UI events and trigger a method call in the ViewModel.
It is very similar to what you get when using EventToCommand (https://msdn.microsoft.com/en-us/magazine/dn237302.aspx) but caliburn makes it so much simpler.

For instance, lets say I want to open the user profile when Tapping on an element somewhere in my Grid, I just write it like this:

1
2
3
4
5
6
<Grid x:Name="GroupMemberItem"
              micro:Message.Attach="[Event Tapped] = [Action OpenProfile($dataContext)]">
 
        <!-- The rest of the grid elemens -->
 
</Grid>
1
  

the $dataContext token you see is a very powerful Caliburn.Micro feature that allows us to pass the Grids’ DataContext to the OpenProfile method. you can read on the more advanced options in the documentation page: http://caliburnmicro.com/documentation/actions

What do you think will happen if OpenProfile method throws an exception?

If OpenProfile throws an exception then the entire application will crash. unless of course you added a event-handler to your AppDomain and caught the exception there. 

Lets say we want a specific handling to exceptions that are raised inside a Caliburn.Micro call.

When we wrote

1
micro:Message.Attach="[Event Tapped] = [Action OpenProfile($dataContext)]

Caliburn created an instance of ActionMessage which handles the parsing and invocation of the method when needed.

lets create a SilentActionMessage that will catch the exception if it was raised and will call some error handler that we will define (usually write to log)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/// <summary>
/// This ActionMessage prevent an exception to proppgate in the case that the method defined in the XAML doesnt exist
/// </summary>
public class SilentActionMessage: ActionMessage
{
    private readonly Action<Exception> _errorAction;
 
    public SilentActionMessage()
    {
         
    }
    public SilentActionMessage(Action<Exception> errorAction )
    {
        _errorAction = errorAction;
    }
 
    protected override void Invoke(object eventArgs)
    {
        try
        {
 
            base.Invoke(eventArgs);
             
        }
        catch(Exception e)
        {
            var ancestors = this.AssociatedObject.GetVisualAncestors();
            if (_errorAction!=null)
            {
                var ancestorsList = ancestors.Aggregate("", (s, ancestor) => s += "," + ancestor.ToString());
                var fullEx =
                    new InvalidOperationException(
                        string.Format("couldnt invoke the Action {0} , Trail to the SilentActionMessage:{1}",
                            this.MethodName, ancestorsList), e);
                
                _errorAction(fullEx);
            }
        }
    }
}

in the code, I’m calling the regular ActionMessage.Invoke method and if there is an exception I call some errorAction with an exception that also shows the trail to the “evil” UI element.

 

in order to attach to Caliburn.Micro I’m adding these lines to my ApplicationBootstrapper

1
2
3
4
5
Parser.InterpretMessageText = (target, text) =>
    new SilentActionMessage(ex => _logger.ErrorException("Exception in SilentActionMessage target:" + target, ex))
    {
        MethodName = Regex.Replace(text, "^Action", string.Empty).Trim()
    };

this solves my problem almost completely, with one exception – asynchronous methods.

when you call an async method without awaiting you wont be able to catch any exception that are thrown from the method.

Luckily, Caliburn.Micro knows how to handle async method that return Task (it does so by convert it to internal implementation of Coroutine). so in order for us to get the exception that was thrown we need to attach to the Coroutine.Completed event

1
2
3
4
5
6
7
Coroutine.Completed += (sender, args) =>
            {
                if (args.Error != null)
                {
                    _logger.ErrorException("exception while invoking ActionMessage", args.Error);
                }
            };

This works beutifully when the called method return type is a Task.
But, if the method is “async void” you will have a problem.

async void are evil, and I haven’t found any solution other than change all you “async void” methods to “async Task“