2

I'm writing a simple game that uses timers from the system.threading namespace to simulate wait times for actions. My goal is to have the timer execute once every second for x amount of seconds. To achieve this I added a counter in the timer callback.

The problem is any code I place after invoking the DeliveryProgressChangedEvent event seems to get ignored. My counter is never incremented thus allowing the timer to run forever.

If I invoke the event after I increment the counter, everything works fine. Just nothing after invoking the event will execute. Rather than going the easy route I'd like to understand if not resolve this problem.

I did a fair bit of research into the system.threading timer object as well as events but wasn't able to find any information related to my issue.

I created a bare bones example of my project to demonstrate the issue below.

Game Class

    class Game
    {
        private Timer _deliveryTimer;
        private int _counter = 0;

        public event EventHandler DeliveryProgressChangedEvent;
        public event EventHandler DeliveryCompletedEvent;

        public Game()
        {
            _deliveryTimer = new Timer(MakeDelivery);
        }

        public void StartDelivery()
        {
            _deliveryTimer.Change(0, 1000);
        }

        private void MakeDelivery(object state)
        {
            if (_counter == 5)
            {
                _deliveryTimer.Change(0, Timeout.Infinite);
                DeliveryCompletedEvent?.Invoke(this, EventArgs.Empty);
            }

            DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);

            ++_counter;
        }
    }

Form Class

    public partial class Form1 : Form
    {
        Game _game = new Game();

        public Form1()
        {
            InitializeComponent();

            _game.DeliveryProgressChangedEvent += onDeliveryProgressChanged;
            _game.DeliveryCompletedEvent += onDeliveryCompleted;

            pbDelivery.Maximum = 5;
        }

        private void onDeliveryProgressChanged(object sender, EventArgs e)
        {
            if (InvokeRequired)
                pbDelivery.BeginInvoke((MethodInvoker)delegate { pbDelivery.Increment(1); });

            MessageBox.Show("Delivery Inprogress");
        }

        private void onDeliveryCompleted(object sender, EventArgs e)
        {
            MessageBox.Show("Delivery Completed");
        }

        private void button1_Click(object sender, EventArgs e)
        {
            _game.StartDelivery();
        }
    }

EDIT

Just to clarify what I mean. Any code I put after DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty); will not execute. In my example ++_counter will not run. The event does fire and the onDeliveryProgressChanged handler does run.

Lance U. Matthews
  • 15,725
  • 6
  • 48
  • 68
