2

I searched for current best practices to handle windows system / tray icons in the .NET environment, but did not find any up-to-date information.

Considering a usual .NET 5 project configuration:

<PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
</PropertyGroup>

With following code (Program.cs):

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;

Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddHostedService<TrayIconService>();
    })
    .Build()
    .Run();

class TrayIconService : IHostedService, IAsyncDisposable
{
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // what is the recommended way to create a windows tray icon in .NET 5?
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        await this.DisposeAsync();
    }

    public async ValueTask DisposeAsync()
    {
        // and how do I close the tray icon and dispose related resources afterwards?
    }
}

Can you help me implementing a simple 'Hello World' context menu windows system tray icon in C# and/or give me some documentation regarding the state-of-the-art try icon usage?

Is an implementation of IHostedService even the best consideration? How do I reference the windows API? Do I need an net5.0-windows project?

Thanks in advance!

ˈvɔlə
  • 9,204
  • 10
  • 63
  • 89
  • 2
    The tray belongs to the shell, which runs in each user's session. The tray icon belongs to an application that runs in the same session. The service, which runs in session 0, has to communicate with any instance of that application, which represents the service to the user in that space. – madreflection Mar 19 '21 at 20:34
  • 2
    https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.notifyicon?view=net-5.0 seems you need to use the old forms api's still. There's also this; https://github.com/HavenDV/H.NotifyIcon.WPF but no native wpf .net5 way from what i could google. – sommmen Mar 19 '21 at 20:37
  • @sommmen So your suggested solution is to use the `HavenDV/H.NotifyIcon.WPF` NuGet package in my windows service? – ˈvɔlə Mar 19 '21 at 20:53
  • Some clarification is needed here: what's called a "hosted service" is a more general concept which may not have anything to do with what Windows calls a "service". You can run a hosted service as a Windows service, and then you can't have any UI (except what you can get done through IPC to another program), or you can have a regular old application with a tray icon, but then that won't run as a Windows service. – Jeroen Mostert Mar 19 '21 at 20:53
  • @JeroenMostert I understand what you are saying. So how do you host my tray icon in .NET 5 and configure it as a windows service? I have edited my question. – ˈvɔlə Mar 19 '21 at 20:55
  • 4
    That's the point: you don't. Windows services cannot display tray icons. (This is not specific to .NET.) You can only run a regular user application that shows a tray icon (and have this run at startup through the user's profile, for example). If you also need code to run as a service (always-on, even if no user is logged in) and you want a user interface to configure it, you need *two* processes: the Windows service and the application showing the icon, with some form of inter-process communication to make the application communicate with the service. Do you really need a service? – Jeroen Mostert Mar 19 '21 at 20:58

1 Answers1

4

I'm sure you have found the answer by now however there are no "solutions" and quite a lot of views so let me give an option on how to solve this.

this actually is quite a common architecture, windows like you to use other technology with the new Windows 11 MAUI API, users, however, are not jumping on the "tiles" design and the timeline is full of click-bait and not a reliable way to "talk" with a user.

There are several ways you can do this, you can start the tray icon in the code of the service making it a windows only service

basically, you are looking at coupling your service with a System.Windows.Forms.NotifyIcon and the TrayIcon.Visible property

With a NotifyIcon you can do something like:

class MyTray:IDisposable
{
   NotifyIcon ni;//needs disposed
    public ()
    {
       ni= new NotifyIcon()
       //use a helper class to generate a context menu for the tray icon if there is a menu like start, stop ...
       ni.ContextMenuStrip = new MyContextMenus(menuDataContext).Create();
     }

}

then when you call it you do:

ni.Icon = Resources.Icon_red;
ni.Text = "Some ballon Text";

//make the tray icon visible ni.Visible = true; the user can interact with the icon on the tray menu here an sample of what the MyContextMenu(backingField) create could look like:

public ContextMenuStrip Create()
{
    // Add the default menu options.
    menu = new ContextMenuStrip();
    ToolStripMenuItem item;
    ToolStripSeparator sep;

    item = new ToolStripMenuItem
    {
        Text = "License",
        Image = Resources.contract
    };
    item.Click += new EventHandler(License_Click);
    menu.Items.Add(item);


    // About.
    item = new ToolStripMenuItem
    {
        Text = "Service Status",
        Image = Resources.data_green1
    };
    item.Click += new EventHandler(Status_Click);

    menu.Items.Add(item);

    // Separator.
    sep = new ToolStripSeparator();
    menu.Items.Add(sep);

    //rule engine editor
    item = new ToolStripMenuItem
    {
        Text = "Rule Engine Editor",
        Image = Resources.data_edit1
    };

    item.Click += new System.EventHandler(Editor_Click);

    menu.Items.Add(item);

    // Separator.
    sep = new ToolStripSeparator();
    menu.Items.Add(sep);
    // Exit.
    item = new ToolStripMenuItem
    {
        Name = "mnuClose",
        Text = "Close",
        Image = Resources.data_down
    };
    item.Click += new EventHandler(Exit_Click);

    menu.Items.Add(item);
    return menu;
}

or decouple it and like in this sample where the service could be on any OS supporting .net and communicate via a protocol like ProtoBuf, Sockets WCF or named pipes.

Perhaps a "better" way to do this

Have a look at this article

This sample uses name pipes ( a network connection) to talk back with the application with the use of NuGet packages and WPF as the presentation platform.

The server talks with anyone listening to the Pipe like this:

using H.Pipes;
using H.Pipes.Args;
using NamedPipesSample.Common;

namespace NamedPipesSample.WindowsService
{
    public class NamedPipesServer : IDisposable
    {
        const string PIPE_NAME = "samplepipe";

        private PipeServer<PipeMessage> server;

        public async Task InitializeAsync()
        {
            server = new PipeServer<PipeMessage>(PIPE_NAME);

            server.ClientConnected += async (o, args) => await OnClientConnectedAsync(args);
            server.ClientDisconnected += (o, args) => OnClientDisconnected(args);
            server.MessageReceived += (sender, args) => OnMessageReceived(args.Message);
            server.ExceptionOccurred += (o, args) => OnExceptionOccurred(args.Exception);

            await server.StartAsync();
        }

        private void OnClientConnected(ConnectionEventArgs<PipeMessage> args)
        {
            Console.WriteLine($"Client {args.Connection.Id} is now connected!");

            await args.Connection.WriteAsync(new PipeMessage
            {
                Action = ActionType.SendText,
                Text = "Hi from server"
            });
        }

        private void OnClientDisconnected(ConnectionEventArgs<PipeMessage> args)
        {
            Console.WriteLine($"Client {args.Connection.Id} disconnected");
        }

        //...
    }
}

if you follow along with the sample the WPF application that acts as the tray-icon will be "plummed" like this:

public async Task InitializeAsync()
{
    if (client != null && client.IsConnected)
        return;

    client = new PipeClient<PipeMessage>(pipeName);
    client.MessageReceived += (sender, args) => OnMessageReceived(args.Message);
    client.Disconnected += (o, args) => MessageBox.Show("Disconnected from server");
    client.Connected += (o, args) => MessageBox.Show("Connected to server");
    client.ExceptionOccurred += (o, args) => OnExceptionOccurred(args.Exception);

    await client.ConnectAsync();

    await client.WriteAsync(new PipeMessage
    {
        Action = ActionType.SendText,
        Text = "Hello from client",
    });
}
Walter Verhoeven
  • 3,867
  • 27
  • 36