Caliburn.Micro is a lightweight implementation of most core Caliburn feaures in a small assembly. Rob did a great work keeping footprint small, but something was necessarily left over.
One of the features missing in Caliburn.Micro is filters, a set of action decorations aimed to provide additional behaviors to a particular action. Filters bring two major advantages:
- they help to keep View Model free from noisy code not concerning its main purpose (thus simplifying its unit testing, too);
- they allow to share the implementation of cross-cutting concerns between different View Models, moving it into infrastructure components.
Since I use filters quite often, I wanted to provide an implementation for Caliburn.Micro, too.
I could have ported it from Caliburn just fixing some slightly changed signature; yet, in the spirit of keeping things small and simple, I decided to use a less fine-grained design. Plus, I wanted to proof a design idea going through my mind from a while.
Basically, I noted a similarity in the hook point provided by Caliburn infrastructure to filters and IResults (coroutines), so I would check if the two concepts could be unified; this would have allowed to include filters in Caliburn.Micro with little additional infrastructure.
Design
Filters falls into two main categories, depending on the mechanism used to interact with the target action: IExecutionWrapper and IContextAware.
The common IFilter interface is little more than a marker and just defines a Priority property aimed to trim the filter application order:
public interface IFilter {
int Priority { get; }
}
IExecutionWrapper
Most filters capability are built around AOP concepts and works through the interception of an action execution: doing this allows to add operation before and after the action execution itself or something more complex like dispatching the action in another thread.
Coroutine infrastructure already has this capability, so a filter willing to intercept an action can simply wrap the original execution into a “wrapping” IResult:
public interface IExecutionWrapper : IFilter {
IResult Wrap(IResult inner);
}
To enable filter hooking, I had to replace the core invocation method of ActionMessage:
//in bootstrapper code:
ActionMessage.InvokeAction = FilterFrameworkCoreCustomization.InvokeAction;
...
public static class FilterFrameworkCoreCustomization
{
...
public static void InvokeAction(ActionExecutionContext context)
{
var values = MessageBinder.DetermineParameters(context, context.Method.GetParameters());
IResult result = new ExecuteActionResult(values);
var wrappers = FilterManager.GetFiltersFor(context).OfType<IExecutionWrapper>();
var pipeline = result.WrapWith(wrappers);
//if pipeline has error, action execution should throw!
pipeline.Completed += (o, e) =>
{
Execute.OnUIThread(() =>
{
if (e.Error != null) throw new Exception(
string.Format("An error occurred while executing {0}", context.Message),
e.Error
);
});
};
pipeline.Execute(context);
}
...
}
Every action is actually executed within an ExecuteActionResult (code omitted here) that deals with simple action as well as coroutines, uniforming all of them to a common IResult interface.
This “core” IResult is afterwards wrapped over and over by each filter attached to the action and finally executed.
Let’s have a look at FilterManager:
public static class FilterManager
{
public static IResult WrapWith(this IResult inner, IEnumerable<IExecutionWrapper> wrappers)
{
IResult previous = inner;
foreach (var wrapper in wrappers)
{
previous = wrapper.Wrap(previous);
}
return previous;
}
public static Func<ActionExecutionContext, IEnumerable<IFilter>> GetFiltersFor = (context) => {
return context.Target.GetType().GetAttributes<IFilter>(true)
.Union(context.Method.GetAttributes<IFilter>(true))
.OrderBy(x => x.Priority);
};
}
Note that the GetFiltersFor method is replaceable to allow for another filter lookup strategy (for example, based on convention or external configuration instead of attributes).
IContextAware
While IExecutionWrapper-s does their work during the action execution, the other filter category, IContextAware, operates when action is not executing, providing preconditions for execution (the related predicate is held by ActionExecutionContext) or observing the ViewModel to force an update of the action availability:
public interface IContextAware : IFilter, IDisposable
{
void MakeAwareOf(ActionExecutionContext context);
}
Filters implementing this interface are given a chance, during ActionMessage initialization, to hook the execution context; to achieve this, I had to slightly tweak the ActionMessage again:
//in bootstrapper code:
var oldPrepareContext = ActionMessage.PrepareContext;
ActionMessage.PrepareContext = context =>
{
oldPrepareContext(context);
FilterFrameworkCoreCustomization.PrepareContext(context);
};
...
public static class FilterFrameworkCoreCustomization
{
...
public static void PrepareContext(ActionExecutionContext context)
{
var contextAwareFilters = FilterManager.GetFiltersFor(context).OfType<IContextAware>()
.ToArray();
contextAwareFilters.Apply(x => x.MakeAwareOf(context));
context.Message.Detaching += (o, e) =>
{
contextAwareFilters.Apply(x => x.Dispose());
};
}
...
}
Implementing filters
To simplify filters construction, I made a base class for IExecutionWrapper which includes all the boilerplate code and provides some standard customization point:
public abstract class ExecutionWrapperBase : Attribute, IExecutionWrapper, IResult
{
public int Priority { get; set; }
/// <summary>
/// Check prerequisites
/// </summary>
protected virtual bool CanExecute(ActionExecutionContext context) { return true;}
/// <summary>
/// Called just before execution (if prerequisites are met)
/// </summary>
protected virtual void BeforeExecute(ActionExecutionContext context) { }
/// <summary>
/// Called after execution (if prerequisites are met)
/// </summary>
protected virtual void AfterExecute(ActionExecutionContext context) { }
/// <summary>
/// Allows to customize the dispatch of the execution
/// </summary>
protected virtual void Execute(IResult inner, ActionExecutionContext context)
{
inner.Execute(context);
}
/// <summary>
/// Called when an exception was thrown during the action execution
/// </summary>
protected virtual bool HandleException(ActionExecutionContext context, Exception ex) { return false; }
IResult _inner;
IResult IExecutionWrapper.Wrap(IResult inner)
{
_inner = inner;
return this;
}
void IResult.Execute(ActionExecutionContext context)
{
if (!CanExecute(context))
{
_completedEvent.Invoke(this, new ResultCompletionEventArgs { WasCancelled = true });
return;
}
try
{
EventHandler<ResultCompletionEventArgs> onCompletion = null;
onCompletion = (o, e) =>
{
_inner.Completed -= onCompletion;
AfterExecute(context);
FinalizeExecution(context, e.WasCancelled, e.Error);
};
_inner.Completed += onCompletion;
BeforeExecute(context);
Execute(_inner, context);
}
catch (Exception ex)
{
FinalizeExecution(context, false, ex);
}
}
void FinalizeExecution(ActionExecutionContext context, bool wasCancelled, Exception ex)
{
if (ex != null && HandleException(context, ex))
ex = null;
_completedEvent.Invoke(this, new ResultCompletionEventArgs { WasCancelled = wasCancelled, Error = ex });
}
event EventHandler<ResultCompletionEventArgs> _completedEvent = delegate { };
event EventHandler<ResultCompletionEventArgs> IResult.Completed
{
add { _completedEvent += value; }
remove { _completedEvent -= value; }
}
}
Finally I could reproduce the behavior of some well known Caliburn filters in Caliburn.Micro:
/// <summary>
/// Provides asynchronous execution of the action in a background thread
/// </summary>
public class AsyncAttribute : ExecutionWrapperBase
{
protected override void Execute(IResult inner, ActionExecutionContext context)
{
ThreadPool.QueueUserWorkItem(state =>
{
inner.Execute(context);
});
}
}
//usage:
//[Async]
//public void MyAction() { ... }
/// <summary>
/// Allows to specify a "rescue" method to handle exception occurred during execution
/// </summary>
public class RescueAttribute : ExecutionWrapperBase
{
public RescueAttribute() : this("Rescue") { }
public RescueAttribute(string methodName)
{
MethodName = methodName;
}
public string MethodName { get; private set; }
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;
else
return true;
}
catch
{
return false;
}
}
}
//usage:
//[Rescue]
//public void ThrowingAction()
//{
// throw new NotImplementedException();
//}
//public bool Rescue(Exception ex)
//{
// MessageBox.Show(ex.ToString());
// return true;
//}
/// <summary>
/// Sets "IsBusy" property to true (on models implementing ICanBeBusy) during the execution
/// </summary>
public class SetBusyAttribute : ExecutionWrapperBase
{
protected override void BeforeExecute(ActionExecutionContext context)
{
SetBusy(context.Target as ICanBeBusy, true);
}
protected override void AfterExecute(ActionExecutionContext context)
{
SetBusy(context.Target as ICanBeBusy, false);
}
protected override bool HandleException(ActionExecutionContext context, Exception ex)
{
SetBusy(context.Target as ICanBeBusy, false);
return false;
}
private void SetBusy(ICanBeBusy model, bool isBusy)
{
if (model != null)
model.IsBusy = isBusy;
}
}
//usage:
//[SetBusy]
//[Async] //prevents UI freezing, thus allowing busy state representation
//public void VeryLongAction() { ... }
/// <summary>
/// Updates the availability of the action (thus updating the UI)
/// </summary>
public class DependenciesAttribute : Attribute, IContextAware
{
ActionExecutionContext _context;
INotifyPropertyChanged _inpc;
public DependenciesAttribute(params string[] propertyNames)
{
PropertyNames = propertyNames ?? new string[] { };
}
public string[] PropertyNames { get; private set; }
public int Priority { get; set; }
public void MakeAwareOf(ActionExecutionContext context)
{
_context = context;
_inpc = context.Target as INotifyPropertyChanged;
if (_inpc != null)
_inpc.PropertyChanged += inpc_PropertyChanged;
}
public void Dispose()
{
if (_inpc != null)
_inpc.PropertyChanged -= inpc_PropertyChanged;
_inpc = null;
}
void inpc_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (PropertyNames.Contains(e.PropertyName))
{
Execute.OnUIThread(() =>
{
_context.Message.UpdateAvailability();
});
}
}
}
//usage:
//[Dependencies("MyProperty", "MyOtherProperty")]
//public void DoAction() { ... }
//public bool CanDoAction() { return MyProperty > 0 && MyOtherProperty < 1; }
/// <summary>
/// Allows to specify a guard method or property with an arbitrary name
/// </summary>
public class PreviewAttribute : Attribute, IContextAware
{
public PreviewAttribute(string methodName)
{
MethodName = methodName;
}
public string MethodName { get; private set; }
public int Priority { get; set; }
public void MakeAwareOf(ActionExecutionContext context)
{
var targetType = context.Target.GetType();
var guard = targetType.GetMethod(MethodName);
if (guard== null)
guard = targetType.GetMethod("get_" + MethodName);
if (guard == null) return;
var oldCanExecute = context.CanExecute;
context.CanExecute = () =>
{
if (!oldCanExecute()) return false;
return (bool)guard.Invoke(
context.Target,
MessageBinder.DetermineParameters(context, guard.GetParameters())
);
};
}
public void Dispose() { }
}
//usage:
//[Preview("IsMyActionAvailable")]
//public void MyAction(int value) { ... }
//public bool IsMyActionAvailable(int value) { ... }
Source code is here: https://hg01.codeplex.com/forks/marcoamendola/caliburnmicromarcoamendolafork