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