Home > Software engineering >  How to invoke a method on the STA thread when you have nothing to invoke on?
How to invoke a method on the STA thread when you have nothing to invoke on?

Time:02-28

This is rather a difficult situation to explain. I have a Windows Forms application that uses a notification icon with a context menu. The app can be configured to start with no form shown or the form can be closed by the user leaving just the notify icon present. Selecting the "Show" option from the context menu strip of the notification icon will create (if closed)/restore(if minimized) the main form. Now this all works fine as the events generated from the menu are running on the STA thread. The problem is the single instance implementation. A notify icon type application really should only ever be running one instance, otherwise you'd have multiple notify icons in the tray and it'd be a huge mess. This is accomplished using a named EventWaitHandle and also includes detection of an attempt to run a second instance, when this happens it signals the main running instance which then restores/creates the main form thus:

    public static bool InitSingleInstance(this Control control, string handleName, Action? NewInstanceAttempt = null)
    {            
        EventWaitHandle ewh = new(false, EventResetMode.ManualReset, handleName, out bool isNew);
        if (isNew)
        {
            Task.Run(() =>
            {
                while (!control.IsDisposed)
                {
                    ewh.WaitOne();
                    ewh.Reset();
                    NewInstanceAttempt?.Invoke();
                }
            });
        }
        else
        {
            Task.Run(() =>
            {
                EventWaitHandle.SignalAndWait(ewh, ewh, 100, true);
            }).Wait();
        }
        return isNew;
    }

The issue I have is that the loop waiting for a signal from a second instance runs in another Thread and thus needs to be delegated to the STA thread to restore/create the form. I use a UserControl to contain the NotifyIcon and Menu (So I can edit them in the designer) and this is created in Program.cs in place of Application.Run(new Form()) but this control is never actually shown itself so it never gets a Handle assigned to it that I can Invoke on so I get an exception when the second instance is run.

public partial class NotifyIconAndMenu : UserControl
{
    private Form? mainForm = null;
    private FormWindowState mainFormState = FormWindowState.Normal;
    private readonly Func<Form> NewForm;

    public NotifyIconAndMenu(Func<Form> newForm)
    {
        NewForm = newForm;
        if(!this.InitSingleInstance("FOOBFOOB387846", NewInstanceAttempt))
            return;
        InitializeComponent();
        Application.Run();
    }

    private void ShowMainForm()
    {
        if (mainForm == null || mainForm.IsDisposed)
        {
            mainForm = NewForm();
            mainForm.Visible = true;
            mainForm.Resize  = MainForm_Resize;
        }
        mainForm.WindowState = mainFormState;
    }

    private void Quit()
    {
        Dispose();
        Application.Exit();
    }

    private void MainForm_Resize(object? sender, EventArgs e)
    {
        if (mainForm != null && mainForm.WindowState != FormWindowState.Minimized)
            mainFormState = mainForm.WindowState;
    }

    private void NewInstanceAttempt() => Invoke(ShowMainForm); // << exception

    private void IconMenu_ItemClicked(object sender, ToolStripItemClickedEventArgs e)
    {
        if (e.ClickedItem == MI_Show) ShowMainForm();
        else
        if (e.ClickedItem == MI_Quit) Quit();
    }
}

Perhaps there is a better way to do this? I did think of running a loop in the constructor waiting on a locked object and pulsing it from the other thread. Like this

    public NotifyIconAndMenu(Func<Form> newForm)
    {
        NewForm = newForm;
        if (!this.InitSingleInstance("NotIconDemo387846", NewInstanceAttempt))
            return;
        InitializeComponent();
        lock (this)
        {
            while (!IsDisposed)
            {
                Monitor.Wait(this);
                ShowMainForm();
            }
        }
    }        
    private void NewInstanceAttempt()
    {
        lock(this)
        {
            Monitor.Pulse(this);
        }
    }

But this seems messy. EDIT: And indeed wouldn't work as it would lock up the STA thread.

CodePudding user response:

I managed to solve it like this:

public partial class NotifyIconAndMenu : UserControl
{
    private Form? mainForm = null;
    private FormWindowState mainFormState = FormWindowState.Normal;
    private readonly Func<Form> NewForm;

    public NotifyIconAndMenu(Func<Form> newForm)
    {
        NewForm = newForm;
        if (!this.InitSingleInstance("GROWPLENTYOFCHEESE4444", NewInstanceAttempt))
            return;
        InitializeComponent();
        FormShowLoop();
        Application.Run();
    }

    private async void FormShowLoop()
    {
        while (!IsDisposed)
        {
            await Task.Run(() =>
            {
                lock (this)
                {
                    Monitor.Wait(this);
                }
            });
            if(!IsDisposed)
                ShowMainForm();
        }
    }


    private void ShowMainForm()
    {
        if (mainForm == null || mainForm.IsDisposed)
        {
            mainForm = NewForm();
            mainForm.Resize  = MainForm_Resize;
            mainForm.Visible = true;
        }
        mainForm.WindowState = mainFormState;
    }

    private void Pulse()
    {
        lock (this)
        {
            Monitor.Pulse(this);
        }
    }

    private void Quit()
    {
        Dispose();
        Pulse();
        Application.Exit();
    }

    private void MainForm_Resize(object? sender, EventArgs e)
    {
        if (mainForm != null && mainForm.WindowState != FormWindowState.Minimized)
            mainFormState = mainForm.WindowState;
    }

    private void NewInstanceAttempt() => Pulse();

    private void IconMenu_ItemClicked(object sender, ToolStripItemClickedEventArgs e)
    {
        if (e.ClickedItem == MI_Show) Pulse();
        else
        if (e.ClickedItem == MI_Quit) Quit();
    }
}
  • Related