Andronomos
  • 146
  • 3
  • 14
  • What does "any code I place after invoking the DeliveryProgressChangedEvent event" mean? `++_counter` doesn't get executed? Or `onDeliveryProgressChanged()`? Or `MakeDelivery()`? Also, I think `if (InvokeRequired)` should have an `else` branch so `pbDelivery`, which appears to be a `ProgressBar`, still gets updated even when `InvokeRequired` is `false`. – Lance U. Matthews Feb 29 '20 at 06:58
  • @BACON Any code I put after `DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);` will not execute. In my example `++_counter` will not run. As for adding an else statement to that check, I purposely chose not to because with the way I've designed the game it's impossible for the invoke to not be required. – Andronomos Feb 29 '20 at 07:01
  • But `DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);` does result in `onDeliveryProgressChanged()` being called? The message box appears? – Lance U. Matthews Feb 29 '20 at 07:03
  • Correct, the event does fire and `onDeliveryProgressChanged()` does get called. Just back in my timer callback nothing happens afterwards. – Andronomos Feb 29 '20 at 07:06
  • Remove the MessageBoxes, use `Console.WriteLine()` instead. Btw, the `DeliveryProgressChangedEvent` and `DeliveryCompletedEvent` sequence should be probably inverted. Add means to `Dispose()` of the Timer if you haven't. Also, always keep in mind that you're working on different threads here. – Jimi Feb 29 '20 at 07:13
  • Removing the MessageBoxes and using `Console.WriteLine()` does seems to have fixed it. I guess they were blocking the UI thread. Though the `Console.WriteLine` never appeared in the output, weird. `DeliveryProgressChangedEvent` will cause things to happen on the UI. I need them to not happen if the delivery is done so I do the check first. – Andronomos Feb 29 '20 at 07:19
  • Just tested it. In `onDeliveryProgressChanged` remove `if (InvokeRequired)`, the MessageBox and change this: `pbDelivery.BeginInvoke(new Action(() => pbDelivery.Increment(1)));`. Then add `Console.WriteLine("Delivery Inprogress");`. Remove the MessageBox in `onDeliveryCompleted` and add `Console.WriteLine("Delivery Completed");` In `MakeDelivery()` add `Console.WriteLine("Counter ++");` before `++_counter;` – Jimi Feb 29 '20 at 07:24
  • According to [How to safely show messagebox in multi-thread application?](https://stackoverflow.com/a/29583883/150605) and [How to Invoke MessageBox on background thread in VB.NET?](https://stackoverflow.com/a/21514043/150605), you should `BeginInvoke()` the `MessageBox.Show()` call. – Lance U. Matthews Feb 29 '20 at 07:25
  • @BACON Thank you, I was not aware of that. One of my game's events sole purpose is to pass a string to the main thread to display in a `MessageBox` so that is extremely useful. @Jimi Thank you, my knowledge of actions is limited to WPF and MVVM so I will look into them some more. – Andronomos Feb 29 '20 at 07:28
  • Yes, well, **remove if (InvokeRequired)** (you don't need it anyway, BeginInvoke is enough). You don't really need MessageBoxes. They're also ugly :) IMO, find another notification method. – Jimi Feb 29 '20 at 07:30
  • This is a simple game I am building mostly as a learning exercise. I would be using WPF and MVVM if I wanted something pretty, lol. I am curious though, what would you suggest for a WinForms app to replace the MessageBox? – Andronomos Feb 29 '20 at 07:33
  • Keep in mind I am only using `MessageBox` in the example above for debugging though I do have one game event for displaying information to the player that does use a `MessageBox` as part of the design. – Andronomos Feb 29 '20 at 07:36
  • To replace the MessageBox? A translucent Panel overlay for example. It's not WPF that allows to do *something pretty*. It's your imagination (well, knowing your tools also contributes :). But you could also use a custom Form, avoid showing it as Modal and run it on the main UI Thread. – Jimi Feb 29 '20 at 07:39
  • True, overriding `OnPaint` and building custom control classes can be pretty fun. WPF sure makes it 10 times easier though with proper support out of the box. – Andronomos Feb 29 '20 at 07:43
  • Can one of you post an answer for me to accept? – Andronomos Feb 29 '20 at 07:48
  • @BACON @JIMI You both figured out it was the `MessageBox` causing the problem so either of you. – Andronomos Feb 29 '20 at 08:44

2 Answers2

5

The problem:
Using a System.Threading.Timer class, when the TimerCallback is called, events are raised, to notify the subscribers of the DeliveryProgressChangedEvent and DeliveryCompletedEvent of custom Game class of the progress of a procedure and the termination of it.

In the sample class, the subscriber (a Form class, here) updates an UI, settings the value of a ProgressBar control and also showing a MessageBox (used in the actual implementation of the class sample shown here).

It appears that after the first event is invoked:

DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);
++_counter;

the line where the _counter should be increased is never reached, thus the code that inspects the _counter to set the Timer to a new value is never executed.

What happens:

  1. The System.Threading.Timer is served by ThreadPool threads (more than one). Its callback is called on a thread other than the UI thread. The events invoked from the callback are also raised in a ThreadPool thread.
    The code in the handler delegate, onDeliveryProgressChanged, is then run on the same Thread.

     private void onDeliveryProgressChanged(object sender, EventArgs e)
     { 
         if (InvokeRequired)
             pbDelivery.BeginInvoke((MethodInvoker)delegate { pbDelivery.Increment(1); });
         MessageBox.Show("Delivery Inprogress");
     }
    

    When the MessageBox is shown - it's a Modal Window - it blocks the Thread from where it's run, as usual. The code following the line where the event is invoked is never reached, so _counter is never increased:

     DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);
     ++_counter;
    
  2. The System.Threading.Timer can be served by more than one thread. I'm just quoting the Docs on this point, it's quite straightforward:

    The callback method executed by the timer should be reentrant, because it is called on ThreadPool threads. The callback can be executed simultaneously on two thread pool threads if the timer interval is less than the time required to execute the callback, or if all thread pool threads are in use and the callback is queued multiple times.

    What happens, in practice, is that while the Thread where the CallBack is executed, is blocked by the MessageBox, this doesn't stop the Timer from executing the CallBack from another thread: a new MessageBox is shown when the event is invoked and it keeps on running until it has resources.

  3. The MessageBox has no Owner. When a MessageBox is shown without specifying the Owner, its class uses GetActiveWindow() to find an Owner for the MessageBox Window. This function tries to return the handle of the active window attached to the calling thread's message queue. But the thread from which the MessageBox is run has no active Window, as a consequence, the Owner is the Desktop (actually, IntPtr.Zero here).

