Reputation: 47
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");
}
}
Upvotes: 0
Views: 2755
Reputation: 2163
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.
Upvotes: 1
Reputation: 36
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.
Upvotes: 0