Parser.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. #if XFORMS
  2. namespace Caliburn.Micro.Core.Xamarin.Forms
  3. #else
  4. namespace Caliburn.Micro
  5. #endif
  6. {
  7. using System;
  8. using System.Collections.Generic;
  9. using System.Linq;
  10. #if WinRT81
  11. using System.Reflection;
  12. using Windows.UI.Xaml;
  13. using Microsoft.Xaml.Interactivity;
  14. using TriggerBase = Microsoft.Xaml.Interactivity.IBehavior;
  15. using EventTrigger = Microsoft.Xaml.Interactions.Core.EventTriggerBehavior;
  16. using TriggerAction = Microsoft.Xaml.Interactivity.IAction;
  17. using System.Text;
  18. using System.Text.RegularExpressions;
  19. using Windows.UI.Xaml.Data;
  20. #elif XFORMS
  21. using System.Reflection;
  22. using System.Text.RegularExpressions;
  23. using global::Xamarin.Forms;
  24. using DependencyObject = global::Xamarin.Forms.BindableObject;
  25. using FrameworkElement = global::Xamarin.Forms.VisualElement;
  26. #else
  27. using System.Reflection;
  28. using System.Text.RegularExpressions;
  29. using System.Windows;
  30. using System.Windows.Data;
  31. using EventTrigger = System.Windows.Interactivity.EventTrigger;
  32. using TriggerBase = System.Windows.Interactivity.TriggerBase;
  33. using TriggerAction = System.Windows.Interactivity.TriggerAction;
  34. using System.Text;
  35. using Caliburn.Micro.Core;
  36. #endif
  37. /// <summary>
  38. /// Parses text into a fully functional set of <see cref="TriggerBase"/> instances with <see cref="ActionMessage"/>.
  39. /// </summary>
  40. public static class Parser
  41. {
  42. static readonly Regex LongFormatRegularExpression = new Regex(@"^[\s]*\[[^\]]*\][\s]*=[\s]*\[[^\]]*\][\s]*$");
  43. static readonly ILog Log = LogManager.GetLog(typeof(Parser));
  44. /// <summary>
  45. /// Parses the specified message text.
  46. /// </summary>
  47. /// <param name="target">The target.</param>
  48. /// <param name="text">The message text.</param>
  49. /// <returns>The triggers parsed from the text.</returns>
  50. public static IEnumerable<TriggerBase> Parse(DependencyObject target, string text)
  51. {
  52. if (string.IsNullOrEmpty(text))
  53. {
  54. return new TriggerBase[0];
  55. }
  56. var triggers = new List<TriggerBase>();
  57. var messageTexts = StringSplitter.Split(text, ';');
  58. foreach (var messageText in messageTexts)
  59. {
  60. var triggerPlusMessage = LongFormatRegularExpression.IsMatch(messageText)
  61. ? StringSplitter.Split(messageText, '=')
  62. : new[] { null, messageText };
  63. var messageDetail = triggerPlusMessage.Last()
  64. .Replace("[", string.Empty)
  65. .Replace("]", string.Empty)
  66. .Trim();
  67. var trigger = CreateTrigger(target, triggerPlusMessage.Length == 1 ? null : triggerPlusMessage[0]);
  68. var message = CreateMessage(target, messageDetail);
  69. #if WinRT81 || XFORMS
  70. AddActionToTrigger(target, message, trigger);
  71. #else
  72. trigger.Actions.Add(message);
  73. #endif
  74. triggers.Add(trigger);
  75. }
  76. return triggers;
  77. }
  78. #if XFORMS
  79. private static void AddActionToTrigger(DependencyObject target, TriggerAction message, TriggerBase trigger) {
  80. if (trigger is EventTrigger) {
  81. var eventTrigger = (EventTrigger) trigger;
  82. eventTrigger.Actions.Add(message);
  83. }
  84. trigger.EnterActions.Add(message);
  85. // TriggerAction doesn't have an associated object property so we have
  86. // to create it ourselves, could be potential issues here with leaking the associated
  87. // object and not correctly detaching, this may depend if the trigger implements it's
  88. // AssociatedObject as a DependencyProperty.
  89. var actionMessage = message as ActionMessage;
  90. var targetElement = target as FrameworkElement;
  91. if (actionMessage != null && targetElement != null)
  92. {
  93. actionMessage.AssociatedObject = targetElement;
  94. }
  95. }
  96. #endif
  97. #if WinRT81
  98. private static void AddActionToTrigger(DependencyObject target, TriggerAction message, TriggerBase trigger)
  99. {
  100. // This is stupid, but there a number of limitiations in the 8.1 Behaviours SDK
  101. // The first is that there is no base class for a Trigger, just IBehaviour. Which
  102. // means there's no strongly typed way to add an action to a trigger. Every trigger
  103. // in the SDK implements the same pattern but no interface, we're going to have to
  104. // use reflection to set it.
  105. // More stupidity, ActionCollection doesn't care about IAction, but DependencyObject
  106. // and there's no actual implementation of
  107. var messageDependencyObject = message as DependencyObject;
  108. if (messageDependencyObject == null)
  109. {
  110. Log.Warn("{0} doesn't inherit DependencyObject and can't be added to ActionCollection", trigger.GetType().FullName);
  111. return;
  112. }
  113. // 95% of the time the trigger will be an EventTrigger, let's optimise for that case
  114. if (trigger is EventTrigger)
  115. {
  116. var eventTrigger = (EventTrigger) trigger;
  117. eventTrigger.Actions.Add(messageDependencyObject);
  118. }
  119. else
  120. {
  121. var actionsProperty = trigger.GetType().GetRuntimeProperty("Actions");
  122. if (actionsProperty == null)
  123. {
  124. Log.Warn("Could not find Actions collection on trigger {0}.", trigger.GetType().FullName);
  125. return;
  126. }
  127. var actionCollection = actionsProperty.GetValue(trigger) as ActionCollection;
  128. if (actionCollection == null)
  129. {
  130. Log.Warn("{0}.Actions is either not an ActionCollection or is null.", trigger.GetType().FullName);
  131. return;
  132. }
  133. actionCollection.Add(messageDependencyObject);
  134. }
  135. // The second is the IAction doesn't have an associated object property so we have
  136. // to create it ourselves, could be potential issues here with leaking the associated
  137. // object and not correctly detaching, this may depend if the trigger implements it's
  138. // AssociatedObject as a DependencyProperty.
  139. // Turns out trying to a binding won't work because the trigger doesn't notify the
  140. // binding of changes, so we just need to set it, yay.
  141. var actionMessage = message as ActionMessage;
  142. var targetElement = target as FrameworkElement;
  143. if (actionMessage != null && targetElement != null)
  144. {
  145. //var binding = new Binding { Source = trigger, Path = new PropertyPath("AssociatedObject") };
  146. //BindingOperations.SetBinding(actionMessage, ActionMessage.AssociatedObjectProperty, binding);
  147. actionMessage.AssociatedObject = targetElement;
  148. }
  149. }
  150. #endif
  151. /// <summary>
  152. /// The function used to generate a trigger.
  153. /// </summary>
  154. /// <remarks>The parameters passed to the method are the the target of the trigger and string representing the trigger.</remarks>
  155. public static Func<DependencyObject, string, TriggerBase> CreateTrigger = (target, triggerText) =>
  156. {
  157. if (triggerText == null)
  158. {
  159. var defaults = ConventionManager.GetElementConvention(target.GetType());
  160. return defaults.CreateTrigger();
  161. }
  162. var triggerDetail = triggerText
  163. .Replace("[", string.Empty)
  164. .Replace("]", string.Empty)
  165. .Replace("Event", string.Empty)
  166. .Trim();
  167. #if XFORMS
  168. return new EventTrigger { Event = triggerDetail };
  169. #else
  170. return new EventTrigger { EventName = triggerDetail };
  171. #endif
  172. };
  173. /// <summary>
  174. /// Creates an instance of <see cref="ActionMessage"/> by parsing out the textual dsl.
  175. /// </summary>
  176. /// <param name="target">The target of the message.</param>
  177. /// <param name="messageText">The textual message dsl.</param>
  178. /// <returns>The created message.</returns>
  179. public static TriggerAction CreateMessage(DependencyObject target, string messageText)
  180. {
  181. var openingParenthesisIndex = messageText.IndexOf('(');
  182. if (openingParenthesisIndex < 0)
  183. {
  184. openingParenthesisIndex = messageText.Length;
  185. }
  186. var closingParenthesisIndex = messageText.LastIndexOf(')');
  187. if (closingParenthesisIndex < 0)
  188. {
  189. closingParenthesisIndex = messageText.Length;
  190. }
  191. var core = messageText.Substring(0, openingParenthesisIndex).Trim();
  192. var message = InterpretMessageText(target, core);
  193. var withParameters = message as IHaveParameters;
  194. if (withParameters != null)
  195. {
  196. if (closingParenthesisIndex - openingParenthesisIndex > 1)
  197. {
  198. var paramString = messageText.Substring(openingParenthesisIndex + 1, closingParenthesisIndex - openingParenthesisIndex - 1);
  199. var parameters = StringSplitter.SplitParameters(paramString);
  200. foreach (var parameter in parameters)
  201. withParameters.Parameters.Add(CreateParameter(target, parameter.Trim()));
  202. }
  203. }
  204. return message;
  205. }
  206. /// <summary>
  207. /// Function used to parse a string identified as a message.
  208. /// </summary>
  209. public static Func<DependencyObject, string, TriggerAction> InterpretMessageText = (target, text) =>
  210. {
  211. return new ActionMessage { MethodName = Regex.Replace(text, "^Action", string.Empty).Trim() };
  212. };
  213. /// <summary>
  214. /// Function used to parse a string identified as a message parameter.
  215. /// </summary>
  216. public static Func<DependencyObject, string, Parameter> CreateParameter = (target, parameterText) =>
  217. {
  218. var actualParameter = new Parameter();
  219. if (parameterText.StartsWith("'") && parameterText.EndsWith("'"))
  220. {
  221. actualParameter.Value = parameterText.Substring(1, parameterText.Length - 2);
  222. }
  223. else if (MessageBinder.SpecialValues.ContainsKey(parameterText.ToLower()) || char.IsNumber(parameterText[0]))
  224. {
  225. actualParameter.Value = parameterText;
  226. }
  227. else if (target is FrameworkElement)
  228. {
  229. var fe = (FrameworkElement)target;
  230. var nameAndBindingMode = parameterText.Split(':').Select(x => x.Trim()).ToArray();
  231. var index = nameAndBindingMode[0].IndexOf('.');
  232. View.ExecuteOnLoad(fe, delegate
  233. {
  234. BindParameter(
  235. fe,
  236. actualParameter,
  237. index == -1 ? nameAndBindingMode[0] : nameAndBindingMode[0].Substring(0, index),
  238. index == -1 ? null : nameAndBindingMode[0].Substring(index + 1),
  239. nameAndBindingMode.Length == 2 ? (BindingMode)Enum.Parse(typeof(BindingMode), nameAndBindingMode[1], true) : BindingMode.OneWay
  240. );
  241. });
  242. }
  243. return actualParameter;
  244. };
  245. /// <summary>
  246. /// Creates a binding on a <see cref="Parameter"/>.
  247. /// </summary>
  248. /// <param name="target">The target to which the message is applied.</param>
  249. /// <param name="parameter">The parameter object.</param>
  250. /// <param name="elementName">The name of the element to bind to.</param>
  251. /// <param name="path">The path of the element to bind to.</param>
  252. /// <param name="bindingMode">The binding mode to use.</param>
  253. public static void BindParameter(FrameworkElement target, Parameter parameter, string elementName, string path, BindingMode bindingMode)
  254. {
  255. #if XFORMS
  256. var element = elementName == "$this" ? target : null;
  257. if (element == null)
  258. {
  259. return;
  260. }
  261. if (string.IsNullOrEmpty(path))
  262. {
  263. path = ConventionManager.GetElementConvention(element.GetType()).ParameterProperty;
  264. }
  265. var binding = new Binding(path) {
  266. Source = element,
  267. Mode = bindingMode
  268. };
  269. parameter.SetBinding(Parameter.ValueProperty, binding);
  270. #else
  271. var element = elementName == "$this"
  272. ? target
  273. : BindingScope.GetNamedElements(target).FindName(elementName);
  274. if (element == null)
  275. {
  276. return;
  277. }
  278. if (string.IsNullOrEmpty(path))
  279. {
  280. path = ConventionManager.GetElementConvention(element.GetType()).ParameterProperty;
  281. }
  282. #if WinRT
  283. var binding = new Binding
  284. {
  285. Path = new PropertyPath(path),
  286. Source = element,
  287. Mode = bindingMode
  288. };
  289. #else
  290. var binding = new Binding(path) {
  291. Source = element,
  292. Mode = bindingMode
  293. };
  294. #endif
  295. #if (SILVERLIGHT && !SL5)
  296. var expression = (BindingExpression)BindingOperations.SetBinding(parameter, Parameter.ValueProperty, binding);
  297. var field = element.GetType().GetField(path + "Property", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
  298. if (field == null) {
  299. return;
  300. }
  301. ConventionManager.ApplySilverlightTriggers(element, (DependencyProperty)field.GetValue(null), x => expression, null, null);
  302. #else
  303. #if !WinRT
  304. binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
  305. #endif
  306. BindingOperations.SetBinding(parameter, Parameter.ValueProperty, binding);
  307. #endif
  308. #endif
  309. }
  310. }
  311. }