I wanted to get opinions from the dev's on items like below see https://caliburnmicro.codeplex.com/discussions/442668 for original source.
On the one hand something like this could be integrated into Caliburn.Micro proper, on the other hand it could make more sense for the original author to package it up as an recipe.
Thoughts?
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:
``` C#
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](http://www.mediafire.com/?irqiiq9bvbn41x4).
Comments: Is it completely wrong to implement this notification via the usual INotifyPropertyChanged? BladeWise's implementation above use same call to __ActionMessage.UpdateAvailability()__ method. I mean, new interface, like tibel suggested, would keep this clean, but would require more plumbing code to get this work (and what about Screen/Conductors? They would need to be altered to implement this interface, otherwise there's the plumbing code issue). Also, hard-coded strings should no longer be used - I think people should use either __NotifyOfPropertyChange<TProperty>__ or assembly weaving via Fody (or other library) and if that doesn't cover all scenarios of property notification, then they should use __NotifyOfPropertyChange<TProperty>__. So for guard methods a nice extension method for __INotifyPropertyChanged__ interface could be created (or for __PropertyChangedBase__ if we don't want to pollute every INPC object with this method). And this way, there's no need for additional notification event (when __INotifyPropertyChanged.PropertyChanged__ event is readily available). Method overloads are another beast, though - but as far as I remember from browsing through CM's code, they don't have much support apart from parameter count (supporting only this in guard method re-evaluation should be relatively simple). Small note: > raising a property changed for a parameter is a possible workaround, but feels quite dirty Works, but only for VM properties. I have __BindableCollection<T>__ in VM and I consume it via ItemsControl in view - as you can see this workaround won't work in such case (or maybe it could if collection item was INPC object, I'm not sure - but in my case it isn't).
On the one hand something like this could be integrated into Caliburn.Micro proper, on the other hand it could make more sense for the original author to package it up as an recipe.
Thoughts?
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:
``` C#
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](http://www.mediafire.com/?irqiiq9bvbn41x4).
Comments: Is it completely wrong to implement this notification via the usual INotifyPropertyChanged? BladeWise's implementation above use same call to __ActionMessage.UpdateAvailability()__ method. I mean, new interface, like tibel suggested, would keep this clean, but would require more plumbing code to get this work (and what about Screen/Conductors? They would need to be altered to implement this interface, otherwise there's the plumbing code issue). Also, hard-coded strings should no longer be used - I think people should use either __NotifyOfPropertyChange<TProperty>__ or assembly weaving via Fody (or other library) and if that doesn't cover all scenarios of property notification, then they should use __NotifyOfPropertyChange<TProperty>__. So for guard methods a nice extension method for __INotifyPropertyChanged__ interface could be created (or for __PropertyChangedBase__ if we don't want to pollute every INPC object with this method). And this way, there's no need for additional notification event (when __INotifyPropertyChanged.PropertyChanged__ event is readily available). Method overloads are another beast, though - but as far as I remember from browsing through CM's code, they don't have much support apart from parameter count (supporting only this in guard method re-evaluation should be relatively simple). Small note: > raising a property changed for a parameter is a possible workaround, but feels quite dirty Works, but only for VM properties. I have __BindableCollection<T>__ in VM and I consume it via ItemsControl in view - as you can see this workaround won't work in such case (or maybe it could if collection item was INPC object, I'm not sure - but in my case it isn't).