ControlScheduler.cs 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT License.
  3. // See the LICENSE file in the project root for more information.
  4. using System.Reactive.Disposables;
  5. using System.Threading;
  6. using System.Windows.Forms;
  7. namespace System.Reactive.Concurrency
  8. {
  9. /// <summary>
  10. /// Represents an object that schedules units of work on the message loop associated with a Windows Forms control.
  11. /// </summary>
  12. public class ControlScheduler : LocalScheduler, ISchedulerPeriodic
  13. {
  14. private readonly Control _control;
  15. /// <summary>
  16. /// Constructs a ControlScheduler that schedules units of work on the message loop associated with the specified Windows Forms control.
  17. /// </summary>
  18. /// <param name="control">Windows Forms control to get the message loop from.</param>
  19. /// <exception cref="ArgumentNullException"><paramref name="control"/> is null.</exception>
  20. /// <remarks>
  21. /// This scheduler type is typically used indirectly through the <see cref="Linq.ControlObservable.ObserveOn{TSource}"/> and <see cref="Linq.ControlObservable.SubscribeOn{TSource}"/> method overloads that take a Windows Forms control.
  22. /// </remarks>
  23. public ControlScheduler(Control control)
  24. {
  25. _control = control ?? throw new ArgumentNullException(nameof(control));
  26. }
  27. /// <summary>
  28. /// Gets the control associated with the ControlScheduler.
  29. /// </summary>
  30. public Control Control
  31. {
  32. get { return _control; }
  33. }
  34. /// <summary>
  35. /// Schedules an action to be executed on the message loop associated with the control.
  36. /// </summary>
  37. /// <typeparam name="TState">The type of the state passed to the scheduled action.</typeparam>
  38. /// <param name="state">State passed to the action to be executed.</param>
  39. /// <param name="action">Action to be executed.</param>
  40. /// <returns>The disposable object used to cancel the scheduled action (best effort).</returns>
  41. /// <exception cref="ArgumentNullException"><paramref name="action"/> is null.</exception>
  42. public override IDisposable Schedule<TState>(TState state, Func<IScheduler, TState, IDisposable> action)
  43. {
  44. if (action == null)
  45. {
  46. throw new ArgumentNullException(nameof(action));
  47. }
  48. if (_control.IsDisposed)
  49. {
  50. return Disposable.Empty;
  51. }
  52. var d = new SingleAssignmentDisposable();
  53. _control.BeginInvoke(new Action(() =>
  54. {
  55. if (!_control.IsDisposed && !d.IsDisposed)
  56. {
  57. d.Disposable = action(this, state);
  58. }
  59. }));
  60. return d;
  61. }
  62. /// <summary>
  63. /// Schedules an action to be executed after dueTime on the message loop associated with the control, using a Windows Forms Timer object.
  64. /// </summary>
  65. /// <typeparam name="TState">The type of the state passed to the scheduled action.</typeparam>
  66. /// <param name="state">State passed to the action to be executed.</param>
  67. /// <param name="action">Action to be executed.</param>
  68. /// <param name="dueTime">Relative time after which to execute the action.</param>
  69. /// <returns>The disposable object used to cancel the scheduled action (best effort).</returns>
  70. /// <exception cref="ArgumentNullException"><paramref name="action"/> is null.</exception>
  71. public override IDisposable Schedule<TState>(TState state, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> action)
  72. {
  73. if (action == null)
  74. {
  75. throw new ArgumentNullException(nameof(action));
  76. }
  77. var dt = Scheduler.Normalize(dueTime);
  78. if (dt.Ticks == 0)
  79. {
  80. return Schedule(state, action);
  81. }
  82. var createTimer = new Func<IScheduler, TState, IDisposable>((scheduler1, state1) =>
  83. {
  84. var d = new MultipleAssignmentDisposable();
  85. var timer = new System.Windows.Forms.Timer();
  86. timer.Tick += (s, e) =>
  87. {
  88. var t = Interlocked.Exchange(ref timer, null);
  89. if (t != null)
  90. {
  91. try
  92. {
  93. if (!_control.IsDisposed && !d.IsDisposed)
  94. {
  95. d.Disposable = action(scheduler1, state1);
  96. }
  97. }
  98. finally
  99. {
  100. t.Stop();
  101. action = static (s, t) => Disposable.Empty;
  102. }
  103. }
  104. };
  105. timer.Interval = (int)dt.TotalMilliseconds;
  106. timer.Start();
  107. d.Disposable = Disposable.Create(() =>
  108. {
  109. var t = Interlocked.Exchange(ref timer, null);
  110. if (t != null)
  111. {
  112. t.Stop();
  113. action = static (s, t) => Disposable.Empty;
  114. }
  115. });
  116. return d;
  117. });
  118. //
  119. // This check is critical. When creating and enabling a Timer object on another thread than
  120. // the UI thread, it won't fire.
  121. //
  122. if (_control.InvokeRequired)
  123. {
  124. return Schedule(state, createTimer);
  125. }
  126. else
  127. {
  128. return createTimer(this, state);
  129. }
  130. }
  131. /// <summary>
  132. /// Schedules a periodic piece of work on the message loop associated with the control, using a Windows Forms Timer object.
  133. /// </summary>
  134. /// <typeparam name="TState">The type of the state passed to the scheduled action.</typeparam>
  135. /// <param name="state">Initial state passed to the action upon the first iteration.</param>
  136. /// <param name="period">Period for running the work periodically.</param>
  137. /// <param name="action">Action to be executed, potentially updating the state.</param>
  138. /// <returns>The disposable object used to cancel the scheduled recurring action (best effort).</returns>
  139. /// <exception cref="ArgumentNullException"><paramref name="action"/> is null.</exception>
  140. /// <exception cref="ArgumentOutOfRangeException"><paramref name="period"/> is less than one millisecond.</exception>
  141. public IDisposable SchedulePeriodic<TState>(TState state, TimeSpan period, Func<TState, TState> action)
  142. {
  143. //
  144. // Threshold derived from Interval property setter in ndp\fx\src\winforms\managed\system\winforms\Timer.cs.
  145. //
  146. if (period.TotalMilliseconds < 1)
  147. {
  148. throw new ArgumentOutOfRangeException(nameof(period));
  149. }
  150. if (action == null)
  151. {
  152. throw new ArgumentNullException(nameof(action));
  153. }
  154. var createTimer = new Func<IScheduler, TState, IDisposable>((scheduler1, state1) =>
  155. {
  156. var timer = new System.Windows.Forms.Timer();
  157. timer.Tick += (s, e) =>
  158. {
  159. if (!_control.IsDisposed)
  160. {
  161. state1 = action(state1);
  162. }
  163. };
  164. timer.Interval = (int)period.TotalMilliseconds;
  165. timer.Start();
  166. return Disposable.Create(() =>
  167. {
  168. var t = Interlocked.Exchange(ref timer, null);
  169. if (t != null)
  170. {
  171. t.Stop();
  172. action = static _ => _;
  173. }
  174. });
  175. });
  176. //
  177. // This check is critical. When creating and enabling a Timer object on another thread than
  178. // the UI thread, it won't fire.
  179. //
  180. if (_control.InvokeRequired)
  181. {
  182. return Schedule(state, createTimer);
  183. }
  184. else
  185. {
  186. return createTimer(this, state);
  187. }
  188. }
  189. }
  190. }