Action guard method re-evaluation

Topics: Actions & Coroutines, Extensibility
May 6, 2013 at 12:15 PM
Every now and then, there is a new user that asks how to trigger availability update for action guards implemented as methods. I am aware that such methods are re-evaluated every time a parameter changes, but there are some cases where the evaluation of a guard depends on both the parameters and the internal state of the class providing the action. In such a scenario, it can be useful to have a way to forcefully request an availability update on the action.

I decided to provide a possible solution, involving a simple naming convention and the use of specific events: consider and action called 'Execute(...)' and the associated method guard 'CanExecute(...)'; my idea is to modify the PrepareContext implementation to check for the existence of a specific event, called 'ReEvaluateCanExecute' and, if available, attach to it and invoke UpdateAvailability whenever the event is invoked.
Since PrepareContext is an extensibility point, this feature can be easily added, without modifying the current CM code base.

The actual implementation is provided below:
namespace ActionGuardSample
{
    #region Namespaces
    using System;
    using System.ComponentModel;
    using System.Linq;
    using System.Reflection;
    using Caliburn.Micro;
    using Action = System.Action;

    #endregion

    /// <summary>
    ///     Static class used to provide Caliburn Micro extensions.
    /// </summary>
    public static class CaliburnMicroExtensions
    {
        #region Static Methods
        /// <summary>
        ///     Prepares the context.
        /// </summary>
        /// <param name="context">The context.</param>
        private static void PrepareContext(ActionExecutionContext context)
        {
            ActionMessage.SetMethodBinding(context);
            if (context.Target != null && context.Method != null)
            {
                var targetType = context.Target.GetType();
                var guardName = string.Format("Can{0}", context.Method.Name);
                var guard = TryFindGuardMethod(context);
                if (guard == null)
                {
                    var inpc = context.Target as INotifyPropertyChanged;
                    if (inpc == null)
                        return;
                    guard = targetType.GetMethod(string.Format("get_{0}", guardName));
                    if (guard == null)
                        return;
                    var handler = (PropertyChangedEventHandler)null;
                    handler = ((s, e) =>
                               {
                                   if (string.IsNullOrEmpty(e.PropertyName) || e.PropertyName == guardName)
                                   {
                                       ((Action)(() =>
                                                 {
                                                     var message = context.Message;
                                                     if (message == null)
                                                         inpc.PropertyChanged -= handler;
                                                     else
                                                         message.UpdateAvailability();
                                                 })).OnUIThread();
                                   }
                               });
                    inpc.PropertyChanged += handler;
                    context.Disposing += (s, e) => inpc.PropertyChanged -= handler;
                    context.Message.Detaching += (s, e) => inpc.PropertyChanged -= handler;
                    context.CanExecute = () => (bool)guard.Invoke(context.Target, MessageBinder.DetermineParameters(context, guard.GetParameters()));
                }
                else
                {
                    var updateEventName = string.Format("ReEvaluate{0}", guardName);
                    var updateEvent = targetType.GetEvent(updateEventName);
                    if (updateEvent != null)
                    {
                        var target = context.Target;
                        EventHandler handler = null;
                        handler = (s, e) => ((Action)(() =>
                                                      {
                                                          var message = context.Message;
                                                          if (message == null)
                                                              updateEvent.RemoveEventHandler(target, handler);
                                                          else
                                                              message.UpdateAvailability();
                                                      })).OnUIThread();
                        updateEvent.AddEventHandler(target, handler);
                        context.Disposing += (s, e) => updateEvent.RemoveEventHandler(target, handler);
                        context.Message.Detaching += (s, e) => updateEvent.RemoveEventHandler(target, handler);
                    }

                    context.CanExecute = () => (bool)guard.Invoke(context.Target, MessageBinder.DetermineParameters(context, guard.GetParameters()));
                }
            }
        }

        /// <summary>
        ///     Tries to find the guard method.
        /// </summary>
        /// <param name="context">The context.</param>
        /// <returns>The guard method.</returns>
        private static MethodInfo TryFindGuardMethod(ActionExecutionContext context)
        {
            var name = string.Format("Can{0}", context.Method.Name);
            var method = context.Target.GetType().GetMethod(name);
            if (method == null)
                return null;
            if (method.ContainsGenericParameters)
                return null;
            if (typeof(bool) != method.ReturnType)
                return null;
            var methodParameters = method.GetParameters();
            var contextMethodParameters = context.Method.GetParameters();
            if (methodParameters.Length == 0)
                return method;
            if (methodParameters.Length != contextMethodParameters.Length)
                return null;
            return methodParameters.Zip(contextMethodParameters, (x, y) => x.ParameterType == y.ParameterType).Any(x => !x) ? null : method;
        }

        /// <summary>
        ///     Enables support for action guard methods re-evaluation, through a specific event naming convention.
        /// </summary>
        public static void EnableActionGuardMethodReEvaluateSupport()
        {
            ActionMessage.PrepareContext = PrepareContext;
        }
        #endregion
    }
}
You can download a working sample here.
May 12, 2013 at 11:49 AM
This discussion has been copied to a work item. Click here to go to the work item and continue the discussion.