Wednesday, May 4, 2011

WPF: DispatcherTimer - Anatomy

The DispatcherTimer class showed up a few times in the output of my DispatcherHooks sample app, so I want to devote this post to its anatomy.

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._dispatcher = dispatcher;
    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._isEnabledthis.Stop();
            else
            {
                if (value && !this._isEnabledthis.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 Stop() method now makes much more sense, aborting any operation in flight and calling the Dispatcher's RemoveTimer() method to disable the timer. To complete our analysis, we will need to took at these timer-related calls in Dispatcher to see what they do. As it turns out, this is a rather complicated topic and will require a full post to cover in detail; I plan to devote my next post to this.

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