1

This is a tray-icon-only Windows Forms application. I'm trying to use argument to control something and change the text on the form for showing the status information.

But I found when I use argument to call it during it's running, the things I want to change are null (NotifyIcon() and MenuItem()), seems it ran a different application when I using arguments. I also tried Invoke() but there is no this definition in NotifyIcon().

Here is the code I wrote:

static void Main(string[] args)
{
    if (args.Length > 0)
    {
        Arg_Call(args[0]);
    }
    if (new Mutex(true, "{XXX}").WaitOne(TimeSpan.Zero, true))
    {
        Init_Tray();
        Application.Run();
    }
}
private static NotifyIcon trayicon;

private static void Init_Tray()
{
    trayicon = new NotifyIcon() { Icon = new Icon(@"D:\projects\Icon.ico"), Text = "Waiting", Visible = true };
    trayicon.Visible = true;
    Application.Run();
}
private static void Arg_Call(string args)
{
    trayicon.Invoke((MethodInvoker)delegate {
        trayicon.Text = "OK";
    }); //from: https://stackoverflow.com/a/661662/8199423
}

Where am I wrong? How to and what is the best way to change the NotifyIcon.Text property in the running form via command-line-arguments?

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
matif
  • 63
  • 10
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/148206/discussion-between-matif-and-peter-duniho). – matif Jul 03 '17 at 09:31

1 Answers1

4

I am sorry I was unable to adequately explain why your question is a duplicate of the existing "single-instance-application" questions. I will try to reiterate the train of thought here:

  1. You wrote "How to and what is the best way to change the texts in the running form via command-line-arguments?"
  2. Your requirement involves a currently-running process, which is presenting the NotifyIcon in the task tray, and the desire to use the command-line to modify that currently-running process's state.
  3. It is a simple fact that when you type anything on the command line, it starts a whole new process. That process is necessarily different from the process that is already running, and which is presenting the NotifyIcon in the task tray.

Putting all of the above together, we have the conclusion that you want a new process that you start on the command line to interact with an existing process. And the simplest way to achieve that goal is to use the built-in single-instance-application support found in .NET. This is because the support for single-instance-applications includes automatic passing of the new command line arguments to the previous running program. Hence, the duplicate.

As I mentioned earlier, you should try to develop the skill to generalize and see how seemingly new problems are really just old problems in disguise and which you or someone else already knows how to solve.

In the same way that all problem solving can be summarized as "break the large problem down into smaller problems, repeat as necessary until all of the smaller problems are problems you already know how to solve", programming is very often not a matter of solving new problems, but rather of recognizing how your current problem is really a problem you already know how to solve.

All that said, I have the impression that you're still having difficulty figuring out how to apply that information to your specific scenario. So, perhaps this is an opportunity to illustrate the validity of the philosophy I espouse, by showing you how your seemingly different problem really is the problem I claim it is. :)

So, let's start with your original scenario. I am not using the code you posted, because it's mostly code that isn't needed. It seemed simpler to me to start from scratch. To do that, I wrote a little TrayManager class that encapsulates the actual NotifyIcon part of the functionality:

class TrayManager : IDisposable
{
    private readonly NotifyIcon _notifyIcon;

    public TrayManager()
    {
        _notifyIcon = new NotifyIcon
        {
            ContextMenu = new ContextMenu(new[]
            {
                new MenuItem("Exit", ContextMenu_Exit)
            }),
            Icon = Resources.TrayIcon,
            Text = "Initial value",
            Visible = true
        };
    }

    public void Dispose()
    {
        Dispose(true);
    }

    public void SetToolTipText(string text)
    {
        _notifyIcon.Text = text;
    }

    protected virtual void Dispose(bool disposing)
    {
        _notifyIcon.Visible = false;
    }

    private void ContextMenu_Exit(object sender, EventArgs e)
    {
        Application.ExitThread();
    }

    ~TrayManager()
    {
        Dispose(false);
    }
}

The above hard-codes the context menu for the icon. Of course, it a real-world program, you'd probably want to decouple the menu from the above class, for greater flexibility.

The simplest way to use the above would look something like this:

static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main(string[] args)
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        using (TrayManager trayManager = new TrayManager())
        {
            Application.Run();
        }
    }
}

So, how do we modify the above so that when you run the program again, you can change the Text property of the NotifyIcon with the command-line arguments you type? That's where the single-instance application comes in. As seen in the duplicate I marked earlier, What is the correct way to create a single-instance application?, one of the simplest ways to accomplish this is to use the Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase class, which has built right in support for single-instance applications and a mechanism for delivering new command line arguments to the existing process.

