Search code examples
c#wpftimer

WPF / C# (No MVVM) Timer with Countdown Clock


Good day all, I'm struggling with a coding problem and I need some help. My process requires a 300 second (5 min) timer that fires an event to refresh a Grid Control. That works just fine. The problem is, I need to countdown the 5 mins/300 seconds to the user so they know the next fresh happens in "X" seconds. The goal is to countdown, refresh, and show the user the next refresh.

All code samples below are examples of things I tried.

I have code that works for the refresh, but something strange happens after the first execution. The timer counts down to 0, refreshes, and then restarts at 300 seconds again (good), but each tick down flashes a second timer behind it. So I see 300, 299, 298, ... and then another 300, 299, 298, ...; therefore, it looks like 300, 299, 298, 300, 297, 299, 296, 298, etc. It's nauseating to watch. Let alone trying to watch 20 minutes in...

My 300-second timer is a System.Timers.Timer int eh below example (Reminder, this works):

public partial class MasterControl
{
private Timer _t;

public MasterControl()
        {
            InitializeComponent();

        }

public void Dispose()
        {
            _t?.Dispose();
            _handle?.Dispose();
        }

        private void Master_OnLoaded(object sender, RoutedEventArgs e)
        {
            var fd = new FillMaster();
            GridMaster.ItemsSource = fd.GridPopulate("TblName", App.UserName);
            _t = new Timer();
            _t.Elapsed += OnTimedEvent;
            _t.Interval = 300000;
            _t.Enabled = true;
        }
        private void OnTimedEvent(object source, ElapsedEventArgs e)
        {
            Dispatcher.Invoke(() =>
            {
                try
                {
                    var fd = new FillMaster();
                    GridMaster.ItemsSource = fd.GridPopulate("TblName", App.UserName);
                }
                catch (SqlException)
                {
                    /*  swallow */
                }
            });
        }
}

What I tried to do was add a countdown that fills a label.

I added

private TimeSpan _time;
private DispatcherTimer _timer;

And I adjusted the OnTimedEvent code to the below example and it didn't work. This is where it started to double up on refresh. I tried GC to see if that would work. No dice.

private void OnTimedEvent(object source, ElapsedEventArgs e)
{
    Dispatcher.Invoke(() =>
    {
        try
        {
            var fd = new FillMaster();
            GridMaster.ItemsSource = fd.GridPopulate("IntakeCheckList", App.UserName);
            TbCountDown.Content = "";
            _time = TimeSpan.FromSeconds(60);

            _timer = new DispatcherTimer(new TimeSpan(0, 0, 1), DispatcherPriority.Normal, delegate
            {
                TbCountDown.Content = _time.ToString("c");
                if (_time == TimeSpan.Zero)
                {
                    _timer.Stop();
                    GC.Collect();
                    GC.WaitForPendingFinalizers();
                    GC.Collect();
                }

                _time = _time.Add(TimeSpan.FromSeconds(-1));
            }, Application.Current.Dispatcher);

            _timer.Start();
        }
        catch (SqlException)
        {
            /*  swallow */
        }
    });
}

I also found some code that casued the same problem.

private void Countdown(int count, TimeSpan interval, Action<int> ts)
        {
            var dt = new DispatcherTimer {Interval = interval};
            dt.Tick += (_, a) =>
            {
                if (count-- == 0)
                    dt.Stop();
                else
                    ts(count);
            };
            ts(count);
            dt.Start();
        }

Then I added the following to the to OnTimedEvent

Countdown(30, TimeSpan.FromSeconds(5), cur => TbCountDown.Content = cur.ToString());

As seen here

private void OnTimedEvent(object source, ElapsedEventArgs e)
        {
            Dispatcher.Invoke(() =>
            {
                Countdown(30, TimeSpan.FromSeconds(5), cur => TbCountDown.Content = cur.ToString());

                try
                {
                    var fd = new FillMaster();
                    GridMaster.ItemsSource = fd.GridPopulate("IntakeCheckList", App.UserName);
                }
                catch (SqlException)
                {
                    /*  swallow */
                }
            });
        }

This also failed with the exact same problem.

Ultimately, is there a way to get the countdown from the System.Timers.Timer or something else you can help me with?

Thank you!


Solution

  • You don't actually need more than a simple DispatcherTimer and a DateTime that is cyclically reset to the current time + 300 seconds.

    public partial class MainWindow : Window
    {
        private readonly DispatcherTimer timer = new DispatcherTimer();
        private DateTime endTime;
    
        public MainWindow()
        {
            InitializeComponent();
    
            timer.Interval = TimeSpan.FromSeconds(1);
            timer.Tick += new EventHandler(OnTimerTick);
            timer.Start();
        }
    
        private void OnTimerTick(object sender, EventArgs e)
        {
            var now = DateTime.Now;
    
            if (endTime < now)
            {
                endTime = now.AddSeconds(300);
            }
    
            label.Content = (endTime - now).ToString(@"mm\:ss");
        }
    }