PageRenderTime 57ms CodeModel.GetById 27ms RepoModel.GetById 0ms app.codeStats 1ms

/ReactiveUI.Xaml/ReactiveAsyncCommand.cs

https://github.com/bsiegel/ReactiveUI
C# | 374 lines | 207 code | 41 blank | 126 comment | 19 complexity | 52d6ce83672d7e14c5f52ab3ad45d3be MD5 | raw file
Possible License(s): Apache-2.0, CC-BY-SA-3.0, LGPL-2.0
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Reactive.Concurrency;
  5. using System.Diagnostics.Contracts;
  6. using System.Reactive;
  7. using System.Reactive.Linq;
  8. using System.Reactive.Subjects;
  9. using System.Reactive.Threading.Tasks;
  10. using ReactiveUI;
  11. using System.Threading.Tasks;
  12. namespace ReactiveUI.Xaml
  13. {
  14. /// <summary>
  15. /// ReactiveAsyncCommand represents commands that run an asynchronous
  16. /// operation in the background when invoked. The main benefit of this
  17. /// command is that it will keep track of in-flight operations and
  18. /// disable/enable CanExecute when there are too many of them (i.e. a
  19. /// "Search" button shouldn't have many concurrent requests running if the
  20. /// user clicks the button many times quickly)
  21. /// </summary>
  22. public class ReactiveAsyncCommand : IReactiveAsyncCommand, IDisposable, IEnableLogger
  23. {
  24. /// <summary>
  25. /// Constructs a new ReactiveAsyncCommand.
  26. /// </summary>
  27. /// <param name="canExecute">An Observable representing when the command
  28. /// can execute. If null, the Command can always execute.</param>
  29. /// <param name="maximumConcurrent">The maximum number of in-flight
  30. /// operations at a time - defaults to one.</param>
  31. /// <param name="scheduler">The scheduler to run the asynchronous
  32. /// operations on - defaults to the Taskpool scheduler.</param>
  33. /// <param name="initialCondition">Initial CanExecute state</param>
  34. public ReactiveAsyncCommand(
  35. IObservable<bool> canExecute = null,
  36. int maximumConcurrent = 1,
  37. IScheduler scheduler = null,
  38. bool initialCondition = true)
  39. {
  40. commonCtor(maximumConcurrent, scheduler, canExecute, initialCondition);
  41. }
  42. protected ReactiveAsyncCommand(
  43. Func<object, bool> canExecute,
  44. int maximumConcurrent = 1,
  45. IScheduler scheduler = null)
  46. {
  47. Contract.Requires(maximumConcurrent > 0);
  48. _canExecuteExplicitFunc = canExecute;
  49. commonCtor(maximumConcurrent, scheduler);
  50. }
  51. /// <summary>
  52. /// Create is a helper method to create a basic ReactiveAsyncCommand
  53. /// in a non-Rx way, closer to how BackgroundWorker works.
  54. /// </summary>
  55. /// <param name="calculationFunc">The function that will calculate
  56. /// results in the background</param>
  57. /// <param name="callbackFunc">The method to be called once the
  58. /// calculation function completes. This method is guaranteed to be
  59. /// called on the UI thread.</param>
  60. /// <param name="maximumConcurrent">The maximum number of in-flight
  61. /// operations at a time - defaults to one.</param>
  62. /// <param name="scheduler">The scheduler to run the asynchronous
  63. /// operations on - defaults to the Taskpool scheduler.</param>
  64. public static ReactiveAsyncCommand Create<TRet>(
  65. Func<object, TRet> calculationFunc,
  66. Action<TRet> callbackFunc,
  67. Func<object, bool> canExecute = null,
  68. int maximumConcurrent = 1,
  69. IScheduler scheduler = null)
  70. {
  71. var ret = new ReactiveAsyncCommand(canExecute, maximumConcurrent, scheduler);
  72. ret.RegisterAsyncFunction(calculationFunc).Subscribe(callbackFunc);
  73. return ret;
  74. }
  75. void commonCtor(int maximumConcurrent, IScheduler scheduler, IObservable<bool> canExecute = null, bool initialCondition = true)
  76. {
  77. _normalSched = scheduler ?? RxApp.DeferredScheduler;
  78. _canExecuteSubject = new ScheduledSubject<bool>(_normalSched);
  79. _executeSubject = new ScheduledSubject<object>(Scheduler.Immediate);
  80. _exSubject = new ScheduledSubject<Exception>(_normalSched, RxApp.DefaultExceptionHandler);
  81. AsyncStartedNotification = new ScheduledSubject<Unit>(RxApp.DeferredScheduler);
  82. AsyncCompletedNotification = new ScheduledSubject<Unit>(RxApp.DeferredScheduler);
  83. ItemsInflight = Observable.Merge(
  84. AsyncStartedNotification.Select(_ => 1),
  85. AsyncCompletedNotification.Select(_ => -1)
  86. ).Scan(0, (acc, x) => {
  87. var ret = acc + x;
  88. if (ret < 0) {
  89. this.Log().Fatal("Reference count dropped below zero");
  90. }
  91. return ret;
  92. }).Multicast(new BehaviorSubject<int>(0)).PermaRef().ObserveOn(RxApp.DeferredScheduler);
  93. bool startCE = (_canExecuteExplicitFunc != null ? _canExecuteExplicitFunc(null) : initialCondition);
  94. CanExecuteObservable = Observable.CombineLatest(
  95. _canExecuteSubject.StartWith(startCE), ItemsInflight.Select(x => x < maximumConcurrent).StartWith(true),
  96. (canEx, slotsAvail) => canEx && slotsAvail)
  97. .DistinctUntilChanged();
  98. CanExecuteObservable.Subscribe(x => {
  99. this.Log().Debug("Setting canExecuteLatest to {0}", x);
  100. _canExecuteLatest = x;
  101. if (CanExecuteChanged != null) {
  102. CanExecuteChanged(this, new EventArgs());
  103. }
  104. });
  105. if (canExecute != null) {
  106. canExecute.Subscribe(_canExecuteSubject.OnNext, _exSubject.OnNext);
  107. }
  108. _maximumConcurrent = maximumConcurrent;
  109. ThrownExceptions = _exSubject;
  110. }
  111. IScheduler _normalSched;
  112. Func<object, bool> _canExecuteExplicitFunc = null;
  113. ISubject<bool> _canExecuteSubject;
  114. bool _canExecuteLatest;
  115. ISubject<object> _executeSubject;
  116. int _maximumConcurrent;
  117. IDisposable _inner = null;
  118. ScheduledSubject<Exception> _exSubject;
  119. public IObservable<int> ItemsInflight { get; protected set; }
  120. public ISubject<Unit> AsyncStartedNotification { get; protected set; }
  121. public ISubject<Unit> AsyncCompletedNotification { get; protected set; }
  122. public IObservable<bool> CanExecuteObservable { get; protected set; }
  123. public IObservable<Exception> ThrownExceptions { get; protected set; }
  124. public event EventHandler CanExecuteChanged;
  125. public bool CanExecute(object parameter)
  126. {
  127. if (_canExecuteExplicitFunc != null) {
  128. _canExecuteSubject.OnNext(_canExecuteExplicitFunc(parameter));
  129. }
  130. this.Log().Debug("CanExecute: returning {0}", _canExecuteLatest);
  131. return _canExecuteLatest;
  132. }
  133. public void Execute(object parameter)
  134. {
  135. if (!CanExecute(parameter)) {
  136. this.Log().Error("Attempted to call Execute when CanExecute is False!");
  137. return;
  138. }
  139. _executeSubject.OnNext(parameter);
  140. }
  141. public IDisposable Subscribe(IObserver<object> observer)
  142. {
  143. return _executeSubject.Subscribe(
  144. Observer.Create<object>(
  145. x => marshalFailures(observer.OnNext, x),
  146. ex => marshalFailures(observer.OnError, ex),
  147. () => marshalFailures(observer.OnCompleted)));
  148. }
  149. public void Dispose()
  150. {
  151. if (_inner != null) {
  152. _inner.Dispose();
  153. }
  154. }
  155. /// <summary>
  156. /// RegisterAsyncFunction registers an asynchronous method that returns a result
  157. /// to be called whenever the Command's Execute method is called.
  158. /// </summary>
  159. /// <param name="calculationFunc">The function to be run in the
  160. /// background.</param>
  161. /// <param name="scheduler"></param>
  162. /// <returns>An Observable that will fire on the UI thread once per
  163. /// invoecation of Execute, once the async method completes. Subscribe to
  164. /// this to retrieve the result of the calculationFunc.</returns>
  165. public IObservable<TResult> RegisterAsyncFunction<TResult>(
  166. Func<object, TResult> calculationFunc,
  167. IScheduler scheduler = null)
  168. {
  169. Contract.Requires(calculationFunc != null);
  170. var asyncFunc = calculationFunc.ToAsync(scheduler ?? RxApp.TaskpoolScheduler);
  171. return RegisterAsyncObservable(asyncFunc);
  172. }
  173. /// <summary>
  174. /// RegisterAsyncAction registers an asynchronous method that runs
  175. /// whenever the Command's Execute method is called and doesn't return a
  176. /// result.
  177. /// </summary>
  178. /// <param name="calculationFunc">The function to be run in the
  179. /// background.</param>
  180. public void RegisterAsyncAction(Action<object> calculationFunc,
  181. IScheduler scheduler = null)
  182. {
  183. Contract.Requires(calculationFunc != null);
  184. RegisterAsyncFunction(x => { calculationFunc(x); return new Unit(); }, scheduler);
  185. }
  186. /// <summary>
  187. /// RegisterAsyncTask registers an TPL/Async method that runs when a
  188. /// Command gets executed and returns the result
  189. /// </summary>
  190. /// <returns>An Observable that will fire on the UI thread once per
  191. /// invoecation of Execute, once the async method completes. Subscribe to
  192. /// this to retrieve the result of the calculationFunc.</returns>
  193. public IObservable<TResult> RegisterAsyncTask<TResult>(Func<object, Task<TResult>> calculationFunc)
  194. {
  195. Contract.Requires(calculationFunc != null);
  196. return RegisterAsyncObservable(x => calculationFunc(x).ToObservable());
  197. }
  198. /// <summary>
  199. /// RegisterAsyncTask registers an TPL/Async method that runs when a
  200. /// Command gets executed and returns no result.
  201. /// </summary>
  202. /// <param name="calculationFunc">The function to be run in the
  203. /// background.</param>
  204. /// <returns>An Observable that signals when the Task completes, on
  205. /// the UI thread.</returns>
  206. public IObservable<Unit> RegisterAsyncTask<TResult>(Func<object, Task> calculationFunc)
  207. {
  208. Contract.Requires(calculationFunc != null);
  209. return RegisterAsyncObservable(x => calculationFunc(x).ToObservable());
  210. }
  211. /// <summary>
  212. /// RegisterAsyncObservable registers an Rx-based async method whose
  213. /// results will be returned on the UI thread.
  214. /// </summary>
  215. /// <param name="calculationFunc">A calculation method that returns a
  216. /// future result, such as a method returned via
  217. /// Observable.FromAsyncPattern.</param>
  218. /// <returns>An Observable representing the items returned by the
  219. /// calculation result. Note that with this method it is possible with a
  220. /// calculationFunc to return multiple items per invocation of Execute.</returns>
  221. public IObservable<TResult> RegisterAsyncObservable<TResult>(Func<object, IObservable<TResult>> calculationFunc)
  222. {
  223. Contract.Requires(calculationFunc != null);
  224. var ret = _executeSubject
  225. .Select(x => {
  226. AsyncStartedNotification.OnNext(Unit.Default);
  227. return calculationFunc(x)
  228. .Catch<TResult, Exception>(ex => {
  229. _exSubject.OnNext(ex);
  230. return Observable.Empty<TResult>();
  231. })
  232. .Finally(() => AsyncCompletedNotification.OnNext(Unit.Default));
  233. });
  234. return ret.Merge().Multicast(new ScheduledSubject<TResult>(RxApp.DeferredScheduler)).PermaRef();
  235. }
  236. /// <summary>
  237. /// RegisterMemoizedFunction is similar to RegisterAsyncFunction, but
  238. /// caches its results so that subsequent Execute calls with the same
  239. /// CommandParameter will not need to be run in the background.
  240. /// </summary>
  241. /// <param name="calculationFunc">The function that performs the
  242. /// expensive or asyncronous calculation and returns the result.
  243. ///
  244. /// Note that this function *must* return an equivalently-same result given a
  245. /// specific input - because the function is being memoized, if the
  246. /// calculationFunc depends on other varables other than the input
  247. /// value, the results will be unpredictable.</param>
  248. /// <param name="maxSize">The number of items to cache. When this limit
  249. /// is reached, not recently used items will be discarded.</param>
  250. /// <param name="onRelease">This optional method is called when an item
  251. /// is evicted from the cache - this can be used to clean up / manage an
  252. /// on-disk cache; the calculationFunc can download a file and save it
  253. /// to a temporary folder, and the onRelease action will delete the
  254. /// file.</param>
  255. /// <param name="sched">The scheduler to run asynchronous operations on
  256. /// - defaults to TaskpoolScheduler</param>
  257. /// <returns>An Observable that will fire on the UI thread once per
  258. /// invocation of Execute, once the async method completes. Subscribe to
  259. /// this to retrieve the result of the calculationFunc.</returns>
  260. public IObservable<TResult> RegisterMemoizedFunction<TResult>(
  261. Func<object, TResult> calculationFunc,
  262. int maxSize = 50,
  263. Action<TResult> onRelease = null,
  264. IScheduler sched = null)
  265. {
  266. Contract.Requires(calculationFunc != null);
  267. Contract.Requires(maxSize > 0);
  268. sched = sched ?? RxApp.TaskpoolScheduler;
  269. return RegisterMemoizedObservable(x => Observable.Return(calculationFunc(x), sched), maxSize, onRelease, sched);
  270. }
  271. /// <summary>
  272. /// RegisterMemoizedObservable is similar to RegisterAsyncObservable, but
  273. /// caches its results so that subsequent Execute calls with the same
  274. /// CommandParameter will not need to be run in the background.
  275. /// </summary>
  276. /// <param name="calculationFunc">The function that performs the
  277. /// expensive or asyncronous calculation and returns the result.
  278. ///
  279. /// Note that this function *must* return an equivalently-same result given a
  280. /// specific input - because the function is being memoized, if the
  281. /// calculationFunc depends on other varables other than the input
  282. /// value, the results will be unpredictable.
  283. /// </param>
  284. /// <param name="maxSize">The number of items to cache. When this limit
  285. /// is reached, not recently used items will be discarded.</param>
  286. /// <param name="onRelease">This optional method is called when an item
  287. /// is evicted from the cache - this can be used to clean up / manage an
  288. /// on-disk cache; the calculationFunc can download a file and save it
  289. /// to a temporary folder, and the onRelease action will delete the
  290. /// file.</param>
  291. /// <param name="sched">The scheduler to run asynchronous operations on
  292. /// - defaults to TaskpoolScheduler</param>
  293. /// <returns>An Observable representing the items returned by the
  294. /// calculation result. Note that with this method it is possible with a
  295. /// calculationFunc to return multiple items per invocation of Execute.</returns>
  296. public IObservable<TResult> RegisterMemoizedObservable<TResult>(
  297. Func<object, IObservable<TResult>> calculationFunc,
  298. int maxSize = 50,
  299. Action<TResult> onRelease = null,
  300. IScheduler sched = null)
  301. {
  302. Contract.Requires(calculationFunc != null);
  303. Contract.Requires(maxSize > 0);
  304. sched = sched ?? RxApp.TaskpoolScheduler;
  305. var cache = new ObservableAsyncMRUCache<object, TResult>(
  306. calculationFunc, maxSize, _maximumConcurrent, onRelease, sched);
  307. return this.RegisterAsyncObservable(cache.AsyncGet);
  308. }
  309. void marshalFailures<T>(Action<T> block, T param)
  310. {
  311. try {
  312. block(param);
  313. } catch (Exception ex) {
  314. _exSubject.OnNext(ex);
  315. }
  316. }
  317. void marshalFailures(Action block)
  318. {
  319. marshalFailures(_ => block(), Unit.Default);
  320. }
  321. }
  322. public static class ReactiveAsyncCommandMixins
  323. {
  324. /// <summary>
  325. /// This method returns the current number of items in flight.
  326. /// </summary>
  327. public static int CurrentItemsInFlight(this IReactiveAsyncCommand This)
  328. {
  329. return This.ItemsInflight.First();
  330. }
  331. }
  332. }
  333. // vim: tw=120 ts=4 sw=4 et :