Construction, Initialization, and State
DispatcherTimer offers four different constructors. Three of them leave the timer in a non-running state, while the fourth has sufficient parameters to get the timer going.
private object _instanceLock = new object();
public DispatcherTimer() : this(DispatcherPriority.Background) {}
public DispatcherTimer(DispatcherPriority priority)
{
this.Initialize(Dispatcher.CurrentDispatcher, priority, TimeSpan.FromMilliseconds(0.0));
}
public DispatcherTimer(DispatcherPriority priority, Dispatcher dispatcher)
{
// guard conditions elided... not important to our discussion
this.Initialize(dispatcher, priority, TimeSpan.FromMilliseconds(0.0));
}
public DispatcherTimer(TimeSpan interval, DispatcherPriority priority, EventHandler callback, Dispatcher dispatcher)
{
// guard conditions elided... not important to our discussion
this.Initialize(dispatcher, priority, interval);this.Tick += callback;
this.Start();
}
private void Initialize(Dispatcher dispatcher, DispatcherPriority priority, TimeSpan interval)
{
// guard conditions elided... not important to our discussion
this._priority = priority;
this._interval = interval;
}
As you can see, all four constructors call DispatcherTimer.Initialize() to set the initial state of the timer. This method simply sets the first three state fields listed below:
- _dispatcher: The Dispatcher the timer executes on. Can be retrieved through the DispatcherTimer.Dispatcher property but cannot be set after construction.
- _priority: The DispatcherPriority at which ticks from this timer are generated. Cannot be set or retrieved through public interfaces after construction.
- _interval: The TimeSpan between successive ticks from the timer. Can be set or retrieved through the DispatcherTimer.Interval property. Setting the interval while the timer is running may prolong the current tick. Subsequent ticks will adopt the new time characteristic.
- _instanceLock: Thread safety mechanism for the timer. Not accessible externally.
- _isEnabled: Represents the running state of the timer. Can be set or retrieved through the DispatcherTimer.IsEnabled property. Setting IsEnabled to true while the timer is not running has the same effect as calling Start(). Setting IsEnabled to false while the timer is running has the same effect as calling Stop().
- _operation: The DispatcherOperation queued by the timer to represent the current pending tick. Not directly accessible externally.
- _tag: User-supplied data. Not used by the timer itself. Can be set or retrieved through the Dispatcher.Tag property.
Callbacks
The fourth constructor above provides a callback for the timer and sets the timer in motion. If you use any of the other three constructors, you should attach an EventHandler to the DispatcherTimer.Tick event before starting the timer. The callback will occur on the corresponding Dispatcher's thread on the given interval until the timer is stopped. The timing may not be precise since the priority of the timer may force its tick events to occur after other higher-priority operations are serviced.
Starting and Stopping
You can control the running state of the timer through either the DispatcherTimer.IsEnabled property or the DispatcherTimer.Start() or DispatcherTimer.Stop() methods. As stated above, the IsEnabled property is equivalent to calling Start() or Stop(), and in fact calls these methods behind-the-scenes. Likewise, the Start() and Stop() methods need to keep the _isEnabled state consistent:
public bool IsEnabled
{
get { return this._isEnabled; }
set
{
lock (this._instanceLock)
{
if (!value && this._isEnabled) this.Stop();
else
{
if (value && !this._isEnabled) this.Start();
}
}
}
}
public void Start()
{
lock (this._instanceLock)
{
if (!this._isEnabled)
{
this._isEnabled = true;
this.Restart();
}
}
}
public void Stop()
{
bool flag = false;
lock (this._instanceLock)
{
if (this._isEnabled)
{
this._isEnabled = false;
flag = true;
if (this._operation != null)
{
this._operation.Abort();
this._operation = null;
}
}
}
if (flag)
{
this._dispatcher.RemoveTimer(this);
}
}
The activities carried out by the Stop() method are difficult to interpret until we analyze the call to Restart() made by the Start() method. Restart() is the most important method in the function of the timer:
private void Restart()
{
lock (this._instanceLock)
{
if (this._operation == null)
{
this._operation = this._dispatcher.BeginInvoke(DispatcherPriority.Inactive,
new DispatcherOperationCallback(this.FireTick), null);
this._dueTimeInTicks = Environment.TickCount + (int)this._interval.TotalMilliseconds;
if (this._interval.TotalMilliseconds == 0.0 && this._dispatcher.CheckAccess())
{
this.Promote();
}
else
{
this._dispatcher.AddTimer(this);
}
}
}
}
internal void Promote()
{
lock (this._instanceLock)
{
if (this._operation != null)
{
this._operation.Priority = this._priority;
}
}
}
Let's decompose this a bit and make sure we understand what's really happening:
- The _instanceLock is held for the duration of the Restart() call. This -- along with similar locks called by IsEnabled, Start(), and Stop() -- make all timer control calls thread-safe.
- Restart() checks to see if we already have an _operation in play. If we do, then we're already started, and no further action is necessary.
- To operate with the Dispatcher, we need to queue up a DispatcherOperation to represent our tick. This is done by calling the Dispatcher's BeginInvoke() method. The resulting operation has a priority of Inactive, meaning that it is not going to run until its priority is modified.
- Restart() calculates the "due time in ticks", which is the wall clock time when the tick operation should complete. This end time is recorded in an internal field accessible to the Dispatcher.
- If the target interval is immediate and we are already on the Dispatcher's thread, Restart() calls Promote() to modify the tick operation's priority to the previously-supplied DispatcherTimer priority, thereby putting the operation in play.
- If the target interval is not immediate or if we are not on the Dispatcher's thread, Restart() calls the Dispatcher's AddTimer() method to coordinate the handling of the tick operation.
The one aspect we haven't covered yet is how subsequent ticks get queued up. Looking at Restart(), we can see that our tick operation calls the FireTick() method when run. We would expect this method to fire the Tick event and Restart() the timer, and it does:
private object FireTick(object unused)
{
this._operation = null;
if (this.Tick != null)
{
this.Tick(this, EventArgs.Empty);
}
if (this._isEnabled)
{
this.Restart();
}
return null;
}
Conclusion
At this point, we have covered the full anatomy of the DispatcherTimer class, but haven't yet revealed the magic behind tick event dispatch. We will examine how the Dispatcher addresses this capability in my next post.
[Deadmau5: Right this Second -- YouTube]
No comments:
Post a Comment