AvaloniaObjectExtensions.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. // Copyright (c) The Avalonia Project. All rights reserved.
  2. // Licensed under the MIT license. See licence.md file in the project root for full license information.
  3. using System;
  4. using System.Reactive;
  5. using System.Reactive.Disposables;
  6. using System.Reactive.Linq;
  7. using System.Reactive.Subjects;
  8. using Avalonia.Data;
  9. using Avalonia.Reactive;
  10. namespace Avalonia
  11. {
  12. /// <summary>
  13. /// Provides extension methods for <see cref="AvaloniaObject"/> and related classes.
  14. /// </summary>
  15. public static class AvaloniaObjectExtensions
  16. {
  17. /// <summary>
  18. /// Converts an <see cref="IObservable{T}"/> to an <see cref="IBinding"/>.
  19. /// </summary>
  20. /// <typeparam name="T">The type produced by the observable.</typeparam>
  21. /// <param name="source">The observable</param>
  22. /// <returns>An <see cref="IBinding"/>.</returns>
  23. public static IBinding ToBinding<T>(this IObservable<T> source)
  24. {
  25. return new BindingAdaptor(source.Select(x => (object)x));
  26. }
  27. /// <summary>
  28. /// Gets an observable for a <see cref="AvaloniaProperty"/>.
  29. /// </summary>
  30. /// <param name="o">The object.</param>
  31. /// <param name="property">The property.</param>
  32. /// <returns>
  33. /// An observable which fires immediately with the current value of the property on the
  34. /// object and subsequently each time the property value changes.
  35. /// </returns>
  36. public static IObservable<object> GetObservable(this IAvaloniaObject o, AvaloniaProperty property)
  37. {
  38. Contract.Requires<ArgumentNullException>(o != null);
  39. Contract.Requires<ArgumentNullException>(property != null);
  40. return new AvaloniaObservable<object>(
  41. observer =>
  42. {
  43. EventHandler<AvaloniaPropertyChangedEventArgs> handler = (s, e) =>
  44. {
  45. if (e.Property == property)
  46. {
  47. observer.OnNext(e.NewValue);
  48. }
  49. };
  50. observer.OnNext(o.GetValue(property));
  51. o.PropertyChanged += handler;
  52. return Disposable.Create(() =>
  53. {
  54. o.PropertyChanged -= handler;
  55. });
  56. },
  57. GetDescription(o, property));
  58. }
  59. /// <summary>
  60. /// Gets an observable for a <see cref="AvaloniaProperty"/>.
  61. /// </summary>
  62. /// <param name="o">The object.</param>
  63. /// <typeparam name="T">The property type.</typeparam>
  64. /// <param name="property">The property.</param>
  65. /// <returns>
  66. /// An observable which fires immediately with the current value of the property on the
  67. /// object and subsequently each time the property value changes.
  68. /// </returns>
  69. public static IObservable<T> GetObservable<T>(this IAvaloniaObject o, AvaloniaProperty<T> property)
  70. {
  71. Contract.Requires<ArgumentNullException>(o != null);
  72. Contract.Requires<ArgumentNullException>(property != null);
  73. return o.GetObservable((AvaloniaProperty)property).Cast<T>();
  74. }
  75. /// <summary>
  76. /// Gets an observable for a <see cref="AvaloniaProperty"/>.
  77. /// </summary>
  78. /// <param name="o">The object.</param>
  79. /// <typeparam name="T">The type of the property.</typeparam>
  80. /// <param name="property">The property.</param>
  81. /// <returns>
  82. /// An observable which when subscribed pushes the old and new values of the property each
  83. /// time it is changed. Note that the observable returned from this method does not fire
  84. /// with the current value of the property immediately.
  85. /// </returns>
  86. public static IObservable<Tuple<T, T>> GetObservableWithHistory<T>(
  87. this IAvaloniaObject o,
  88. AvaloniaProperty<T> property)
  89. {
  90. Contract.Requires<ArgumentNullException>(o != null);
  91. Contract.Requires<ArgumentNullException>(property != null);
  92. return new AvaloniaObservable<Tuple<T, T>>(
  93. observer =>
  94. {
  95. EventHandler<AvaloniaPropertyChangedEventArgs> handler = (s, e) =>
  96. {
  97. if (e.Property == property)
  98. {
  99. observer.OnNext(Tuple.Create((T)e.OldValue, (T)e.NewValue));
  100. }
  101. };
  102. o.PropertyChanged += handler;
  103. return Disposable.Create(() =>
  104. {
  105. o.PropertyChanged -= handler;
  106. });
  107. },
  108. GetDescription(o, property));
  109. }
  110. /// <summary>
  111. /// Gets a subject for a <see cref="AvaloniaProperty"/>.
  112. /// </summary>
  113. /// <param name="o">The object.</param>
  114. /// <param name="property">The property.</param>
  115. /// <param name="priority">
  116. /// The priority with which binding values are written to the object.
  117. /// </param>
  118. /// <returns>
  119. /// An <see cref="ISubject{Object}"/> which can be used for two-way binding to/from the
  120. /// property.
  121. /// </returns>
  122. public static ISubject<object> GetSubject(
  123. this IAvaloniaObject o,
  124. AvaloniaProperty property,
  125. BindingPriority priority = BindingPriority.LocalValue)
  126. {
  127. return Subject.Create<object>(
  128. Observer.Create<object>(x => o.SetValue(property, x, priority)),
  129. o.GetObservable(property));
  130. }
  131. /// <summary>
  132. /// Gets a subject for a <see cref="AvaloniaProperty"/>.
  133. /// </summary>
  134. /// <typeparam name="T">The property type.</typeparam>
  135. /// <param name="o">The object.</param>
  136. /// <param name="property">The property.</param>
  137. /// <param name="priority">
  138. /// The priority with which binding values are written to the object.
  139. /// </param>
  140. /// <returns>
  141. /// An <see cref="ISubject{T}"/> which can be used for two-way binding to/from the
  142. /// property.
  143. /// </returns>
  144. public static ISubject<T> GetSubject<T>(
  145. this IAvaloniaObject o,
  146. AvaloniaProperty<T> property,
  147. BindingPriority priority = BindingPriority.LocalValue)
  148. {
  149. return Subject.Create<T>(
  150. Observer.Create<T>(x => o.SetValue(property, x, priority)),
  151. o.GetObservable(property));
  152. }
  153. /// <summary>
  154. /// Gets a weak observable for a <see cref="AvaloniaProperty"/>.
  155. /// </summary>
  156. /// <param name="o">The object.</param>
  157. /// <param name="property">The property.</param>
  158. /// <returns>An observable.</returns>
  159. public static IObservable<object> GetWeakObservable(this IAvaloniaObject o, AvaloniaProperty property)
  160. {
  161. Contract.Requires<ArgumentNullException>(o != null);
  162. Contract.Requires<ArgumentNullException>(property != null);
  163. return new WeakPropertyChangedObservable(
  164. new WeakReference<IAvaloniaObject>(o),
  165. property,
  166. GetDescription(o, property));
  167. }
  168. /// <summary>
  169. /// Binds a property on an <see cref="IAvaloniaObject"/> to an <see cref="IBinding"/>.
  170. /// </summary>
  171. /// <param name="target">The object.</param>
  172. /// <param name="property">The property to bind.</param>
  173. /// <param name="binding">The binding.</param>
  174. /// <param name="anchor">
  175. /// An optional anchor from which to locate required context. When binding to objects that
  176. /// are not in the logical tree, certain types of binding need an anchor into the tree in
  177. /// order to locate named controls or resources. The <paramref name="anchor"/> parameter
  178. /// can be used to provice this context.
  179. /// </param>
  180. /// <returns>An <see cref="IDisposable"/> which can be used to cancel the binding.</returns>
  181. public static IDisposable Bind(
  182. this IAvaloniaObject target,
  183. AvaloniaProperty property,
  184. IBinding binding,
  185. object anchor = null)
  186. {
  187. Contract.Requires<ArgumentNullException>(target != null);
  188. Contract.Requires<ArgumentNullException>(property != null);
  189. Contract.Requires<ArgumentNullException>(binding != null);
  190. var metadata = property.GetMetadata(target.GetType()) as IDirectPropertyMetadata;
  191. var result = binding.Initiate(
  192. target,
  193. property,
  194. anchor,
  195. metadata?.EnableDataValidation ?? false);
  196. if (result != null)
  197. {
  198. return BindingOperations.Apply(target, property, result, anchor);
  199. }
  200. else
  201. {
  202. return Disposable.Empty;
  203. }
  204. }
  205. /// <summary>
  206. /// Subscribes to a property changed notifications for changes that originate from a
  207. /// <typeparamref name="TTarget"/>.
  208. /// </summary>
  209. /// <typeparam name="TTarget">The type of the property change sender.</typeparam>
  210. /// <param name="observable">The property changed observable.</param>
  211. /// <param name="action">
  212. /// The method to call. The parameters are the sender and the event args.
  213. /// </param>
  214. /// <returns>A disposable that can be used to terminate the subscription.</returns>
  215. public static IDisposable AddClassHandler<TTarget>(
  216. this IObservable<AvaloniaPropertyChangedEventArgs> observable,
  217. Action<TTarget, AvaloniaPropertyChangedEventArgs> action)
  218. where TTarget : AvaloniaObject
  219. {
  220. return observable.Subscribe(e =>
  221. {
  222. if (e.Sender is TTarget)
  223. {
  224. action((TTarget)e.Sender, e);
  225. }
  226. });
  227. }
  228. /// <summary>
  229. /// Subscribes to a property changed notifications for changes that originate from a
  230. /// <typeparamref name="TTarget"/>.
  231. /// </summary>
  232. /// <typeparam name="TTarget">The type of the property change sender.</typeparam>
  233. /// <param name="observable">The property changed observable.</param>
  234. /// <param name="handler">Given a TTarget, returns the handler.</param>
  235. /// <returns>A disposable that can be used to terminate the subscription.</returns>
  236. public static IDisposable AddClassHandler<TTarget>(
  237. this IObservable<AvaloniaPropertyChangedEventArgs> observable,
  238. Func<TTarget, Action<AvaloniaPropertyChangedEventArgs>> handler)
  239. where TTarget : class
  240. {
  241. return observable.Subscribe(e => SubscribeAdapter(e, handler));
  242. }
  243. /// <summary>
  244. /// Gets a description of a property that van be used in observables.
  245. /// </summary>
  246. /// <param name="o">The object.</param>
  247. /// <param name="property">The property</param>
  248. /// <returns>The description.</returns>
  249. private static string GetDescription(IAvaloniaObject o, AvaloniaProperty property)
  250. {
  251. return $"{o.GetType().Name}.{property.Name}";
  252. }
  253. /// <summary>
  254. /// Observer method for <see cref="AddClassHandler{TTarget}(IObservable{AvaloniaPropertyChangedEventArgs},
  255. /// Func{TTarget, Action{AvaloniaPropertyChangedEventArgs}})"/>.
  256. /// </summary>
  257. /// <typeparam name="TTarget">The sender type to accept.</typeparam>
  258. /// <param name="e">The event args.</param>
  259. /// <param name="handler">Given a TTarget, returns the handler.</param>
  260. private static void SubscribeAdapter<TTarget>(
  261. AvaloniaPropertyChangedEventArgs e,
  262. Func<TTarget, Action<AvaloniaPropertyChangedEventArgs>> handler)
  263. where TTarget : class
  264. {
  265. var target = e.Sender as TTarget;
  266. if (target != null)
  267. {
  268. handler(target)(e);
  269. }
  270. }
  271. private class BindingAdaptor : IBinding
  272. {
  273. private IObservable<object> _source;
  274. public BindingAdaptor(IObservable<object> source)
  275. {
  276. this._source = source;
  277. }
  278. public InstancedBinding Initiate(
  279. IAvaloniaObject target,
  280. AvaloniaProperty targetProperty,
  281. object anchor = null,
  282. bool enableDataValidation = false)
  283. {
  284. return InstancedBinding.OneWay(_source);
  285. }
  286. }
  287. }
  288. }