This can be manually verified by activating (clicking on) the Form where the MessageBox is called: the MessageBox Window will disappear under the Form, since it's not owned by it.

How to solve:

  1. Of course, using another Timer. The System.Windows.Forms.Timer (WinForms) or the DispatcherTimer (WPF) are the natural substitutes. Their events are raised in the UI Thread.

► The code presented here is just a WinForms implementation made to reproduce a problem, hence these may not apply to all contexts.

  1. Use a System.Timers.Timer: the SynchronizingObject property provides means to marshal the events back to the Thread that created the current class instance (same consideration in relation to the concrete implementation context).

  2. Generate an AsyncOperation using the AsyncOperationManager.CreateOperation() method, then use a SendOrPostCallback delegate to let the AsyncOperation call the SynchronizationContext.Post() method (classic BackGroundWorker style).

  3. BeginInvoke() the MessageBox, attaching it to the UI Thread SynchronizationContext. E.g.,:

     this.BeginInvoke(new Action(() => MessageBox.Show(this, "Delivery Completed")));
    

    Now the MessageBox is owned by the Form and it will behave as usual. The ThreadPool thread is free to continue: the Modal Window is synched with the UI Thread.

  4. Avoid using a MessageBox for this kind of notifications, since it's really annoying :) There are many other ways to notify a User of status changes. The MessageBox is probably the less thoughtful.

To make them work as intended, without changing the current implementation, the Game and Form1 classes can be refactored like this:

class Game
{
    private System.Threading.Timer deliveryTimer = null;
    private int counter;

    public event EventHandler DeliveryProgressChangedEvent;
    public event EventHandler DeliveryCompletedEvent;

    public Game(int eventsCount) { counter = eventsCount; }

    public void StartDelivery() {
        deliveryTimer = new System.Threading.Timer(MakeDelivery);
        deliveryTimer.Change(1000, 1000);
    }

    public void StopDelivery() {
        deliveryTimer?.Dispose();
        deliveryTimer = null;
    }

    private void MakeDelivery(object state) {
        if (deliveryTimer is null) return;
        DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);
        counter -= 1;

        if (counter == 0) {
            deliveryTimer?.Dispose();
            deliveryTimer = null;
            DeliveryCompletedEvent?.Invoke(this, EventArgs.Empty);
        }
    }
}


public partial class Form1 : Form
{
    Game game = null;

    public Form1() {
        InitializeComponent();
        pbDelivery.Maximum = 5;

        game = new Game(pbDelivery.Maximum);
        game.DeliveryProgressChangedEvent += onDeliveryProgressChanged;
        game.DeliveryCompletedEvent += onDeliveryCompleted;
    }

    private void onDeliveryProgressChanged(object sender, EventArgs e)
    {
        this.BeginInvoke(new MethodInvoker(() => {
            pbDelivery.Increment(1);
            // This MessageBox is used to test the progression of the events and
            // to verify that the Dialog is now modal to the owner Form.  
            // Of course it's not used in an actual implentation.  
            MessageBox.Show(this, "Delivery In progress");
        }));
    }

    private void onDeliveryCompleted(object sender, EventArgs e)
    {
        this.BeginInvoke(new Action(() => MessageBox.Show(this, "Delivery Completed")));
    }

