Search code examples
c#.net-5trayiconihostedservice

.NET 5 Tray Icon Usage in C# Windows Service


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!


Solution

  • 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",
        });
    }