// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT License. // See the LICENSE file in the project root for more information. using System.Reactive.Disposables; using System.Threading; using System.Windows.Forms; namespace System.Reactive.Concurrency { /// /// Represents an object that schedules units of work on the message loop associated with a Windows Forms control. /// public class ControlScheduler : LocalScheduler, ISchedulerPeriodic { private readonly Control _control; /// /// Constructs a ControlScheduler that schedules units of work on the message loop associated with the specified Windows Forms control. /// /// Windows Forms control to get the message loop from. /// is null. /// /// This scheduler type is typically used indirectly through the and method overloads that take a Windows Forms control. /// public ControlScheduler(Control control) { _control = control ?? throw new ArgumentNullException(nameof(control)); } /// /// Gets the control associated with the ControlScheduler. /// public Control Control { get { return _control; } } /// /// Schedules an action to be executed on the message loop associated with the control. /// /// The type of the state passed to the scheduled action. /// State passed to the action to be executed. /// Action to be executed. /// The disposable object used to cancel the scheduled action (best effort). /// is null. public override IDisposable Schedule(TState state, Func action) { if (action == null) { throw new ArgumentNullException(nameof(action)); } if (_control.IsDisposed) { return Disposable.Empty; } var d = new SingleAssignmentDisposable(); _control.BeginInvoke(new Action(() => { if (!_control.IsDisposed && !d.IsDisposed) { d.Disposable = action(this, state); } })); return d; } /// /// Schedules an action to be executed after dueTime on the message loop associated with the control, using a Windows Forms Timer object. /// /// The type of the state passed to the scheduled action. /// State passed to the action to be executed. /// Action to be executed. /// Relative time after which to execute the action. /// The disposable object used to cancel the scheduled action (best effort). /// is null. public override IDisposable Schedule(TState state, TimeSpan dueTime, Func action) { if (action == null) { throw new ArgumentNullException(nameof(action)); } var dt = Scheduler.Normalize(dueTime); if (dt.Ticks == 0) { return Schedule(state, action); } var createTimer = new Func((scheduler1, state1) => { var d = new MultipleAssignmentDisposable(); var timer = new System.Windows.Forms.Timer(); timer.Tick += (s, e) => { var t = Interlocked.Exchange(ref timer, null); if (t != null) { try { if (!_control.IsDisposed && !d.IsDisposed) { d.Disposable = action(scheduler1, state1); } } finally { t.Stop(); action = static (s, t) => Disposable.Empty; } } }; timer.Interval = (int)dt.TotalMilliseconds; timer.Start(); d.Disposable = Disposable.Create(() => { var t = Interlocked.Exchange(ref timer, null); if (t != null) { t.Stop(); action = static (s, t) => Disposable.Empty; } }); return d; }); // // This check is critical. When creating and enabling a Timer object on another thread than // the UI thread, it won't fire. // if (_control.InvokeRequired) { return Schedule(state, createTimer); } else { return createTimer(this, state); } } /// /// Schedules a periodic piece of work on the message loop associated with the control, using a Windows Forms Timer object. /// /// The type of the state passed to the scheduled action. /// Initial state passed to the action upon the first iteration. /// Period for running the work periodically. /// Action to be executed, potentially updating the state. /// The disposable object used to cancel the scheduled recurring action (best effort). /// is null. /// is less than one millisecond. public IDisposable SchedulePeriodic(TState state, TimeSpan period, Func action) { // // Threshold derived from Interval property setter in ndp\fx\src\winforms\managed\system\winforms\Timer.cs. // if (period.TotalMilliseconds < 1) { throw new ArgumentOutOfRangeException(nameof(period)); } if (action == null) { throw new ArgumentNullException(nameof(action)); } var createTimer = new Func((scheduler1, state1) => { var timer = new System.Windows.Forms.Timer(); timer.Tick += (s, e) => { if (!_control.IsDisposed) { state1 = action(state1); } }; timer.Interval = (int)period.TotalMilliseconds; timer.Start(); return Disposable.Create(() => { var t = Interlocked.Exchange(ref timer, null); if (t != null) { t.Stop(); action = static _ => _; } }); }); // // This check is critical. When creating and enabling a Timer object on another thread than // the UI thread, it won't fire. // if (_control.InvokeRequired) { return Schedule(state, createTimer); } else { return createTimer(this, state); } } } }