Quantcast
Channel: Caliburn.Micro: Xaml Made Easy
Viewing all articles
Browse latest Browse all 1760

Commented Unassigned: Action guard method re-evaluation [312]

$
0
0
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: Using PropertyChanged notifications can lead to unnecessary work from the UI engine, since such notifications are monitored to update binding on the UI, potentially triggering measure/arrange/render passes. In my opinion, it is better to let PropertyChanged notifications out of the picture, since they have specific implications. Regarding Foody, I don't really knew the project until now, but as far as I can see, it is something like PostSharp or projects alike (I could be wrong, but since it embeds a MSBuild task...). If I am correct, this kind of approach can fail when obfuscation is used, if generated code still pushes hard-coded strings into INP notifications. Using expressions (resolved at runtime) instead, avoids this (and that's why I think it's a better way to handle such notifications). The real issue is with INPC design itself, I am afraid... Regarding collections, you are right: the default CM implementation only reacts to updates to parameter values (i.e. whenever the Parameter.Value property changes), but in certain situations the developer would prefer that the avilability is triggered if a collection used as an action parameter changes. I faced such an issue, and I could solve the problem extending the Parameter class to provide such functionality: ``` C# namespace Caliburn.Micro.Sample { #region Namespaces using System; using System.Collections.Specialized; using System.Reflection; using System.Windows; using Caliburn.Micro; #endregion /// <summary> /// Class used to define an advanced action message <see cref="Parameter" />, able to react to collection changes. /// </summary> public class AdvancedParameter : Parameter, IWeakEventListener { /// <summary> /// Initializes the <see cref="AdvancedParameter" /> class. /// </summary> static AdvancedParameter() { ValueProperty.OverrideMetadata(typeof(AdvancedParameter), new PropertyMetadata(OnValuePropertyChanged)); } #region Static Methods /// <summary> /// Called when the value property changes. /// </summary> /// <param name="d">The dependency object.</param> /// <param name="e"> /// The <see cref="System.Windows.DependencyPropertyChangedEventArgs" /> instance containing the event data. /// </param> private static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var parameter = (AdvancedParameter)d; var oldValue = e.OldValue as INotifyCollectionChanged; if (oldValue != null) CollectionChangedEventManager.RemoveListener(oldValue, parameter); var newValue = e.NewValue as INotifyCollectionChanged; if (newValue != null) CollectionChangedEventManager.AddListener(newValue, parameter); var owner = parameter.GetOwner(); if (owner != null) owner.UpdateAvailability(); } #endregion #region IWeakEventListener Members /// <summary> /// Receives events from the centralized event manager. /// </summary> /// <param name="managerType"> /// The type of the <see cref="T:System.Windows.WeakEventManager" /> calling this method. /// </param> /// <param name="sender">Object that originated the event.</param> /// <param name="e">Event data.</param> /// <returns> /// true if the listener handled the event. It is considered an error by the /// <see /// cref="T:System.Windows.WeakEventManager" /> /// handling in WPF to register a listener for an event that the listener does not handle. Regardless, the method /// should return false if it receives an event that it does not recognize or handle. /// </returns> bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) { if (managerType == typeof(CollectionChangedEventManager)) { var owner = GetOwner(); if (owner != null) owner.UpdateAvailability(); return true; } return false; } #endregion /// <summary> /// Gets the action message owner. /// </summary> /// <returns>The action message owner.</returns> private ActionMessage GetOwner() { return (ActionMessage)typeof(Parameter).GetProperty("Owner", BindingFlags.Instance | BindingFlags.NonPublic) .GetValue(this, null); } } } ``` (Note: the reflection used to get the Parameter Owner will not be necessary with CM 2.0, since the accessibility has been changed to protected in the latest code-base). In conjuntion with some other extensions, I am able to use a binding in short syntax, and write something like this ``` XML <Button Message.Attach="[Event Click] = [Action AddItems({Binding ElementName=someListBox, Path=SelectedItems})]"/> ``` instead of using the long syntax ``` XML <Button> <i:Interation.Triggers> <i:EventTrigger EventName="Click"> <cal:ActionMessage MethodName="AddItems"> <ext:AdvancedParameter Value="{Binding ElementName=someListBox, Path=SelectedItems}"/> </cal:ActionMessage> </i:EventTrigger> </i:Interation.Triggers> </Button> ``` The above AdvancedParameter class will work even in your case. Note that the above class does not cover all the cases where action guard re-evaluation is needed, since it only covers updates from the parameter point of view when a collection is used, while action method re-evaluation can be triggered on internal state changes.

Viewing all articles
Browse latest Browse all 1760

Latest Images

Trending Articles



Latest Images

<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>