Search code examples
c#.netwinformsnotifyiconservicecontroller

The ContextMenu of a NotifyIcon is not updating instantly


I'm creating a tray icon application with a context menu with 3 items in it. The tray icon is to control a service running, so the user can quickly start or stop it. Depending on the service status, I want to disable the Start Button if the service is already running.

The issue I'm facing is that it's updating the ContextMenu but only after opening the menu the second time.

For example: Service is running so the "Start" button should be disabled. Once I then click Stop, I need to open the ContextMenu two times for it to update and enable the Start Button.

Is there a better way to update a ContextMenu than the one I've created here?

class TrayApp : ApplicationContext
{
    private NotifyIcon trayIcon;
    private ServiceController sc;
    
    public TrayApp()
    {
        sc = new ServiceController("RamLogger");

        trayIcon = new NotifyIcon()
        {
            Icon = Properties.Resources.icon1,
            Text = "RamLogger",
            ContextMenu = GetContextMenu(),
            Visible = true
        };
        trayIcon.MouseClick += new MouseEventHandler(OnClick);
    }

    void OnClick(object sender, MouseEventArgs e)
    {
        if(e.Button == MouseButtons.Right)
        {
            trayIcon.ContextMenu = GetContextMenu();
        }
    }
    
    private ContextMenu GetContextMenu()
    {
        sc.Refresh();
        ContextMenu cm = new ContextMenu();
        cm.MenuItems.Clear();

        if (sc.Status == ServiceControllerStatus.Running || sc.Status == ServiceControllerStatus.StartPending)
        {
            cm.MenuItems.Add(new MenuItem("Status: Running"));
            cm.MenuItems.Add(new MenuItem("-"));
            cm.MenuItems.Add(new MenuItem("Start", Start) { Enabled = false });
            cm.MenuItems.Add(new MenuItem("Stop", Stop) { Enabled = true });
        }
        else
        {
            cm.MenuItems.Add(new MenuItem("Status: Stopped"));
            cm.MenuItems.Add(new MenuItem("-"));
            cm.MenuItems.Add(new MenuItem("Start", Start) { Enabled = true });
            cm.MenuItems.Add(new MenuItem("Stop", Stop) { Enabled = false });
        }
        return cm;
    }
}

Solution

  • The main problem here is that you're replacing the ContextMenu when the menu itself is about to be presented, clicking on the Tray Icon. The instance of the menu has already been initialized to the old one.

    You can simply create the menu once, then enable / disable items based on the state of the Service you're querying.

    I'm replacing the ContextMenu with a ContextMenuStrip, since the former has been deprecated in .NET. It will also ease the transition from .NET Framework to .NET 6+

    I've also added a Close Menu Item, which is used to terminate the Process.
    This is also going to dispose both the ContextMenuStrip and the NotifyIcon, so it's removed from the Tray

    using System.ServiceProcess;
    using System.Windows.Forms;
    
    class TrayAppContext : ApplicationContext {
        private readonly NotifyIcon trayIcon;
        private readonly ServiceController sc;
        private readonly ContextMenuStrip cms = null;
    
        public TrayAppContext() {
            sc = new ServiceController("RamLogger");
            cms = GetTrayMenu();
            trayIcon = new NotifyIcon() {
                Icon = Properties.Resources.icon1,
                Text = "RamLogger",
                ContextMenuStrip = cms,
                Visible = true
            };
            trayIcon.MouseClick += OnClick;
        }
    
        void OnClick(object sender, MouseEventArgs e) {
            if (e.Button != MouseButtons.Right) return;
    
            bool serviceAvailable = sc.Status.HasFlag(ServiceControllerStatus.Running) || 
                                    sc.Status.HasFlag(ServiceControllerStatus.StartPending);
    
            cms!.Items["Start"].Enabled = !serviceAvailable;
            cms!.Items["Stop"].Enabled = serviceAvailable;
        }
    
        private ContextMenuStrip GetTrayMenu() {
            var cms = new ContextMenuStrip();
            cms.Items.Add("Status", null, null);
            cms.Items.Add("-");
            cms.Items.Add(new ToolStripMenuItem("Start", null, Start, "Start") { Enabled = false });
            cms.Items.Add(new ToolStripMenuItem("Stop", null, Stop, "Stop") { Enabled = false });
            cms.Items.Add("-");
            cms.Items.Add(new ToolStripMenuItem("Close", null, Close, "Close") { Enabled = true });
            return cms;
        }
    
        void Start(object sender, EventArgs e) => sc.Start();
        void Stop(object sender, EventArgs e) => sc.Stop();
        void Close(object sender, EventArgs e) {
            cms?.Dispose();
            trayIcon.Dispose();
            ExitThreadCore();
        }
    }
    

    In case someone wants to test this and doesn't know how, in Program.cs replace the default:

    Application.Run(new Form1());
    

    with:

    Application.Run(new TrayAppContext());