1

I am using a timer class (wrapper around System.Timers.Timer), which I would like to use to display a 2 minute countdown in a WPF window, accurate to every 10 milliseconds. Basically, I'd like to dispay a string in the format mm\\:ss\\.ff that updates every 10 milliseconds. Here is the class I have:

using System;
using System.Timers;
using Proj.Utilities.Extensions;

namespace Proj.Framework
{
/// <summary>
/// Counts down in wall time, raising events at a specified updated period.
/// </summary>
public sealed class MillisecondTimer
{
    private readonly TimeSpan _initialTime;
    private readonly Timer _timerRep;
    private TimeSpan _timeRemaining;

    /// <summary>
    /// The time remaining in the countdown.
    /// </summary>
    public TimeSpan TimeRemaining
    {
        get { return _timeRemaining; }
        private set
        {
            _timeRemaining = value;
            if (_timeRemaining <= TimeSpan.Zero)
            {
                InvokeCountDownElapsed();
            }
        }
    }

    /// <summary>
    /// True if the timer is currently counting down; false otherwise.
    /// </summary>
    public bool IsCountingDown => _timerRep.Enabled;

    /// <summary>
    /// Raised every time the update period elapses.
    /// </summary>
    public event EventHandler TimeChanged;

    /// <summary>
    /// Raised when the entire countdown elapses.
    /// </summary>
    public event EventHandler CountDownElapsed;

    /// <summary>
    /// Creates a new CountDownTimer.
    /// </summary>
    /// <param name="countDownTime">
    /// The amount of time the timer should count down for.
    /// </param>
    /// <param name="updatePeriod">
    /// The period with which the CountDownTimer should raise events.
    /// </param>
    public MillisecondTimer(TimeSpan countDownTime, TimeSpan updatePeriod)
    {
        _initialTime = countDownTime;
        _timerRep = new Timer(10) { AutoReset = true };


        AttachEventHandlers();
    }

    private void AttachEventHandlers()
    {
        AttachedElapsedEventHandler();
        AttachCountDownElapsedEventHandler();
    }

    private void AttachedElapsedEventHandler()
    {
        _timerRep.Elapsed += OnElapsed;
    }

    private void AttachCountDownElapsedEventHandler()
    {
        CountDownElapsed += OnCountDownElapsed;
    }

    private void InvokeTimeChanged()
    {
        //Defined in Proj.Utilities.Extentions
        TimeChanged.InvokeIfInstantiated(this, new EventArgs());
    }

    private void InvokeCountDownElapsed()
    {
        CountDownElapsed.InvokeIfInstantiated(this, new EventArgs());
    }

    private void OnElapsed(object sender, ElapsedEventArgs e)
    {
        TimeRemaining -= TimeSpan.FromMilliseconds(10);
        InvokeTimeChanged();
    }

    private void OnCountDownElapsed(object sender, EventArgs e)
    {
        Stop();
    }

    /// <summary>
    /// Restarts the countdown.
    /// </summary>
    public void Restart()
    {
        TimeRemaining = _initialTime;
        _timerRep.Start();
        InvokeTimeChanged();
    }

    /// <summary>
    /// Stops the countdown.
    /// </summary>
    public void Stop()
    {
        _timerRep.Stop();
    }
}
}

It works, in that it does what I expect it to do, but it ends up being too slow. When I want it to countdown from 10 seconds, it takes about 15 seconds. The ultimate goal is a class that can count down from 2 minutes with 10 millisecond resolution, accurately. Why does this take longer than it should, and what could I do to make it work better?

ms5991
  • 201
  • 2
  • 9
  • Some related info here http://stackoverflow.com/questions/3744032/why-are-net-timers-limited-to-15-ms-resolution – Steve Oct 26 '15 at 17:36
  • "*accurate to every 10 milliseconds"* Not with windows, I think the minimum resolution is about 15 milliseconds, and scheduling/preemption can push that much higher. You may be seeing the result of that (which is why 10 seconds looks like it takes 15). If you want an accurate 10 second timer (with variable update intervals) then you need two timers, one for the whole period and one for the update, or compare against a date/time but that has resolution issues as well. – Ron Beyer Oct 26 '15 at 17:57

2 Answers2

3

Don't change your TimeRemaining each time - keep the starting time and compare to the current time to get TimeRemaining:

TimeRemaining = (DateTime.Now - _initialTime);

That way any error you get from the Timer is not accumulated but evens out in the end.

D Stanley
  • 149,601
  • 11
  • 178
  • 240
  • Perfect! The only thing is that it needs to be `var now = DateTime.Now;` `TimeRemaining = (_initialTime - (now.TimeOfDay - _started));` `InvokeTimeChanged();` so that types work out. `_started` is the time when the `Restart()` method was called. Thanks! – ms5991 Oct 26 '15 at 17:58
  • Bugfix aside; the resolution provided by DateTime.Now is insufficient for the users requirements. – Trevor Ash Oct 26 '15 at 19:02
  • @Atoms Maybe not to that exact specification, but most screens can't even _display_ changes every 10 ms (60Hz = ~17MS) So I doubt that the 10ms is a hard requirement. – D Stanley Oct 26 '15 at 19:14
0

I once wrote a wrapper around the Multimedia Timer API which can be accurate up 1 ms, see here. Not tested for production use, so caveat emptor.

However, your approach seems incorrect to me. Ask yourself if you really need accurate 10 ms timing to display a countdown for a 2 minute interval. You seem to be putting two responsibilities on the timer. Keeping accurate time and raising events periodically. Timers are good only for latter of these two. Use Stopwatch to keep time accurately. Use a timer to take the elapsed time from the stopwatch and update a display periodically. That way time is kept accurately regardless of the accuracy of the timer. Users aren't going to notice slight deviations in how long the timer takes to elapse then and aren't going to care if it updates every 10 or 20ms.

Community
  • 1
  • 1
Mike Zboray
  • 39,828
  • 3
  • 90
  • 122