The one little draw-back is that this class was designed for Winforms programs, with the assumption that there will be a main form. To use it will require creating a Form instance. For a program without the need for an actual form, this means creating a Form instance that is never shown, and making sure that it's never shown does require a little bit of finagling. Specifically:

class TrayOnlyApplication : WindowsFormsApplicationBase
{
    public TrayOnlyApplication()
    {
        IsSingleInstance = true;
        MainForm = new Form { ShowInTaskbar = false, WindowState = FormWindowState.Minimized };

        // Default behavior for single-instance is to activate main form
        // of original instance when second instance is run, which will show
        // the window (i.e. reset Visible to true) and restore the window
        // (i.e. reset WindowState to Normal). For a tray-only program,
        // we need to force the dummy window to stay invisible.
        MainForm.VisibleChanged += (s, e) => MainForm.Visible = false;
        MainForm.Resize += (s, e) => MainForm.WindowState = FormWindowState.Minimized;
    }
}

The only thing in the above that gives us the single-instance application behavior we want is the setting of IsSingleInstance = true;. Everything else is there just to satisfy the requirement that some Form object is present as the MainForm, without actually showing that object on the screen.

Having added the above class to the project, we can now "connect the dots". The new Program class looks like this:

static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main(string[] args)
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        using (TrayManager trayManager = new TrayManager())
        {
            TrayOnlyApplication app = new TrayOnlyApplication();

            app.StartupNextInstance += (s, e) => trayManager
                .SetToolTipText(e.CommandLine.Count > 0 ? e.CommandLine[0] : "<no value given>");
            app.Run(args);
        }
    }
}

You'll note two changes:

  1. In addition to the TrayManager, which handles the NotifyIcon, we now also create the TrayOnlyApplication object, subscribing to its StartupNextInstance event so that we can receive the command line arguments given to any new instance, and use that to set the Text property of the NotifyIcon object (by passing that to the method created specifically for that purpose).
  2. Instead of using Application.Run() to run the require message-pump loop to handle window messages, we use the Run() method our TrayOnlyApplication class inherited from the WindowsFormsApplicationBase class. Either of these methods handle message pumping while the program is running, and return control to the caller when the Application.ExitThread() method is called, so both approaches to message pumping work with the code in the TrayManager.

Now, the above example is simply a slight modification of the original version that didn't enforce single-instance operation. You might notice that it has the arguable deficiency that it always creates the tray icon, whether or not it's the first instance to run. Subsequent instances will run, create the tray icon, then immediately dismiss the icon and exit.

The WindowsFormsApplicationBase provides a mechanism to avoid this, the Startup event. While the StartupNextInstance event is raised in any instance of the application that is run when an instance already is running, the Startup event is raised only when no other instance is already running. I.e. in the instance where you actually want to do things, like show the tray icon.

We can take advantage of that event to defer creation of the NotifyIcon until we know whether we actually need it or not:

static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main(string[] args)
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        TrayManager trayManager = null;
        TrayOnlyApplication app = new TrayOnlyApplication();

        // Startup is raised only when no other instance of the
        // program is already running.
        app.Startup += (s, e) => trayManager = new TrayManager();

        // StartNextInstance is run when the program if a
        // previously -run instance is still running.
        app.StartupNextInstance += (s, e) => trayManager
            .SetToolTipText(e.CommandLine.Count > 0 ? e.CommandLine[0] : "<no value given>");

        try
        {
            app.Run(args);
        }
        finally
        {
            trayManager?.Dispose();
        }
    }
}

Note that here, we need to write the try/finally explicitly instead of using the using statement, because the using statement requires initialization of the variable when it's declared, and we want to defer initialization until later, or never, depending on which instance is being run.

(Unfortunately, there's no way to defer creation of the dummy window in the TrayOnlyApplication class, since it's required just to call the Run() method, which requires a valid Form object be already set, and the determination as to which instance is being run happens in that call, not before.)

And that's all there is to it. The above shows, clearly I hope, exactly how the single-instance application techniques available to you directly solve the problem you are asking for help with. By providing a mechanism for a newly-run instance of your program to communicate the command line arguments passed to it, to the already-running instance of the same program, that newly-run instance can cause the already-running instance to perform whatever work it needs to (such as changing the tool-tip text for the tray icon, for example).

Naturally, any similar mechanism will achieve the same result. The only important thing is to have the newly-run instance detect an existing instance, and communicate with it. It just happens that the WindowsFormsApplicationBase class provides that functionality pre-made. There are lots of other ways to do the same thing, each with their own pros and cons.

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136