Search code examples
c#wpfcountdowndispatchertimer

WPF Countdown too slow (DispatcherTimer)


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");
    }
}

Solution

  • 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.