ActionMessage.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. namespace Caliburn.Micro {
  2. using System;
  3. using System.Collections.Generic;
  4. using System.ComponentModel;
  5. using System.Linq;
  6. using System.Reflection;
  7. #if WinRT81
  8. using Windows.UI.Xaml;
  9. using Windows.UI.Xaml.Data;
  10. using Windows.UI.Xaml.Markup;
  11. using Windows.UI.Xaml.Media;
  12. using Windows.UI.Xaml.Controls;
  13. using Microsoft.Xaml.Interactivity;
  14. using TriggerBase = Microsoft.Xaml.Interactivity.IBehavior;
  15. using EventTrigger = Microsoft.Xaml.Interactions.Core.EventTriggerBehavior;
  16. #else
  17. using System.Windows;
  18. using System.Windows.Controls;
  19. using System.Windows.Controls.Primitives;
  20. using System.Windows.Data;
  21. using System.Windows.Interactivity;
  22. using System.Windows.Markup;
  23. using System.Windows.Media;
  24. using EventTrigger = System.Windows.Interactivity.EventTrigger;
  25. using Caliburn.Micro.Core;
  26. using System.IO;
  27. using System.Xml;
  28. #endif
  29. /// <summary>
  30. /// Used to send a message from the UI to a presentation model class, indicating that a particular Action should be invoked.
  31. /// </summary>
  32. #if WinRT
  33. [ContentProperty(Name = "Parameters")]
  34. #else
  35. [ContentProperty("Parameters")]
  36. [DefaultTrigger(typeof(FrameworkElement), typeof(EventTrigger), "MouseLeftButtonDown")]
  37. [DefaultTrigger(typeof(ButtonBase), typeof(EventTrigger), "Click")]
  38. [TypeConstraint(typeof(FrameworkElement))]
  39. #endif
  40. public class ActionMessage : TriggerAction<FrameworkElement>, IHaveParameters {
  41. static readonly ILog Log = LogManager.GetLog(typeof(ActionMessage));
  42. ActionExecutionContext context;
  43. #if WINDOWS_PHONE
  44. internal Microsoft.Phone.Shell.IApplicationBarMenuItem applicationBarSource;
  45. #endif
  46. internal static readonly DependencyProperty HandlerProperty = DependencyProperty.RegisterAttached(
  47. "Handler",
  48. typeof(object),
  49. typeof(ActionMessage),
  50. new PropertyMetadata(null, HandlerPropertyChanged)
  51. );
  52. ///<summary>
  53. /// Causes the action invocation to "double check" if the action should be invoked by executing the guard immediately before hand.
  54. ///</summary>
  55. /// <remarks>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.</remarks>
  56. public static bool EnforceGuardsDuringInvocation = false;
  57. ///<summary>
  58. /// Causes the action to throw if it cannot locate the target or the method at invocation time.
  59. ///</summary>
  60. /// <remarks>True by default.</remarks>
  61. public static bool ThrowsExceptions = true;
  62. /// <summary>
  63. /// Represents the method name of an action message.
  64. /// </summary>
  65. public static readonly DependencyProperty MethodNameProperty =
  66. DependencyProperty.Register(
  67. "MethodName",
  68. typeof(string),
  69. typeof(ActionMessage),
  70. null
  71. );
  72. /// <summary>
  73. /// Represents the parameters of an action message.
  74. /// </summary>
  75. public static readonly DependencyProperty ParametersProperty =
  76. DependencyProperty.Register(
  77. "Parameters",
  78. typeof(AttachedCollection<Parameter>),
  79. typeof(ActionMessage),
  80. null
  81. );
  82. /// <summary>
  83. /// Creates an instance of <see cref="ActionMessage"/>.
  84. /// </summary>
  85. public ActionMessage() {
  86. SetValue(ParametersProperty, new AttachedCollection<Parameter>());
  87. }
  88. /// <summary>
  89. /// Gets or sets the name of the method to be invoked on the presentation model class.
  90. /// </summary>
  91. /// <value>The name of the method.</value>
  92. #if !WinRT
  93. [Category("Common Properties")]
  94. #endif
  95. public string MethodName {
  96. get { return (string)GetValue(MethodNameProperty); }
  97. set { SetValue(MethodNameProperty, value); }
  98. }
  99. /// <summary>
  100. /// Gets the parameters to pass as part of the method invocation.
  101. /// </summary>
  102. /// <value>The parameters.</value>
  103. #if !WinRT
  104. [Category("Common Properties")]
  105. #endif
  106. public AttachedCollection<Parameter> Parameters {
  107. get { return (AttachedCollection<Parameter>)GetValue(ParametersProperty); }
  108. }
  109. /// <summary>
  110. /// Occurs before the message detaches from the associated object.
  111. /// </summary>
  112. public event EventHandler Detaching = delegate { };
  113. /// <summary>
  114. /// Called after the action is attached to an AssociatedObject.
  115. /// </summary>
  116. #if WinRT81
  117. protected override void OnAttached() {
  118. if (!View.InDesignMode) {
  119. Parameters.Attach(AssociatedObject);
  120. Parameters.OfType<Parameter>().Apply(x => x.MakeAwareOf(this));
  121. if (View.ExecuteOnLoad(AssociatedObject, ElementLoaded)) {
  122. // Not yet sure if this will be needed
  123. //var trigger = Interaction.GetTriggers(AssociatedObject)
  124. // .FirstOrDefault(t => t.Actions.Contains(this)) as EventTrigger;
  125. //if (trigger != null && trigger.EventName == "Loaded")
  126. // Invoke(new RoutedEventArgs());
  127. }
  128. View.ExecuteOnUnload(AssociatedObject, ElementUnloaded);
  129. }
  130. base.OnAttached();
  131. }
  132. void ElementUnloaded(object sender, RoutedEventArgs e)
  133. {
  134. OnDetaching();
  135. }
  136. #else
  137. protected override void OnAttached() {
  138. if (!View.InDesignMode) {
  139. Parameters.Attach(AssociatedObject);
  140. Parameters.Apply(x => x.MakeAwareOf(this));
  141. if (View.ExecuteOnLoad(AssociatedObject, ElementLoaded)) {
  142. var trigger = Interaction.GetTriggers(AssociatedObject)
  143. .FirstOrDefault(t => t.Actions.Contains(this)) as EventTrigger;
  144. if (trigger != null && trigger.EventName == "Loaded")
  145. Invoke(new RoutedEventArgs());
  146. }
  147. }
  148. base.OnAttached();
  149. }
  150. #endif
  151. static void HandlerPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
  152. ((ActionMessage)d).UpdateContext();
  153. }
  154. /// <summary>
  155. /// Called when the action is being detached from its AssociatedObject, but before it has actually occurred.
  156. /// </summary>
  157. protected override void OnDetaching() {
  158. if (!View.InDesignMode) {
  159. Detaching(this, EventArgs.Empty);
  160. AssociatedObject.Loaded -= ElementLoaded;
  161. Parameters.Detach();
  162. }
  163. base.OnDetaching();
  164. }
  165. void ElementLoaded(object sender, RoutedEventArgs e) {
  166. UpdateContext();
  167. DependencyObject currentElement;
  168. if (context.View == null) {
  169. currentElement = AssociatedObject;
  170. while (currentElement != null) {
  171. if (Action.HasTargetSet(currentElement))
  172. break;
  173. currentElement = BindingScope.GetVisualParent(currentElement);
  174. }
  175. }
  176. else currentElement = context.View;
  177. #if NET
  178. var binding = new Binding {
  179. Path = new PropertyPath(Message.HandlerProperty),
  180. Source = currentElement
  181. };
  182. #elif WinRT
  183. var binding = new Binding {
  184. Source = currentElement
  185. };
  186. #else
  187. const string bindingText = "<Binding xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation\' xmlns:cal='clr-namespace:Caliburn.Micro;assembly=Caliburn.Micro.Platform' Path='(cal:Message.Handler)' />";
  188. var binding = (Binding)XamlReader.Load(bindingText);
  189. binding.Source = currentElement;
  190. #endif
  191. BindingOperations.SetBinding(this, HandlerProperty, binding);
  192. }
  193. void UpdateContext() {
  194. if (context != null)
  195. context.Dispose();
  196. context = new ActionExecutionContext {
  197. Message = this,
  198. Source = AssociatedObject
  199. };
  200. PrepareContext(context);
  201. UpdateAvailabilityCore();
  202. }
  203. /// <summary>
  204. /// Invokes the action.
  205. /// </summary>
  206. /// <param name="eventArgs">The parameter to the action. If the action does not require a parameter, the parameter may be set to a null reference.</param>
  207. protected override void Invoke(object eventArgs) {
  208. Log.Info("Invoking {0}.", this);
  209. if (context == null) {
  210. UpdateContext();
  211. }
  212. if (context.Target == null || context.View == null) {
  213. PrepareContext(context);
  214. if (context.Target == null) {
  215. var ex = new Exception(string.Format("No target found for method {0}.", context.Message.MethodName));
  216. Log.Error(ex);
  217. if (!ThrowsExceptions)
  218. return;
  219. throw ex;
  220. }
  221. if (!UpdateAvailabilityCore()) {
  222. return;
  223. }
  224. }
  225. if (context.Method == null) {
  226. var ex = new Exception(string.Format("Method {0} not found on target of type {1}.", context.Message.MethodName, context.Target.GetType()));
  227. Log.Error(ex);
  228. if (!ThrowsExceptions)
  229. return;
  230. throw ex;
  231. }
  232. context.EventArgs = eventArgs;
  233. if (EnforceGuardsDuringInvocation && context.CanExecute != null && !context.CanExecute()) {
  234. return;
  235. }
  236. InvokeAction(context);
  237. context.EventArgs = null;
  238. }
  239. /// <summary>
  240. /// Forces an update of the UI's Enabled/Disabled state based on the the preconditions associated with the method.
  241. /// </summary>
  242. public virtual void UpdateAvailability() {
  243. if (context == null)
  244. return;
  245. if (context.Target == null || context.View == null)
  246. PrepareContext(context);
  247. UpdateAvailabilityCore();
  248. }
  249. bool UpdateAvailabilityCore() {
  250. Log.Info("{0} availability update.", this);
  251. return ApplyAvailabilityEffect(context);
  252. }
  253. /// <summary>
  254. /// Returns a <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
  255. /// </summary>
  256. /// <returns>
  257. /// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
  258. /// </returns>
  259. public override string ToString() {
  260. return "Action: " + MethodName;
  261. }
  262. /// <summary>
  263. /// Invokes the action using the specified <see cref="ActionExecutionContext"/>
  264. /// </summary>
  265. public static Action<ActionExecutionContext> InvokeAction = context => {
  266. var values = MessageBinder.DetermineParameters(context, context.Method.GetParameters());
  267. var returnValue = context.Method.Invoke(context.Target, values);
  268. var task = returnValue as System.Threading.Tasks.Task;
  269. if (task != null) {
  270. returnValue = task.AsResult();
  271. }
  272. var result = returnValue as IResult;
  273. if (result != null) {
  274. returnValue = new[] { result };
  275. }
  276. var enumerable = returnValue as IEnumerable<IResult>;
  277. if (enumerable != null) {
  278. returnValue = enumerable.GetEnumerator();
  279. }
  280. var enumerator = returnValue as IEnumerator<IResult>;
  281. if (enumerator != null) {
  282. Coroutine.BeginExecute(enumerator,
  283. new CoroutineExecutionContext
  284. {
  285. Source = context.Source,
  286. View = context.View,
  287. Target = context.Target
  288. });
  289. }
  290. };
  291. /// <summary>
  292. /// Applies an availability effect, such as IsEnabled, to an element.
  293. /// </summary>
  294. /// <remarks>Returns a value indicating whether or not the action is available.</remarks>
  295. public static Func<ActionExecutionContext, bool> ApplyAvailabilityEffect = context => {
  296. #if WINDOWS_PHONE
  297. var message = context.Message;
  298. if (message != null && message.applicationBarSource != null) {
  299. if (context.CanExecute != null) {
  300. message.applicationBarSource.IsEnabled = context.CanExecute();
  301. }
  302. return message.applicationBarSource.IsEnabled;
  303. }
  304. #endif
  305. #if SILVERLIGHT || WinRT
  306. var source = context.Source as Control;
  307. #else
  308. var source = context.Source;
  309. #endif
  310. if (source == null) {
  311. return true;
  312. }
  313. #if SILVERLIGHT || WinRT
  314. var hasBinding = ConventionManager.HasBinding(source, Control.IsEnabledProperty);
  315. #else
  316. var hasBinding = ConventionManager.HasBinding(source, UIElement.IsEnabledProperty);
  317. #endif
  318. if (!hasBinding && context.CanExecute != null) {
  319. source.IsEnabled = context.CanExecute();
  320. }
  321. return source.IsEnabled;
  322. };
  323. /// <summary>
  324. /// Finds the method on the target matching the specified message.
  325. /// </summary>
  326. /// <param name="target">The target.</param>
  327. /// <param name="message">The message.</param>
  328. /// <returns>The matching method, if available.</returns>
  329. public static Func<ActionMessage, object, MethodInfo> GetTargetMethod = (message, target) => {
  330. #if WinRT
  331. return (from method in target.GetType().GetRuntimeMethods()
  332. where method.Name == message.MethodName
  333. let methodParameters = method.GetParameters()
  334. where message.Parameters.Count == methodParameters.Length
  335. select method).FirstOrDefault();
  336. #else
  337. return (from method in target.GetType().GetMethods()
  338. where method.Name == message.MethodName
  339. let methodParameters = method.GetParameters()
  340. where message.Parameters.Count == methodParameters.Length
  341. select method).FirstOrDefault();
  342. #endif
  343. };
  344. /// <summary>
  345. /// Sets the target, method and view on the context. Uses a bubbling strategy by default.
  346. /// </summary>
  347. public static Action<ActionExecutionContext> SetMethodBinding = context => {
  348. var source = context.Source;
  349. DependencyObject currentElement = source;
  350. while (currentElement != null) {
  351. if (Action.HasTargetSet(currentElement)) {
  352. var target = Message.GetHandler(currentElement);
  353. if (target != null) {
  354. var method = GetTargetMethod(context.Message, target);
  355. if (method != null) {
  356. context.Method = method;
  357. context.Target = target;
  358. context.View = currentElement;
  359. return;
  360. }
  361. }
  362. else {
  363. context.View = currentElement;
  364. return;
  365. }
  366. }
  367. currentElement = BindingScope.GetVisualParent(currentElement);
  368. }
  369. if (source != null && source.DataContext != null) {
  370. var target = source.DataContext;
  371. var method = GetTargetMethod(context.Message, target);
  372. if (method != null) {
  373. context.Target = target;
  374. context.Method = method;
  375. context.View = source;
  376. }
  377. }
  378. };
  379. /// <summary>
  380. /// Prepares the action execution context for use.
  381. /// </summary>
  382. public static Action<ActionExecutionContext> PrepareContext = context => {
  383. SetMethodBinding(context);
  384. if (context.Target == null || context.Method == null)
  385. {
  386. return;
  387. }
  388. var possibleGuardNames = BuildPossibleGuardNames(context.Method).ToList();
  389. var guard = TryFindGuardMethod(context, possibleGuardNames);
  390. if (guard == null)
  391. {
  392. var inpc = context.Target as INotifyPropertyChanged;
  393. if (inpc == null)
  394. return;
  395. var targetType = context.Target.GetType();
  396. string matchingGuardName = null;
  397. foreach (string possibleGuardName in possibleGuardNames)
  398. {
  399. matchingGuardName = possibleGuardName;
  400. guard = GetMethodInfo(targetType, "get_" + matchingGuardName);
  401. if (guard != null) break;
  402. }
  403. if (guard == null)
  404. return;
  405. PropertyChangedEventHandler handler = null;
  406. handler = (s, e) => {
  407. if (string.IsNullOrEmpty(e.PropertyName) || e.PropertyName == matchingGuardName)
  408. {
  409. Caliburn.Micro.Core.Execute.OnUIThread(() =>
  410. {
  411. var message = context.Message;
  412. if (message == null)
  413. {
  414. inpc.PropertyChanged -= handler;
  415. return;
  416. }
  417. message.UpdateAvailability();
  418. });
  419. }
  420. };
  421. inpc.PropertyChanged += handler;
  422. context.Disposing += delegate { inpc.PropertyChanged -= handler; };
  423. context.Message.Detaching += delegate { inpc.PropertyChanged -= handler; };
  424. }
  425. context.CanExecute = () => (bool)guard.Invoke(
  426. context.Target,
  427. MessageBinder.DetermineParameters(context, guard.GetParameters()));
  428. };
  429. /// <summary>
  430. /// Try to find a candidate for guard function, having:
  431. /// - a name matching any of <paramref name="possibleGuardNames"/>
  432. /// - no generic parameters
  433. /// - a bool return type
  434. /// - no parameters or a set of parameters corresponding to the action method
  435. /// </summary>
  436. /// <param name="context">The execution context</param>
  437. /// <param name="possibleGuardNames">Method names to look for.</param>
  438. ///<returns>A MethodInfo, if found; null otherwise</returns>
  439. static MethodInfo TryFindGuardMethod(ActionExecutionContext context, IEnumerable<string> possibleGuardNames) {
  440. var targetType = context.Target.GetType();
  441. MethodInfo guard = null;
  442. foreach (string possibleGuardName in possibleGuardNames)
  443. {
  444. guard = GetMethodInfo(targetType, possibleGuardName);
  445. if (guard != null) break;
  446. }
  447. if (guard == null) return null;
  448. if (guard.ContainsGenericParameters) return null;
  449. if (!typeof(bool).Equals(guard.ReturnType)) return null;
  450. var guardPars = guard.GetParameters();
  451. var actionPars = context.Method.GetParameters();
  452. if (guardPars.Length == 0) return guard;
  453. if (guardPars.Length != actionPars.Length) return null;
  454. var comparisons = guardPars.Zip(
  455. context.Method.GetParameters(),
  456. (x, y) => x.ParameterType == y.ParameterType
  457. );
  458. if (comparisons.Any(x => !x))
  459. {
  460. return null;
  461. }
  462. return guard;
  463. }
  464. /// <summary>
  465. /// Returns the list of possible names of guard methods / properties for the given method.
  466. /// </summary>
  467. public static Func<MethodInfo, IEnumerable<string>> BuildPossibleGuardNames = method => {
  468. var guardNames = new List<string>();
  469. const string GuardPrefix = "Can";
  470. var methodName = method.Name;
  471. guardNames.Add(GuardPrefix + methodName);
  472. const string AsyncMethodSuffix = "Async";
  473. if (methodName.EndsWith(AsyncMethodSuffix, StringComparison.OrdinalIgnoreCase)) {
  474. guardNames.Add(GuardPrefix + methodName.Substring(0, methodName.Length - AsyncMethodSuffix.Length));
  475. }
  476. return guardNames;
  477. };
  478. static MethodInfo GetMethodInfo(Type t, string methodName)
  479. {
  480. #if WinRT
  481. return t.GetRuntimeMethods().SingleOrDefault(m => m.Name == methodName);
  482. #else
  483. return t.GetMethod(methodName);
  484. #endif
  485. }
  486. }
  487. }