0

I'm trying to figure out why the System.Windows.Forms.ContextMenu in my Windows Forms application is closing immediately after I open it by right-clicking its tray icon on the taskbar.

This doesn't occur immediately, but strangely starts happening on the second opening. (i.e. I can open/close the context menu once normally and from then on the issue crops up) After right-clicking, I can visually see the menu appear for a fraction of a second before it automatically closes.

described behavior

Here is the C# code in Program.cs. I was originally following one of Barnacules Nerdgasm's "#Codegasm" tutorials (YouTube, GitHub), which involved removing the forms aspect of the project, leaving this as the only source file.

using System;
using System.Windows.Forms;
using System.Threading;
using System.Net.NetworkInformation;
using System.Net;
using System.Drawing;

namespace PingGoogleDNS
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            PingGoogleDNS pinger = new PingGoogleDNS();
            pinger.StartPinger();
        }

        class PingGoogleDNS
        {
            #region Global Thread and Icon Objects
            NotifyIcon pingIcon;
            Icon goodIcon = new Icon("good_connection.ico");
            Icon weakIcon = new Icon("weak_connection.ico");
            Icon noIcon = new Icon("no_connection.ico");

            Thread pingWorker;
            MenuItem roundTripTime;
            #endregion

            #region StartPinger
            public void StartPinger()
            {
                pingIcon = new NotifyIcon();
                pingIcon.Icon = noIcon /*disconnectedIcon*/;
                pingIcon.Visible = true;

                MenuItem quit = new MenuItem("Quit / Exit");
                MenuItem name = new MenuItem("Pings Google DNS (8.8.8.8)");
                roundTripTime = new MenuItem("Latest ping: N/A ms");
                ContextMenu contextMenu = new ContextMenu();
                contextMenu.MenuItems.Add(quit);
                contextMenu.MenuItems.Add(name);
                contextMenu.MenuItems.Add(roundTripTime);
                pingIcon.ContextMenu = contextMenu;

                quit.Click += Quit_Click;

                pingWorker = new Thread(new ThreadStart(PingSenderThread));
                pingWorker.Start();
                Console.WriteLine("Start");
            }
            #endregion

            #region Quit Button Handler
            private void Quit_Click(object sender, EventArgs e)
            {
                pingIcon.Dispose();
                pingWorker.Abort();
                Environment.Exit(1);
            }
            #endregion

            #region Ping Sender Thread
            public void PingSenderThread()
            {
                Ping pingSender = new Ping();

                IPAddress iPAddress;
                IPAddress.TryParse("8.8.8.8", out iPAddress);

                PingReply reply;
                try
                {
                    while (true)
                    {
                        try
                        {
                            reply = pingSender.Send(iPAddress, 2000 /* ms */);

                            if (reply.Status == IPStatus.Success)
                            {
                                if (reply.RoundtripTime < 500 /* ms */)
                                    pingIcon.Icon = goodIcon /*connectedIcon*/;
                                else
                                    pingIcon.Icon = weakIcon;

                                pingIcon.Text = "8.8.8.8 ping success! (" + reply.RoundtripTime + " ms)";
                                roundTripTime.Text = "Latest ping: " + reply.RoundtripTime + " ms";

                                Thread.Sleep(1000);
                            }
                        }
                        catch (PingException pe)
                        {
                            pingIcon.Icon = noIcon /*disconnectedIcon*/;
                            pingIcon.Text = "8.8.8.8 ping failure.";
                            roundTripTime.Text = "Latest ping: N/A ms";
                        }
                    }
                }
                catch (ThreadAbortException)
                {
                    // No need to do anything, just catch the ThreadAbortException.
                }
            }
            #endregion
        }
    }
}

I tried to debug this by dummy adding listeners to the ContextMenu's Popup and Collapse events to see if it registered being closed:

// In StartPinger()
contextMenu.Popup += ContextMenu_Popup;
contextMenu.Collapse += ContextMenu_Collapse;

// ...

private void ContextMenu_Popup(object sender, EventArgs e)
{
     Console.WriteLine("menu opened");
}

private void ContextMenu_Collapse(object sender, EventArgs e)
{
     Console.WriteLine("menu closed");
}

But, I only see "menu opened" being printed to the console when I right-click and no "menu closed" are ever printed. This has confused me and I don't know what to try next. Has anyone else encountered this before?

ifconfig
  • 6,242
  • 7
  • 41
  • 65
  • 2
    Where's your message pump? e.g. [`Application.Run()`](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.application.run?view=net-5.0#System_Windows_Forms_Application_Run)? Try adding that to the end of your `Main`. Also, looks like you are going to have cross-thread issues trying to update icons from your pinger thread. – Wyck Oct 03 '21 at 01:22
  • @Wyck I'm not sure I follow, could you explain what you mean by message pump? I'm pretty new to WinForms, despite writing this project originally a couple of years ago. – ifconfig Oct 03 '21 at 01:24
  • Oh, ok. I'll give that a shot. – ifconfig Oct 03 '21 at 01:24
  • Damn, that was the issue! Could you add an answer briefly explaining why that fixed the issue? I'll accept it :) *I'd also like to know why the `Collapse` event wasn't triggered, if you happen to know* – ifconfig Oct 03 '21 at 01:26
  • 2
    Every event driven application such as yours enters an endless loop at some point where it processes events and dispatches to handlers. That's what Application.Run() is. You need it. All UI-related stuff should happen on that Main thread. If you need another thread to do network IO such as your ping, then you will have to [Invoke](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.control.invoke?view=net-5.0) updates to your UI back to the Main thread. – Wyck Oct 03 '21 at 01:26
  • Also, I'm curious about the potential threading issue you mentioned... I'm not aware of any way to protect the `NotifyIcon` with a mutex, so is there another approach I should use instead? – ifconfig Oct 03 '21 at 01:30
  • 1
    Every part of the UI can only be accessed from the main thread (the `[STAThread]` one). Read up on `Control.Invoke` (to marshal a call from another thread to the main thread) and `Control.InvokeRequired` (to decide if an `Invoke` is required). Remember that your Form class and most things on it are descended from the `Control` class. Here's a recent answer from me about this: https://stackoverflow.com/questions/69259572/running-two-tasks-simultaneously/69260290#69260290 – Flydog57 Oct 03 '21 at 01:38

1 Answers1

1

You're missing a message loop (a.k.a. message pump). Add this to the end of your Main function.

Application.Run();

Every event driven application such as yours enters an endless loop at some point where it processes events and dispatches to handlers. That's what Application.Run() is. (Read the Remarks section of the documentation for a more complete description.) You need this message loop for any UI-related stuff to work properly, such as mouse clicks, windows, and popup menus.

Also, it looks like you are going to have cross-thread issues trying to update icons from your pinger thread. All UI-related stuff should happen on that Main thread. If you need another thread to do network IO such as your ping, then you will have to Invoke any code that updates UI so that that code runs in the context of your Main thread.

Wyck
  • 10,311
  • 6
  • 39
  • 60
  • To call Invoke when you have no form, consider using the technique of using [Dispatcher.CurrentDispatcher](https://stackoverflow.com/questions/303116/system-windows-threading-dispatcher-and-winforms). – Wyck Oct 03 '21 at 01:38
  • I haven't yet encountered issues with the icon being updated from a thread, but I will certainly keep this in mind if I do. Thanks again! – ifconfig Oct 03 '21 at 01:49
  • @ifconfig, if it bites you, leave a comment and I'll explain how to fix it in detail. For now, it looks like you're good to go! – Wyck Oct 03 '21 at 02:08