0

I am currently writing a WPF application that I want to have a Countdown Timer in it. Here is my CountDown class:

 internal class CountDown : INotifyPropertyChanged
{
    private readonly DispatcherTimer _timer;
    private string _currentTimeString;
    private TimeSpan _runTime;
    private TimeSpan _timeleft;

    public CountDown(TimeSpan runTime)
    {
        if (runTime == null) throw new ArgumentNullException("runTime");
        _runTime = runTime;

        _timer = new DispatcherTimer();
        _timer.Interval = new TimeSpan(0, 0, 0, 0, 10);

        _timer.Tick += Update;
    }

    public CountDown(TimeSpan runTime, TimeSpan interval)
    {
        if (runTime == null) throw new ArgumentNullException("runTime");
        _runTime = runTime;

        _timer = new DispatcherTimer();

        if (interval == null) throw new ArgumentNullException("interval");
        _timer.Interval = interval;

        _timer.Tick += Update;
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public string CurrentTimeString
    {
        get { return _currentTimeString; }
        private set
        {
            _currentTimeString = value;
            NotifyPropertyChanged();
        }
    }

    public void Start()
    {
        var task = new Task(_timer.Start);
        _timeleft = _runTime;
        task.Start();
    }

    private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
    private void Update(object sender, EventArgs e)
    {
        _timeleft -= _timer.Interval;

        DateTime newTime = new DateTime();
        newTime = DateTime.MinValue;
        newTime += _timeleft;

        CurrentTimeString = newTime.ToString("mm:ss:ff");
    }
}

Composition Root:

public MainWindow()
    {
        CountDown countDown = new CountDown(new TimeSpan(0, 1, 0));

        InitializeComponent();
        tb1.DataContext = countDown; //tb1 = TextBlock
        countDown.Start();
    }

Everything is working fine except when I set the interval to like 10ms, then it's slower than real seconds. How can I fix this?

EDIT: I can't answer my own questions yet, so here it goes: I completely rewrote my class without using any timers. Found out that these aren't accurate enough for me.

public class CountDown : INotifyPropertyChanged
{
    private string _currentTimeString;
    private TimeSpan _runTime;
    private bool _shouldStop;
    private DateTime _timeToStop;
    private TimeSpan _updateInterval;

    public CountDown(TimeSpan runTime)
    {
        if (runTime == null) throw new ArgumentNullException("runTime");
        _runTime = runTime;

        _updateInterval = new TimeSpan(0, 0, 0, 0, 10);

        Tick += Update;
    }

    public CountDown(TimeSpan runTime, TimeSpan updateInterval)
    {
        if (runTime == null) throw new ArgumentNullException("runTime");
        _runTime = runTime;

        if (updateInterval == null) throw new ArgumentNullException("updateInterval");
        _updateInterval = updateInterval;

        Tick += Update;
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public event Action Tick;

    public string CurrentTimeString
    {
        get { return _currentTimeString; }
        set
        {
            _currentTimeString = value;
            NotifyPropertyChanged();
        }
    }
    public void Start()
    {
        _shouldStop = false;
        _timeToStop = DateTime.Now + _runTime;
        var task = new Task(GenerateTicks);
        task.Start();
    }

    public void Stop()
    {
        _shouldStop = true;
    }

    private void GenerateTicks()
    {
        while (_shouldStop == false)
        {
            if (Tick != null)
                Tick();

            Thread.Sleep(_updateInterval);
        }
    }

    private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    private void Update()
    {
        var timeLeft = _timeToStop - DateTime.Now;

        if (timeLeft <= TimeSpan.Zero)
        {
            _shouldStop = true;
            return;
        }

        var timeLeftDate = DateTime.MinValue + timeLeft;

        CurrentTimeString = timeLeftDate.ToString("mm:ss:ff");
    }
}
refl3x
  • 47
  • 3
  • 10
  • 1
    Timers can only tick 64 times per second. 15.625 milliseconds, a core number in the operating system, that's the clock interrupt rate. So your timer will *always* tick late. Use wall time instead, store the value of DateTime.UtcNow when you start the timer. When it ticks, use DateTime.UtcNow again and subtract the stored value. – Hans Passant Jul 27 '14 at 23:09
  • About the edit: Why do you use a Task to generate the ticks? Why not just use the already existing timer? I'm not that into Threading, but I guess this is really bad approach of doing this. This spins up the CPU massively. http://stackoverflow.com/questions/3886171/why-thread-sleep-is-so-cpu-intensive – SharpShade Jul 28 '14 at 22:13
  • I've read somewhere in MSDN that timers aren't guaranteed to raise an event when they should, just that they don't raise it before(!) they should. Which explains why my countdown was so slow. The sleep method stops the thread for exactly that time. Plus, the professor I had this semester did it this way once. EDIT: in the question you linked, doesn't the guy in the second answer say that it isn't actually that expensive? – refl3x Jul 29 '14 at 11:12
  • I depends on the case. Using timers is still preferable. I haven't heard of not raising events of timers. And even if it misses one, you still won't notice it. – SharpShade Jul 29 '14 at 17:00

2 Answers2

1

First of all you don't need Tasks in order to accomplish a countdown. If you use a timer which ticks every 50ms you won't block anything. Faster ticks than 50ms won't make sense, because I guess your countdown shows hours, minutes or seconds. Milliseconds are a bit too much for a timer, isn't it? And even if you want to display the ms-range the human eye won't notice whether the countdown was updated every 10 or 50ms.

Next it would probably be easier to handle if you used DateTime as time-base. It makes it easier to calculate the actually remaining time.

using System;
using System.Timers;

public class Countdown
{
    private readonly TimeSpan countdownTime;
    private readonly Timer timer;
    private DateTime startTime;

    public Countdown(TimeSpan countdownTime)
    {
        this.countdownTime = countdownTime;
        this.timer = new Timer(10);
    }

    public string RemainingTime { get; private set; }

    public void Start()
    {
        this.startTime = DateTime.Now;
        this.timer.Start();
    }

    private void Timer_Tick(object state)
    {
        var now = DateTime.Now;
        var difference = now - this.startTime;
        var remaining = this.countdownTime - difference;
        if (remaining < TimeSpan.Zero)
        {
            this.timer.Stop();
            // Raise Event or something
        }

        this.RemainingTime = remaining.ToString("mm:ss:fff");
    }
}

An asynchronous countdown would be a bit overpowered for this situation. But if you require it, it's easily upgraded.

WhileTrueSleep
  • 1,524
  • 1
  • 19
  • 32
SharpShade
  • 1,761
  • 2
  • 32
  • 45
  • I have already found out how to do it without any timers, but thank you anyway! Edited my question till now because I can't answer my own question yet. – refl3x Jul 27 '14 at 23:12
  • To clear things up: The countdown shows minutes, seconds and milliseconds. I want the fast update because that reminds me of a stopwatch I had as a child. – refl3x Jul 29 '14 at 11:17
0

Your Countdown class has 2 parameters in the constructor. Your single parameter constructor is not being called in this case (the one you had with 10ms). You ARE supplying 1s (new TimeSpan(0,0,1)) for the interval in the second parameter, so that is what you see in the UI when you run it.

SeanW
  • 36
  • 4
  • Yes I know, but thats not what i meant. Should have changed that. When I let it work with 10ms, it counts slower than with 1s. – refl3x Jul 27 '14 at 22:39