    private void button1_Click(object sender, EventArgs e)
    {
        game.StartDelivery();
    }
}
Jimi
  • 29,621
  • 8
  • 43
  • 61
  • So does `Show()` need to be both `*Invoke()`d _and_ have the `owner` parameter specified, or is it either/or and both is preferable/clearer, or `owner` has no bearing on which thread is used to display the box? I'm also still not quite understanding why the end of `MakeDelivery()` would be executed _never_ instead of just _later_. Would the original code would work as expected as long as the `MessageBox` was always dismissed within the 1-second timer period? Or that's a moot point because calling `MessageBox.Show()` from a non-UI thread means that thread is lost (blocked) forever inside? – Lance U. Matthews Feb 29 '20 at 20:18
  • @BACON If the MessageBox is called from non-UI thread, you need to `BeginInvoke()` it (not `Invoke()`, you'll end up with a deadlock). The Owner is set that you want it or not: if you don't specify it, the class goes on a hunt for it (it needs one) 1) If you specify the Owner and the MessageBox in called from a non-UI thread, you get a Cross-Thread violation exception 2) If you set `CheckForIllegalCrossThreadCalls = false`, you can set the Owner, but the MessageBox will be owned by the Desktop anyway. 3) If you `BeginInvoke()` the MessageBox and set the Owner, it then works as usual. – Jimi Feb 29 '20 at 20:47
  • @BACON The ThreadPool thread is blocked when you show the Modal Window. You can try to dismiss the MessageBox before the Timer executes the CallBack, but the Thread will never know about it. The Timer will, most probably, continue to spawn MessageBoxes, since it will continue to raise events without getting to the line that increments the counter. – Jimi Feb 29 '20 at 20:53
  • @Jimi Thanks for the detailed answer. I learned a lot reading it. I do have a question though. You recommend using the timer object from the `Windows.Forms` namespace. I purposely chose not to use it as it felt dirty loading that namespace in a non form class. Separation of concerns and all that. Is that me just being silly? Also would it still run on the UI thread if spawned in my game class? – Andronomos Mar 01 '20 at 00:46
  • Adding the `System.Windows.Forms` namespace to the class is not strictly related to the Separation of Concerns principle, which governs the interaction of a program's Modules. You current design respects the SOC: the Form knows nothing of the Game class, it just consumes its events; the Game class knows nothing about the Form or its components, it simply does its job when initialized. Deployment is another matter. The `System.Windows.Forms` specifically targets this platform. A general-purpose class should not rely on specific implementations, so it should use general-purpose classes/methods. – Jimi Mar 01 '20 at 07:48
  • Is your Game class meant to be a general-purpose class object, so it could work the same way in a Console application, in a WinForms or WPF app or in a non-Windows System altogether? This is design concern you need to care about. If this class will just work on a WinForms application, then using specific namespaces is not a problem, it may also provide a simplified solution to common platform-specific requirements. If not, you just need to learn how to marshal the events to the caller Thread's SychronizationContext (if this is actually a requirement that improves the overall functionality). – Jimi Mar 01 '20 at 07:49
  • @Jimi I plan on having animations (like a truck that "drives" in from the side of the form to make the delivery). So if I am able to get the timer logic working the way I want I may port the project over to WPF. Not sure if using the `Windows.Forms` namespace in WPF is considered bad practice. – Andronomos Mar 01 '20 at 08:04
  • As I wrote in the *How to solve* part: *(...)But, since the code presented is explicitly made to reproduce the problem, it may not apply*. If your class(es) is/are meant to work on different platforms, by all means, use a general-purpose threaded Timer. Following the same *logic*, you don't want to reference a `DispatcherTimer` in WinForms. Or, do you actually care? Is it *bad* to reference a WPF assembly in a class that is also meant to be used in WinForms? I use `BitmapSource` in WinForms and `Bitmap` in WPF, for example. Because I need both. It's not *good* or *bad*, these are just tools. – Jimi Mar 01 '20 at 08:38
  • The point here is: is it *good* or *bad* to raise the events in a non-UI Thread, requiring the subscribers to marshal back to the UI Thread all the time? How much does it cost, on the UI level? Does it cost less if the synchronization is performed on the producer side? Make these considerations, test the actual implementation. If it's costly on the UI side, then provide means to marshal the events on the Thread that initialized your class(es). The SynchronizationContext is made for this. – Jimi Mar 01 '20 at 08:42
  • I've added a few more notes to the *How to solve* part. – Jimi Mar 01 '20 at 09:20
1

Good information. The callback method executed by the timer should be reentrant, because it is called on ThreadPool threads. The callback can be executed simultaneously on two thread pool threads if the timer interval is less than the time required to execute the callback, or if all thread pool threads are in use and the callback is queued multiple times.

Renee9
  • 11
  • 2