namespace Caliburn.Micro {
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
#if WinRT81
using Windows.UI.Xaml;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Markup;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Controls;
using Microsoft.Xaml.Interactivity;
using TriggerBase = Microsoft.Xaml.Interactivity.IBehavior;
using EventTrigger = Microsoft.Xaml.Interactions.Core.EventTriggerBehavior;
#else
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Interactivity;
using System.Windows.Markup;
using System.Windows.Media;
using EventTrigger = System.Windows.Interactivity.EventTrigger;
using Caliburn.Micro.Core;
using System.IO;
using System.Xml;
#endif
///
/// Used to send a message from the UI to a presentation model class, indicating that a particular Action should be invoked.
///
#if WinRT
[ContentProperty(Name = "Parameters")]
#else
[ContentProperty("Parameters")]
[DefaultTrigger(typeof(FrameworkElement), typeof(EventTrigger), "MouseLeftButtonDown")]
[DefaultTrigger(typeof(ButtonBase), typeof(EventTrigger), "Click")]
[TypeConstraint(typeof(FrameworkElement))]
#endif
public class ActionMessage : TriggerAction, IHaveParameters {
static readonly ILog Log = LogManager.GetLog(typeof(ActionMessage));
ActionExecutionContext context;
#if WINDOWS_PHONE
internal Microsoft.Phone.Shell.IApplicationBarMenuItem applicationBarSource;
#endif
internal static readonly DependencyProperty HandlerProperty = DependencyProperty.RegisterAttached(
"Handler",
typeof(object),
typeof(ActionMessage),
new PropertyMetadata(null, HandlerPropertyChanged)
);
///
/// Causes the action invocation to "double check" if the action should be invoked by executing the guard immediately before hand.
///
/// This is disabled by default. If multiple actions are attached to the same element, you may want to enable this so that each individaul action checks its guard regardless of how the UI state appears.
public static bool EnforceGuardsDuringInvocation = false;
///
/// Causes the action to throw if it cannot locate the target or the method at invocation time.
///
/// True by default.
public static bool ThrowsExceptions = true;
///
/// Represents the method name of an action message.
///
public static readonly DependencyProperty MethodNameProperty =
DependencyProperty.Register(
"MethodName",
typeof(string),
typeof(ActionMessage),
null
);
///
/// Represents the parameters of an action message.
///
public static readonly DependencyProperty ParametersProperty =
DependencyProperty.Register(
"Parameters",
typeof(AttachedCollection),
typeof(ActionMessage),
null
);
///
/// Creates an instance of .
///
public ActionMessage() {
SetValue(ParametersProperty, new AttachedCollection());
}
///
/// Gets or sets the name of the method to be invoked on the presentation model class.
///
/// The name of the method.
#if !WinRT
[Category("Common Properties")]
#endif
public string MethodName {
get { return (string)GetValue(MethodNameProperty); }
set { SetValue(MethodNameProperty, value); }
}
///
/// Gets the parameters to pass as part of the method invocation.
///
/// The parameters.
#if !WinRT
[Category("Common Properties")]
#endif
public AttachedCollection Parameters {
get { return (AttachedCollection)GetValue(ParametersProperty); }
}
///
/// Occurs before the message detaches from the associated object.
///
public event EventHandler Detaching = delegate { };
///
/// Called after the action is attached to an AssociatedObject.
///
#if WinRT81
protected override void OnAttached() {
if (!View.InDesignMode) {
Parameters.Attach(AssociatedObject);
Parameters.OfType().Apply(x => x.MakeAwareOf(this));
if (View.ExecuteOnLoad(AssociatedObject, ElementLoaded)) {
// Not yet sure if this will be needed
//var trigger = Interaction.GetTriggers(AssociatedObject)
// .FirstOrDefault(t => t.Actions.Contains(this)) as EventTrigger;
//if (trigger != null && trigger.EventName == "Loaded")
// Invoke(new RoutedEventArgs());
}
View.ExecuteOnUnload(AssociatedObject, ElementUnloaded);
}
base.OnAttached();
}
void ElementUnloaded(object sender, RoutedEventArgs e)
{
OnDetaching();
}
#else
protected override void OnAttached() {
if (!View.InDesignMode) {
Parameters.Attach(AssociatedObject);
Parameters.Apply(x => x.MakeAwareOf(this));
if (View.ExecuteOnLoad(AssociatedObject, ElementLoaded)) {
var trigger = Interaction.GetTriggers(AssociatedObject)
.FirstOrDefault(t => t.Actions.Contains(this)) as EventTrigger;
if (trigger != null && trigger.EventName == "Loaded")
Invoke(new RoutedEventArgs());
}
}
base.OnAttached();
}
#endif
static void HandlerPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
((ActionMessage)d).UpdateContext();
}
///
/// Called when the action is being detached from its AssociatedObject, but before it has actually occurred.
///
protected override void OnDetaching() {
if (!View.InDesignMode) {
Detaching(this, EventArgs.Empty);
AssociatedObject.Loaded -= ElementLoaded;
Parameters.Detach();
}
base.OnDetaching();
}
void ElementLoaded(object sender, RoutedEventArgs e) {
UpdateContext();
DependencyObject currentElement;
if (context.View == null) {
currentElement = AssociatedObject;
while (currentElement != null) {
if (Action.HasTargetSet(currentElement))
break;
currentElement = BindingScope.GetVisualParent(currentElement);
}
}
else currentElement = context.View;
#if NET
var binding = new Binding {
Path = new PropertyPath(Message.HandlerProperty),
Source = currentElement
};
#elif WinRT
var binding = new Binding {
Source = currentElement
};
#else
const string bindingText = "";
var binding = (Binding)XamlReader.Load(bindingText);
binding.Source = currentElement;
#endif
BindingOperations.SetBinding(this, HandlerProperty, binding);
}
void UpdateContext() {
if (context != null)
context.Dispose();
context = new ActionExecutionContext {
Message = this,
Source = AssociatedObject
};
PrepareContext(context);
UpdateAvailabilityCore();
}
///
/// Invokes the action.
///
/// The parameter to the action. If the action does not require a parameter, the parameter may be set to a null reference.
protected override void Invoke(object eventArgs) {
Log.Info("Invoking {0}.", this);
if (context == null) {
UpdateContext();
}
if (context.Target == null || context.View == null) {
PrepareContext(context);
if (context.Target == null) {
var ex = new Exception(string.Format("No target found for method {0}.", context.Message.MethodName));
Log.Error(ex);
if (!ThrowsExceptions)
return;
throw ex;
}
if (!UpdateAvailabilityCore()) {
return;
}
}
if (context.Method == null) {
var ex = new Exception(string.Format("Method {0} not found on target of type {1}.", context.Message.MethodName, context.Target.GetType()));
Log.Error(ex);
if (!ThrowsExceptions)
return;
throw ex;
}
context.EventArgs = eventArgs;
if (EnforceGuardsDuringInvocation && context.CanExecute != null && !context.CanExecute()) {
return;
}
InvokeAction(context);
context.EventArgs = null;
}
///
/// Forces an update of the UI's Enabled/Disabled state based on the the preconditions associated with the method.
///
public virtual void UpdateAvailability() {
if (context == null)
return;
if (context.Target == null || context.View == null)
PrepareContext(context);
UpdateAvailabilityCore();
}
bool UpdateAvailabilityCore() {
Log.Info("{0} availability update.", this);
return ApplyAvailabilityEffect(context);
}
///
/// Returns a that represents the current .
///
///
/// A that represents the current .
///
public override string ToString() {
return "Action: " + MethodName;
}
///
/// Invokes the action using the specified
///
public static Action InvokeAction = context => {
var values = MessageBinder.DetermineParameters(context, context.Method.GetParameters());
var returnValue = context.Method.Invoke(context.Target, values);
var task = returnValue as System.Threading.Tasks.Task;
if (task != null) {
returnValue = task.AsResult();
}
var result = returnValue as IResult;
if (result != null) {
returnValue = new[] { result };
}
var enumerable = returnValue as IEnumerable;
if (enumerable != null) {
returnValue = enumerable.GetEnumerator();
}
var enumerator = returnValue as IEnumerator;
if (enumerator != null) {
Coroutine.BeginExecute(enumerator,
new CoroutineExecutionContext
{
Source = context.Source,
View = context.View,
Target = context.Target
});
}
};
///
/// Applies an availability effect, such as IsEnabled, to an element.
///
/// Returns a value indicating whether or not the action is available.
public static Func ApplyAvailabilityEffect = context => {
#if WINDOWS_PHONE
var message = context.Message;
if (message != null && message.applicationBarSource != null) {
if (context.CanExecute != null) {
message.applicationBarSource.IsEnabled = context.CanExecute();
}
return message.applicationBarSource.IsEnabled;
}
#endif
#if SILVERLIGHT || WinRT
var source = context.Source as Control;
#else
var source = context.Source;
#endif
if (source == null) {
return true;
}
#if SILVERLIGHT || WinRT
var hasBinding = ConventionManager.HasBinding(source, Control.IsEnabledProperty);
#else
var hasBinding = ConventionManager.HasBinding(source, UIElement.IsEnabledProperty);
#endif
if (!hasBinding && context.CanExecute != null) {
source.IsEnabled = context.CanExecute();
}
return source.IsEnabled;
};
///
/// Finds the method on the target matching the specified message.
///
/// The target.
/// The message.
/// The matching method, if available.
public static Func GetTargetMethod = (message, target) => {
#if WinRT
return (from method in target.GetType().GetRuntimeMethods()
where method.Name == message.MethodName
let methodParameters = method.GetParameters()
where message.Parameters.Count == methodParameters.Length
select method).FirstOrDefault();
#else
return (from method in target.GetType().GetMethods()
where method.Name == message.MethodName
let methodParameters = method.GetParameters()
where message.Parameters.Count == methodParameters.Length
select method).FirstOrDefault();
#endif
};
///
/// Sets the target, method and view on the context. Uses a bubbling strategy by default.
///
public static Action SetMethodBinding = context => {
var source = context.Source;
DependencyObject currentElement = source;
while (currentElement != null) {
if (Action.HasTargetSet(currentElement)) {
var target = Message.GetHandler(currentElement);
if (target != null) {
var method = GetTargetMethod(context.Message, target);
if (method != null) {
context.Method = method;
context.Target = target;
context.View = currentElement;
return;
}
}
else {
context.View = currentElement;
return;
}
}
currentElement = BindingScope.GetVisualParent(currentElement);
}
if (source != null && source.DataContext != null) {
var target = source.DataContext;
var method = GetTargetMethod(context.Message, target);
if (method != null) {
context.Target = target;
context.Method = method;
context.View = source;
}
}
};
///
/// Prepares the action execution context for use.
///
public static Action PrepareContext = context => {
SetMethodBinding(context);
if (context.Target == null || context.Method == null)
{
return;
}
var possibleGuardNames = BuildPossibleGuardNames(context.Method).ToList();
var guard = TryFindGuardMethod(context, possibleGuardNames);
if (guard == null)
{
var inpc = context.Target as INotifyPropertyChanged;
if (inpc == null)
return;
var targetType = context.Target.GetType();
string matchingGuardName = null;
foreach (string possibleGuardName in possibleGuardNames)
{
matchingGuardName = possibleGuardName;
guard = GetMethodInfo(targetType, "get_" + matchingGuardName);
if (guard != null) break;
}
if (guard == null)
return;
PropertyChangedEventHandler handler = null;
handler = (s, e) => {
if (string.IsNullOrEmpty(e.PropertyName) || e.PropertyName == matchingGuardName)
{
Caliburn.Micro.Core.Execute.OnUIThread(() =>
{
var message = context.Message;
if (message == null)
{
inpc.PropertyChanged -= handler;
return;
}
message.UpdateAvailability();
});
}
};
inpc.PropertyChanged += handler;
context.Disposing += delegate { inpc.PropertyChanged -= handler; };
context.Message.Detaching += delegate { inpc.PropertyChanged -= handler; };
}
context.CanExecute = () => (bool)guard.Invoke(
context.Target,
MessageBinder.DetermineParameters(context, guard.GetParameters()));
};
///
/// Try to find a candidate for guard function, having:
/// - a name matching any of
/// - no generic parameters
/// - a bool return type
/// - no parameters or a set of parameters corresponding to the action method
///
/// The execution context
/// Method names to look for.
///A MethodInfo, if found; null otherwise
static MethodInfo TryFindGuardMethod(ActionExecutionContext context, IEnumerable possibleGuardNames) {
var targetType = context.Target.GetType();
MethodInfo guard = null;
foreach (string possibleGuardName in possibleGuardNames)
{
guard = GetMethodInfo(targetType, possibleGuardName);
if (guard != null) break;
}
if (guard == null) return null;
if (guard.ContainsGenericParameters) return null;
if (!typeof(bool).Equals(guard.ReturnType)) return null;
var guardPars = guard.GetParameters();
var actionPars = context.Method.GetParameters();
if (guardPars.Length == 0) return guard;
if (guardPars.Length != actionPars.Length) return null;
var comparisons = guardPars.Zip(
context.Method.GetParameters(),
(x, y) => x.ParameterType == y.ParameterType
);
if (comparisons.Any(x => !x))
{
return null;
}
return guard;
}
///
/// Returns the list of possible names of guard methods / properties for the given method.
///
public static Func> BuildPossibleGuardNames = method => {
var guardNames = new List();
const string GuardPrefix = "Can";
var methodName = method.Name;
guardNames.Add(GuardPrefix + methodName);
const string AsyncMethodSuffix = "Async";
if (methodName.EndsWith(AsyncMethodSuffix, StringComparison.OrdinalIgnoreCase)) {
guardNames.Add(GuardPrefix + methodName.Substring(0, methodName.Length - AsyncMethodSuffix.Length));
}
return guardNames;
};
static MethodInfo GetMethodInfo(Type t, string methodName)
{
#if WinRT
return t.GetRuntimeMethods().SingleOrDefault(m => m.Name == methodName);
#else
return t.GetMethod(methodName);
#endif
}
}
}