Search code examples
c#.netvisual-studiowinformstimer

C# WinForms forms timer not ticking inside of loop


I created a C# project, a WinForms App with a background class. The WinForms App class contains a single point and draws this point everytime you call the "Refresh()" Method. With a press on the spacebar you get the background class running. The background class provides a "run()" method this method calls a calculation Method a specific number of times (while loop). the calculation method then performs some calculations and adds the results to a stack and then starts a timer. This timer contains a draw method, that takes the data from the stack and tells the Form to print a the circle for every stack entry. (Its quite confusing to describe code is below and should be easy to understand)

Now I got to a quite strange problem I don't understand:(see sample code below) In the background class when the while loop calls the calculation the calculation will be done but the time will not execute the draw method. But when you comment out the while loop start and end and instead press the spacebar manually multiple times everything works fine. Why is the code working without the loop and manually pressing multiple times but not with the while loop? And how can I fix it to get it working with the loop?

Steps to reproduce the problem: Create a new WinForms App in visual studio, replace the Form1.cs with the code below and create a BackgroundRunner.cs class with the code below. Then simply run the project and press the spacebar to get the background class running. To get the code working: Just comment out the while loop start and end and press the spacebar manually multiple times. (the circle shout move)

Additional: Yep in this broken down example it looks weird to create two seperate classes and a forms timer but in the whole project I need to get it working this way.

Thanks in advance.

I've already googled and found solutions like the timer and UI need to be on the same thread etc. But I think both are on the same thread, because it works without the loop.

This is the Form1.cs class:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace TestApp
{
    public partial class Form1 : Form
    {

        private Point p;
        private BackgroundRunner runner;
        private int count = 0;
        private int max = 10;

        public Form1() {
            InitializeComponent();
            p = new Point(0, 0);
            runner = new BackgroundRunner(this);
        }

        private void Form1_Load(object sender, EventArgs e) {

        }

        //Method to refresh the form (draws the single point)
        override
        protected void OnPaint(PaintEventArgs e) {
            drawPoint(p, e.Graphics);
        }

        //Method to draw a point
        private void drawPoint(Point p, Graphics g) {
            Brush b = new SolidBrush(Color.Black);
            g.FillEllipse(b, p.X, p.Y, 10, 10);
        }

        //when you press the space bar the runner will start
        override
        protected void OnKeyDown(KeyEventArgs e) {
            if(e.KeyValue == 32) {
                runner.run();
            }
        }

        public int getCount() {
            return count;
        }

        public int getMax() {
            return max;
        }

        //sets point, refreshes the form and increases the count
        public void setPoint(Point pnew) {
            p = pnew;
            Refresh();
            count++;
        }
    }
}

And this is the BackgroundRunner.cs class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace TestApp
{
    class BackgroundRunner
    {
        private Form1 form;
        private System.Windows.Forms.Timer timer;
        private Stack<int> test;
        private int max;
        private int count;

        public BackgroundRunner(Form1 formtemp) {
            form = formtemp;
            timer = new System.Windows.Forms.Timer();
            timer.Interval = 100;
            timer.Tick += drawMovement;
        }

        public void run() {
            count = form.getCount();
            max = form.getMax();

            //Here happens the strange stuff:
            //If you comment out the while loop start and end 
            //and press manually the space bar the circle will move 
            //from right to left an up to down. 
            //But if you use the while loop nothing will happen 
            //and the "drawMovement" Method will never be executed, 
            //because the message inside will never be written...
            Console.WriteLine("Count: " + count + " Max: " + max);
            while (count < max) {
                Console.WriteLine("Calc Move");
                calculateMovement();

                count = form.getCount();
                max = form.getMax();
                Thread.Sleep(50);
            }
        }

        //Just a method that adds data to the stack and then start the timer
        private void calculateMovement() {
            test = new Stack<int>(5);
            test.Push(10);
            test.Push(20);
            test.Push(30);
            test.Push(40);
            test.Push(50);

            timer.Start();
        }

        //Method that goes through the stack and updates 
        //the forms point incl refresh
        private void drawMovement(object o, EventArgs e) {
            Console.WriteLine("Draw Move");
            if (test.Count <= 0) {
                timer.Stop();
                return;
            }
            int x = test.Pop();
            int y = count;
            form.setPoint(new System.Drawing.Point(x, y));
        }


    }
}


Solution

  • Never block the main thread where the message loop takes place.

    The timer is triggered within the message dispathing, that is why your timer is not triggered while you are blocking the main thread by the loop and sleep.

    Make the method async by changing the public void run() into public async void run()

    Replace Threed.Sleep with await Task.Delay then the loop would work asynchronously.

    This would probably throw Exceptions if your application targets .NET Framewok version 2.0 or later (surely), because it is not thread-safe. To solve this, put all codes that access the UI controls (which could be simply where the exception is thrown from, just in case that you are not sure what they are.) into

    form.Invoke(new Action(()=>{
        //Statements to access the UI controls
    }));
    

    Correction

    The await will return execution on the UI thread because of the SynchronizationContext it is posted to. So there won't be exception about crossthreading.

    by J.vanLangen