Recovering from Exception in IResult

Feb 2, 2011 at 10:03 PM

Say I have the following Action

 

public IEnumerable<IResult> SaveStation()
{ 
     yield return Busy.MakeBusy();
     yield return new StationSave(_station);
     yield return Busy.MakeNotBusy();
     yield return Show.Tab<StationBrowseViewModel>();
}

 

StationSave is an IResult wrapper around a simple (WCF) service invocation. The service uses FaultContract/FaultException<T> for failures.

In the case of a fault, the user needs to be notified and the FaultContract will contain some useful info about what went wrong. Currently the Save result catches the exception and inserts it into ResultCompletionEventArgs of the Completed event. By doing so, the SequentialResult created by the pipeline is cancelled (because of the error), thus leaving the screen in a Busy state.

What I'm really after is ideas about the best way to recover from the error (remove the busy state) and notify the user (I have a couple of IResult implementations for different styles of notification which I would like to use) of the details provided in the fault contract. By attaching to the Completed event in my VM I can get the error, but at this point I am no longer in the context of the Action pipeline so any IResults I would like to use (the MakeNotBusy and my show notification implementation) I have to execute manually (and I'd have to new up my own ActionExecutionContext which I don't want to be doing).

I have had a look at Marco Amendola's rescue filter for Caliburn.Micro from here, but again I can't pass back IResults from the Rescue method.

Have I missed something obvious? How do others handle this situation?

Coordinator
Feb 3, 2011 at 2:42 AM

This is all a little less than ideal until we get real async methods in C#5. But, as a simple solution, you could not report the exception in completed event, but make it a property on the IResult which you could check and handle within the action itself. So, you could do something like this:

public IEnumerable<IResult> SaveStation()
{ 
     yield return Busy.MakeBusy();

     var save = new StationSave(_station);
     yield return save; 

     yield return Busy.MakeNotBusy();

     if(save.HasError)
        yield return Show.MessageBox(save.Exception.Message);
     else yield return Show.Tab<StationBrowseViewModel>();
}

You could also alter Marco's filter plugin such that the filters could return IEnumerable<IResult>, but the above recommendation is probably simpler.

Feb 3, 2011 at 9:32 AM

As an intermediate option, you might also consider customizing the action invocation, attempting to call a rescue method on the VM:

        public static class ActionRescue
	{
		public static void Initialize()
		{

			ActionMessage.InvokeAction = context =>
			{
				var values = MessageBinder.DetermineParameters(context, context.Method.GetParameters());
				object returnValue = null;
				try
				{
					returnValue = context.Method.Invoke(context.Target, values);
				}
				catch (Exception ex)
				{
					if (!TryInvokeRescue(context.Target, ex)) throw;
				}


				if (returnValue is IResult)
					returnValue = new[] { returnValue as IResult };


				EventHandler<ResultCompletionEventArgs> completionHandler = null;
				completionHandler = (o, e) =>
				{
					if (e.Error != null)
						TryInvokeRescue(context.Target, e.Error);

					Coroutine.Completed -= completionHandler;
				};


				Coroutine.Completed += completionHandler;


				if (returnValue is IEnumerable<IResult>)
					Coroutine.BeginExecute(((IEnumerable<IResult>)returnValue).GetEnumerator(), context);
				else if (returnValue is IEnumerator<IResult>)
					Coroutine.BeginExecute(((IEnumerator<IResult>)returnValue), context);

			};
		}

		static bool TryInvokeRescue(object target, Exception ex)
		{
			var rescue = target.GetType().GetMethod("Rescue");
			if (rescue == null) return false;

			return (bool)rescue.Invoke(target, new object[] { ex });
		}
	}

 

You have to "hook" the customization in the bootstrapper:

 

      public class AppBootstrapper {
               
	        protected override void Configure()
		{
			...
			ActionRescue.Initialize();
                        ...
		}

 

 

Then you could write something like this:

public class MyViewModel {

	public bool Rescue(Exception ex) {
		//handle exception
		return true;
	}
	public IEnumerable<IResult> SaveStation()
	{ 
		 yield return Busy.MakeBusy();
		 yield return new StationSave(_station);
		 yield return Busy.MakeNotBusy();
		 yield return Show.Tab<StationBrowseViewModel>();
	}

}

Feb 3, 2011 at 8:06 PM

Thank you both Rob and Marco, you've put me in the right direction. I'll have a fiddle and see what I like best and will post results back here.

Feb 9, 2011 at 2:29 AM
Edited Feb 9, 2011 at 2:45 AM

For now I have just chosen to modify Marcos RescueAttribute filter to allow execution of further IResults. For this, the HandleException method of RescueAttribute becomes something like the following:

protected override bool HandleException(ActionExecutionContext context, Exception ex)
{
	var method = context.Target.GetType().GetMethod(MethodName, new[] { typeof(Exception) });
	if (method == null) return false;

	try
	{
		var result = method.Invoke(context.Target, new object[] { ex });

		if (result is bool)
			return (bool) result;
		
                if (result is IResult)
                        result = new[] { result as IResult };
                if (result is IEnumerable<IResult>)
                        Coroutine.Execute(((IEnumerable<IResult>) result).GetEnumerator(), context);
                else if (result is IEnumerator<IResult>)
                        Coroutine.Execute(((IEnumerator<IResult>) result), context);
                return true;
 	}
  	catch
        {
 		return false;
 	}
 }


 

Feb 24, 2011 at 2:08 PM

What if SaveStation was actioned by SequentialResult in, say, OnActivate?

Feb 24, 2011 at 5:53 PM

Of course:

SequentialResult sr = new SequentialResult(...);
sr.OnComplete += (s, e) => {
  if(e.Error != null)
    Rescue(e.Error);
};

sr.Execute(...);