2

I'm creating a countdown timer that allows the user to choose a valid time, and once the countdown reaches zero, it plays a sound.

I've instantiated a timer from the System.Timers namespace and set its Interval to 1 second. I convert the user-specified time to seconds, and decrement that value by 1 every time the Timer.Elapsed function hits. Once it reaches zero, that means the countdown has reached zero which means it's time to play the sound.

However, whenever it doesn't reach zero, I decrement the time value by 1 and I also want to increment the ProgressBar by using the progressbar.Increment function.

Unfortunately, whenever I do this, it gives me an exception having to do with multithreading issues. I know what is wrong, but I am not sure how to fix it. Do I need to start the timer.Elapsed function on a new thread?

The error is:

Cross-thread operation not valid: Control 'CountdownProgress' accessed from a thread other than the thread it was created on.

Also, any tips on better programming habits are welcome as well. Many thanks!

Here is the code:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Timers;
using System.Media;
using System.Diagnostics;
using System.Threading;

namespace WindowsFormsApplication1
{
    public partial class Form_CDA : Form
    {
        public Form_CDA()
        {
            InitializeComponent();
        }

        private bool m_CountdownActive; // declare global variables and instantiate timer.
        private bool m_Cancelled;
        public decimal seconds;
        public decimal minutes;
        public decimal hours;
        public decimal time;
        public System.Timers.Timer timer = new System.Timers.Timer();

        private void Form_CDA_Load(object sender, EventArgs e)
        {
            m_Cancelled = false;
            timer.AutoReset = false;
            timer.Interval = 0;
            m_CountdownActive = false;
            richTextBox1.Text = "Hello, please select an hour between 0 and 100, a minute between 0 and 59, and a second between 0 and 59. The countdown timer will play a sound when finished.";
            btn_Cancel.Enabled = false;
            seconds = 0;
            minutes = 0;
            hours = 0;
            time = 0;
            m_StatusBar.Text = "Program properly loaded, waiting for user input"; // initialize variables.
        }

        private void btn_SetCountdown_Click(object sender, EventArgs e)
        {
            seconds = numUpDown_Seconds.Value;
            minutes = numUpDown_Minutes.Value;
            hours = numUpDown_Hours.Value;
            time = (hours * 3600) + (minutes * 60) + seconds; // calculate the total time in seconds.
            if (time != 0) // if time is not zero, start timer and set up event handler timer.elapsed.
            {
                timer.Interval = 1000;
                timer.AutoReset = true;
                timer.Start();
                timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
                CountdownProgress.Maximum = (int)time;
                CountdownProgress.Minimum = 0;
                CountdownProgress.Value = 0;
            }
            else
            {
                m_StatusBar.Text = "Invalid selection of times. Try again.";
                return;
            }
            DateTime dt = DateTime.Now;
            dt.AddSeconds((double)time);
            Label_Countdown.Text = "Finishing time: " + dt.ToString(); // display end time to user.
            m_CountdownActive = true;
            btn_Cancel.Enabled = true;
            btn_SetCountdown.Enabled = false;
            numUpDown_Hours.Enabled = false;
            numUpDown_Minutes.Enabled = false;
            numUpDown_Seconds.Enabled = false; // disable controls.
            m_Cancelled = true;
            m_StatusBar.Text = "Timer set to " + numUpDown_Hours.Value.ToString() + " hours, " + numUpDown_Minutes.Value.ToString() + " minutes, " + numUpDown_Seconds.Value.ToString() + " seconds.";
        }

        void timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            if (time == 0)
            {
                m_StatusBar.Text = "Countdown Finished";
                SoundPlayer soundPlayer = new SoundPlayer(@"C:\Users\marupakuuu\Desktop\New stuff\doorbell.wav");
                soundPlayer.Play(); // play sound.
                timer.Stop();
                return;
            }
            else
            {
                time = time - 1;
                CountdownProgress.Increment(1); // exception occurs here; multithreading issues.
            }
        }

        private void btn_Cancel_Click(object sender, EventArgs e)
        {
            // if user wishes to stop the countdown to start a new one.
            m_Cancelled = true;
            m_CountdownActive = false;
            btn_SetCountdown.Enabled = true;
            numUpDown_Seconds.Value = 0;
            numUpDown_Minutes.Value = 0;
            numUpDown_Hours.Value = 0;
            numUpDown_Hours.Enabled = true;
            numUpDown_Minutes.Enabled = true;
            numUpDown_Seconds.Enabled = true;
            btn_Cancel.Enabled = false;
            m_StatusBar.Text = "Countdown cancelled";
        }
    }
}
jay_t55
  • 11,362
  • 28
  • 103
  • 174
tf.rz
  • 1,347
  • 6
  • 18
  • 47

1 Answers1

8

A control can only be accessed within the thread it was created. Therefore use Invoke to execute the given code as a delegate on the main thread, where the control (progressbar) was created.

void timer_Elapsed(object sender, ElapsedEventArgs e)
{
    if (time == 0)
    {
        m_StatusBar.Text = "Countdown Finished";
        SoundPlayer soundPlayer = new SoundPlayer(@"C:\Users\marupakuuu\Desktop\New stuff\doorbell.wav");
        soundPlayer.Play(); // play sound.
        timer.Stop();
        return;
    }
    else
    {
        time = time - 1;
        Invoke(new Action(() => CountdownProgress.Increment(1)));
    }
}

You may want to read http://msdn.microsoft.com/en-us/library/zyzhdc6b.aspx

ebb
  • 9,297
  • 18
  • 72
  • 123
  • Thank you so much! Workin' perfectly. Currently reading the article as well. Brilliant! :) – tf.rz Oct 30 '11 at 22:09
  • @tf.rz However, the real question is *why* is the CountdownProgress on another thread? The WinForm Timer control runs the event on the thread the timer was created. –  Oct 30 '11 at 22:11
  • 1
    @pst, See http://msdn.microsoft.com/en-us/library/system.timers.timer.elapsed.aspx - it says: `If the SynchronizingObject property is Nothing, the Elapsed event is raised on a ThreadPool thread.` – ebb Oct 30 '11 at 22:12
  • @ebb Good point. I was considering [System.Windows.Forms.Timer](http://msdn.microsoft.com/en-us/library/system.windows.forms.timer.aspx), which I recommend for UI work in general... –  Oct 30 '11 at 23:58