Named Pipes in .NET 6 with Tray Icon and Service

There are many examples out there which explains how to build an application with a tray icon and a Windows Service. A few posts describe how to communicate between the two.

Since I haven’t found any guide which describes how to do all of those things using .NET 6, I decided to write a detailed guide where you can follow along.

Since we are targeting .NET 6, you will need Visual Studio 2022.

App which communicates with a Windows Service over named pipes

Why would you want to have a Windows Service which can communicate with your application?

  • You have an application which runs in user context, without any administrative rights, and you need to perform some tasks which requires higher privileges.
  • You already have a Windows Service, but you need a user interface where the user can interact with the service.

The examples in this guide will also work for communication between multiple processes running in the same context.

Why Named Pipes in .NET 6?

  • Named pipes is a good way to implement inter process communication, i.e. the ability for processes to talk to each other.
  • .NET 6 has cross platform support. You can reuse many parts of this application on Linux and MacOS. This is not the case with .NET Framework
  • .NET 6 the latest version of .NET as of today, and currently the most future safe.
  • .NET 6 has global and implicit usings, which makes our code look cleaner.

A lot of the code will however work in earlier versions of .NET

Nuget Packages Used

Out of Scope for this Guide (things you might want to change later)

While this is very comprehensive guide, some things had to be left out so we can focus on the core value. When following along, you should have in mind that the following concepts have been left out:

  • Interfaces for services etc. – which allows for dependency injection and unit tests
  • Unit tests / integration tests
  • Proper error handling
  • More null checks
  • Proper logging
  • Namespace and project folder structure

Windows Service

Let’s start by creating our Windows Service. It will contain the following main components:

We start by creating a new Console Application. Since we want to use .NET 6, make sure to not select the .NET Framework alternative.

Give your solution and your project a name which makes sense.

Make sure to select .NET 6 as target framework.

Service Host

– Install Nuget package System.ServiceProcess.ServiceController and create a class called ServiceHost. This class should inherit from ServiceBase, which makes it possible to run our assembly as a service.

  • Create a default constructor and set the ServiceName property.
  • Override OnStart, OnStop and OnShutdown methods. This will make it possible to control what happens when the service is started or stopped.
using System.ServiceProcess;

namespace NamedPipesSample.WindowsService
{
    internal class ServiceHost : ServiceBase
    {
        public ServiceHost()
        {
            ServiceName = "Named Pipes Sample Service";
        }

        protected override void OnStart(string[] args)
        {

        }

        protected override void OnStop()
        {
            
        }

        protected override void OnShutdown()
        {
            
        }
    }
}

– Now, we need to add some methods so we can have a separate background thread running for our service.

  • Run(string [args]) – Create a new background thread and starts it.
  • InitializeServiceThread() – The actual service thread. Will run while stopping == false. Without the 100ms delay in the loop, one of our CPU cores would be running at 100%. This loop can later be replaced by other continuously running code.
  • Abort() – Sets stopping to true which shuts down our service thread.
using System.ServiceProcess;
using System.Threading;
using System.Threading.Tasks;

namespace NamedPipesSample.WindowsService
{
    internal class ServiceHost : ServiceBase
    {
        private static Thread serviceThread;
        private static bool stopping;

        //...

        public static void Run(string[] args)
        {
            serviceThread = new Thread(InitializeServiceThread)
            {
                Name = "Named Pipes Sample Service Thread",
                IsBackground = true
            };
            serviceThread.Start();            
        }

        public static void Abort()
        {
            stopping = true;
        }

        private static void InitializeServiceThread()
        {
            while(!stopping)
            {
                Task.Delay(100).GetAwaiter().GetResult();
            }
        }
    }
}

We need to make sure Run is called when the service starts.

protected override void OnStart(string[] args)
{
	Run(args);
}

…and that our service thread exits when the service is stopping.

protected override void OnStop()
{
	Abort();
}

protected override void OnShutdown()
{
	Abort();
}

Program.cs – Application Entry Point

– Now, let’s update our Main method in Program.cs, so we can test our service host.

  • In .NET 6 we can omit both namespace, class Program and public static void Main(string[] args) from our console application’s entry point Program.cs.
  • ServiceBase.Run can only be invoked when our service is actually running is a Windows service. It cannot be invoked from a console application. Therefore, we need to check if we are in UserInteractive environment, and use ServiceHost.Run directly in this scenario.
using NamedPipesSample.WindowsService;
using System.ServiceProcess;

Console.WriteLine("Starting Service...");

if (!Environment.UserInteractive)
{
	using (var serviceHost = new ServiceHost())
		ServiceBase.Run(serviceHost);
}
else
{
	Console.WriteLine("User interactive mode");

	ServiceHost.Run(args);

	Console.WriteLine("Press ESC to stop...");
	while (Console.ReadKey(true).Key != ConsoleKey.Escape)
	{

	}

	ServiceHost.Abort();
}

– It’s time to test our service host.

It works!

Common Project

Since we want to communicate between two assemblies, we need to have a model which describes the data being sent between our client and server.

– Good practice is to have this model within a separate common project, which is referenced by our service and client project.

The service and client project should not have any “knowledge” about each others existence. Also, the common project should not hold any references to the other projects. By doing this, we have loose coupling and we can easily replace the client or server without having to modify the other projects.

Create new Common Project

– Create a new class library. Make sure to not select the .NET Framework option.

– Name your common project. A good naming convention would be <SolutionName>.<ProjectName>.

– For this project, we can use .NET Standard 2.0, which makes it possible to reference this project from any recent .NET version including .NET Framework.

– Start by removing the initial Class1.cs file we got in the new project and create a new class called PipeMessage.cs. Make the class public.

PipeMessage.cs

Create a new class PipeMessage.cs

– Create a public Enum ActionType in this file. This enum will let us specify what kind of action we would like the receiving part to perform. The enum should contain the following constants:

  • Unknown – If we have assigned a value to our enum property, the enum will have the Unknown value. This is good practice to avoid unwanted behavior if we forget to assign a value.
  • SendText – This action will be used when we want to send a text message from the service to the client or vice versa.
  • ShowTrayIcon – An action that we will use later in this tutorial to pin a tray icon to the taskbar corner.
  • HideTrayIcon – An action that we will use later in this tutorial to move a pinned tray icon to the overflow menu.
public enum ActionType
{
	Unknown,
	SendText,
	ShowTrayIcon,
	HideTrayIcon
}

The PipeMessage class should have the following public properties:

  • Id (Guid) – A unique id for each message.
  • Action (ActionType) – The action we want to perform.
  • Text (string) – Only used when our action is SendMessage.

Also, make sure to add the Serializable attribute so instances of this class can be transferred over our pipe.

using System;

namespace NamedPipesSample.Common
{
    [Serializable]
    public class PipeMessage
    {
        public PipeMessage()
        {
            Id = Guid.NewGuid();
        }

        public Guid Id { get; set; }
        public ActionType Action { get; set; }
        public string Text { get; set; }
    }
	
	//...
}

Named Pipes Server in Service Project

Go back to our Windows service project and add a project reference to the Common project we just created.

Install Nuget package H.Pipes.

Create a new class called NamedPipesServer.cs. This class will be responsible for listening to connections over named pipes, and invoking actions based on messages received. We also make sure that this class implements IDisposable, so that we can dispose our pipe properly.

  • Define a name for our pipe. This name must be unique on the system. Choose carefully to avoid collisions.
  • Create a new PipeServer instance of type PipeMessage. PipeServer in this case comes from the H.Pipes package. PipeMessage is the class we created in our common project.
  • Define an async method called InitializeAsync. Since we cannot have async constructurs, it’s better to have a separate async method for initialization.
  • Wire up some events for ClientConnected, ClientDisconnected, MessageReceived and OnExceptionOccured, each to a separate private method.
  • Create a method OnClientConnectedAsync where we write a message to the console and send a text message to the client.
  • Define a method OnClientDisconnected where we write a message to the console. If we need to put some other logic which needs to be called on client disconnect later on, we can add it to this method.
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");
        }

        //...
    }
}

– Continue by implementing OnMessageReceived. We switch case on the message action.

  • If we receive a message from the client, we write it to the console.
  • ShowTrayIcon and HideTrayIcon will be implemented later on in this tutorial.
  • If we do not recognize the message action, we write the action name to the console.
private void OnMessageReceived(PipeMessage? message)
{
	switch(message.Action)
	{
		case ActionType.SendText:
			Console.WriteLine($"Text from client: {message.Text}");
			break;

		case ActionType.ShowTrayIcon:
			throw new NotImplementedException();

		case ActionType.HideTrayIcon:
			throw new NotImplementedException();

		default:
			Console.WriteLine($"Unknown Action Type: {message.Action}");
			break;
	}
}

– We also make sure to implement OnExceptionOccurred, which lets us see any pipe errors occurring in the console window. In a real-world scenario, we would want to add some logging here.

private void OnExceptionOccurred(Exception ex)
{
	Console.WriteLine($"Exception occured in pipe: {ex}");
}

Finally, we implement Dispose() for IDisposable. We also create a separate DisposeAsync() method since we want to dispose our server async where possible. We call the DisposeAsync method from the Dipose method and make sure this call is run synchronously by appending .GetAwaiter().GetResult().

public void Dispose()
{
	DisposeAsync().GetAwaiter().GetResult();
}

public async Task DisposeAsync()
{
	if(server != null)
		await server.DisposeAsync();
}

Start NamedPipesServer in ServiceHost

We have our pipe server ready and we need to make sure it starts with the Windows Service.

  • Add a private static NamedPipesServer field called pipeServer in the ServiceHost.cs class.
private static NamedPipesServer pipeServer;

– Add two rows in the top of InitializeServiceThread():

  • Create an instance of NamedPipesServer and assign it to the pipeServer field.
  • Call IntializeAsync() on our pipeServer. Since this method is marked as async and InitializeServiceThread is synchronous, we append .GetAwaiter().GetResult() which makes this call run synchronous.
private static void InitializeServiceThread()
{
	pipeServer = new NamedPipesServer();
	pipeServer.InitializeAsync().GetAwaiter().GetResult();

	while(!stopping)
	{
		Task.Delay(100).GetAwaiter().GetResult();
	}
}

Tray Icon (Client Application)

A tray icon typically runs in user context.

– Using the Hardcodet.NotifyIcon.Wpf Nuget package, we can quite easily build a WPF-based application with a tray icon.

Create a new project

This time, we select WPF Application. As earlier, make sure to not select the .NET Framework alternative.

– Choose a name for your project.

– Select .NET 6 as target framework.

Add Tray Icon to project

– First, install Hardcodet.NotifyIcon.Wpf Nuget package.

We need to have an .ico file in our project which can be used as the tray icon. You can download sample icon files here.

– Add the .ico file to your project and set Build Action to Resource.

Resource Dictionary

– Now we need to create a Resource Dictionary so we can display our tray icon.

  • Add a new item of type Resource Dictionary to the Tray Icon project. We can name this NotifyIconResources.xaml.

We need to tell the resource dictionary where to find the .ico file. What we also want to add is a context menu with a few items and bindings. This makes it possible to right-click our tray icon and bring up a context menu. This sample code is taken from here.

  • Edit the .xaml file just created
  • Paste the code below
  • Make sure clr-namespace matches your project’s namespace
  • Make sure IconSource points to your .ico file
  • We also define our DataContext, which is NotifyIconViewModel. This view model will be implemented later on.
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:tb="http://www.hardcodet.net/taskbar"
                    xmlns:local="clr-namespace:NamedPipesSample.TrayIcon">

    <!-- The taskbar context menu - the first row is a dummy to show off simple data binding -->
    <!--
        The "shared" directive is needed if we reopen the sample window a few times - WPF will otherwise
        reuse the same context menu (which is a resource) again (which will have its DataContext set to the old TaskbarIcon)
  -->
    <ContextMenu x:Shared="false" x:Key="SysTrayMenu">
        <MenuItem Header="Show Window" Command="{Binding ShowWindowCommand}" />
        <MenuItem Header="Hide Window" Command="{Binding HideWindowCommand}" />
        <Separator />
        <MenuItem Header="Exit" Command="{Binding ExitApplicationCommand}" />
    </ContextMenu>


    <!-- the application's NotifyIcon - started from App.xaml.cs. Declares its own view model. -->
    <tb:TaskbarIcon x:Key="NotifyIcon"
                    IconSource="/trayicon.ico"
                    ToolTipText="Double-click for window, right-click for menu"
                    DoubleClickCommand="{Binding ShowWindowCommand}"
                    ContextMenu="{StaticResource SysTrayMenu}">

        <!-- self-assign a data context (could also be done programmatically) -->
        <tb:TaskbarIcon.DataContext>
            <local:NotifyIconViewModel />
        </tb:TaskbarIcon.DataContext>
    </tb:TaskbarIcon>

</ResourceDictionary>

DelegateCommand.cs

– Create a new class DelegateCommand.cs. In this class we define logic for our command bindings we are about to implement in the view model. The sample code is taken from here.

  • We add two public properties which are not defined by the interface, but will be used in our view model:
    • Action CommandAction lets us define method calls in our view model which are invoked when we execute a command.
    • Func<bool> CanExecuteFunc lets us define an expressions in the view model which defines if the command can execute.
  • DelegateCommand class should implement ICommand which defines the following methods and event:
    • event EventHandler? CanExecuteChanged occurs when changes occur that affect whether or not the command should execute.
    • bool CanExecute(object? parameter) is true if this command can be executed; otherwise, false. This allows us to add some custom logic to globally check if a commands can execute. In the implementation we return true if CanExecuteFunc has not been set by the view model.
    • void Execute(object? parameter) defines the method to be called when the command is invoked. This makes it possible to add our custom code globally whenever a command is executed – for instance we could add some logging or tracking.
using System;
using System.Windows.Input;

namespace NamedPipesSample.TrayIcon
{
    public class DelegateCommand : ICommand
    {
        public Action CommandAction { get; set; }
        public Func<bool> CanExecuteFunc { get; set; }

        public void Execute(object parameter)
        {
            CommandAction();
        }

        public bool CanExecute(object parameter)
        {
            return CanExecuteFunc == null || CanExecuteFunc();
        }

        public event EventHandler? CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
    }
}

View Model for Tray Icon

Add a new class NotifyIconViewModel.cs. Here we will be defining the ViewModel for our Tray Icon. It provides bindable properties and commands for the Tray Icon.

Here, we implement the bindings as ICommand from our resource dictionary. The sample code is taken from here.

  • ShowWindowCommand returns a new DelegateCommand which shows our application’s MainWindow.
  • HideWindowCommand hides our MainWindow.
  • ExitApplicationCommand shuts down our application.
using System.Windows;
using System.Windows.Input;

namespace NamedPipesSample.TrayIcon
{
    public class NotifyIconViewModel
    {
        /// <summary>
        /// Shows a window, if none is already open.
        /// </summary>
        public ICommand ShowWindowCommand
        {
            get
            {
                return new DelegateCommand
                {
                    CanExecuteFunc = () => Application.Current.MainWindow == null ||
                        !Application.Current.MainWindow.IsVisible,

                    CommandAction = () =>
                    {
                        Application.Current.MainWindow = new MainWindow();
                        Application.Current.MainWindow.Show();
                    }
                };
            }
        }

        /// <summary>
        /// Hides the main window. This command is only enabled if a window is open.
        /// </summary>
        public ICommand HideWindowCommand
        {
            get
            {
                return new DelegateCommand
                {
                    CommandAction = () => Application.Current.MainWindow.Close(),

                    CanExecuteFunc = () => Application.Current.MainWindow != null &&
                        Application.Current.MainWindow.IsVisible
                };
            }
        }


        /// <summary>
        /// Shuts down the application.
        /// </summary>
        public ICommand ExitApplicationCommand
        {
            get
            {
                return new DelegateCommand { CommandAction = () => Application.Current.Shutdown() };
            }
        }
    }
}

Tray Icon’s Application Entry Point

At this stage we need to wire things up in App.xaml which is the entry point of a WPF application in .NET.

  • We set ShudownMode to OnExplicitShutdown. If not, our app will exit when the main Window is closed (an our tray icon will be no more)
  • If we do not want the main window to open when the application starts (which we might), we can omit StartupUri and its value. For now, we leave it here.
  • We specify the file where our resource dictionary resides, so it can be found by our app.
<Application x:Class="NamedPipesSample.TrayIcon.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:NamedPipesSample.TrayIcon"
             ShutdownMode="OnExplicitShutdown"
             StartupUri="MainWindow.xaml">
    
    <Application.Resources>
      
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="NotifyIconResources.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>

    </Application.Resources>
</Application>

The next step is to tell our project to use our resource dictionary. Open App.xaml.cs. This file is the code behind for App.xaml. This sample code is taken from here.

  • We add a private nullable field TaskbarIcon notifyIcon which holds our NotifyIcon from the resource dictionary.
  • By overriding OnStartup we make sure that the notifyIcon field is assigned when the application start. We also make sure to call the OnStartup method in our base class.
  • Good practice is to dispose our TaskbarIcon on application exit. We can do this by overriding OnExit. Here, we finish up by invoking OnExit in the base class.
using Hardcodet.Wpf.TaskbarNotification;
using System.Windows;

namespace NamedPipesSample.TrayIcon
{
    public partial class App : Application
    {
        private TaskbarIcon? notifyIcon;

        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            notifyIcon = (TaskbarIcon)FindResource("NotifyIcon");
        }

        protected override void OnExit(ExitEventArgs e)
        {
            notifyIcon?.Dispose();
            base.OnExit(e);
        }
    }
}

Testing our Tray Icon

We should now be able do a test run of our tray icon.

The main window will open given that the StartupUri wasn’t removed from App.xaml.

If everything worked alright, we should also be able to see our tray icon. Right-clicking the icon hopefully brings up our context menu where we can show and hide the main window as well as exit the app.

Here, we can see the effect of CanExecuteFunc which were assigned in our view model. I.e. the Show Window command is disabled when the main window is visible.

Pipe Client in Tray Icon

Finally it’s time to connect our client app to the Windows service.

– Install Nuget package H.Pipes in the tray icon project.

Add a project reference to the common project.

Create a new class NamedPipesClient.cs.

We implement this class as a singleton, so that we can easily call it everywhere in our project.

  • Make sure the NamedPipesClient class implements IDisposable so we can dispose the client on application exit.
  • Add a const string which defines the pipe’s name. This constant should have the same value as in our pipe server.
  • Create a private static NamedPipesClient field called instance will hold our single instance of this class.
  • Create a static property Instance which returns NamedPipesClient. This property does not have a setter. The getter returns our instance field if not null, otherwise it returns a new NamedPipesClient.
  • Add a private constructor which takes 0 arguments. In the constructor we set the private instance field to the current instance of our class. Since this is a singleton, the constructor will only be called once.
using H.Pipes;
using NamedPipesSample.Common;
using System;
using System.Threading.Tasks;
using System.Windows;

namespace NamedPipesSample.TrayIcon
{
    public class NamedPipesClient : IDisposable
    {
        const string pipeName = "samplepipe";

        private static NamedPipesClient instance;
        private PipeClient<PipeMessage> client;

        public static NamedPipesClient Instance
        {
            get
            {
                return instance ?? new NamedPipesClient();
            }
        }

        private NamedPipesClient()
        {
            instance = this;
        }
    }

    //...
}

Continue by adding an IntializeAsync() method to NamedPipesClient.

  • If the client is already connected we do not continue with the initialization.
  • We create a new PipeClient instance with the pipeName constant of type PipeMessage.
  • Events are wired up. Some events invoke a method using a property from args. For simplicity in this demo, we just show a message on connect and disconnect.
  • We then connect the client and send a hello message to the server.
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",
	});
}
  • We implement OnMessageReceived in a similar way to what we did in the server. In the client however, we only need to be able to listen for text messages from the server. In this demo, we just show any message in a message box.
  • If an exception occurs in the pipe, we show the exception in message box, which makes it easier to troubleshoot any issues during the initial development phase. This is not suitable for a production app.
private void OnMessageReceived(PipeMessage message)
{
	switch (message.Action)
	{
		case ActionType.SendText:
			MessageBox.Show(message.Text);
			break;
		default:
			MessageBox.Show($"Method {message.Action} not implemented");
			break;
	}
}

private void OnExceptionOccurred(Exception exception)
{
	MessageBox.Show($"An exception occured: {exception}");
}

We finish up NamedPipesClient by implementing the Dispose() method, where we dispose the pipe client, given that it’s not null.

public void Dispose()
{
	if (client != null)
		client.DisposeAsync().GetAwaiter().GetResult();
}

Initialize and Dispose Pipe Client in App.xaml.cs

As the entry point for our tray icon is App.xaml.cs, this should also be where we initialize NamedPipesClient and have it connect to our pipe server.

  • In the constructor, we call InitializeAsync()
  • Since this call is asyncbut happens in an synchronous method, and we want to catch any errors we add ContinueWith to display any errors in a MessageBox.
  • We make sure only exceptions call MessageBox.Show by adding a second argument of TaskContinuationOptions.OnlyOnFaulted
  • In a production app you probably want to handle any errors occurring in a different way.
  • We also make sure to dispose the pipe client (and thereby disconnect from pipe server) by adding calling NamedPipesClient.Instance.Dipose() in the first line of OnExit method..
using Hardcodet.Wpf.TaskbarNotification;
using System.Threading.Tasks;
using System.Windows;

namespace NamedPipesSample.TrayIcon
{
    public partial class App : Application
    {
        private TaskbarIcon notifyIcon;

        public App()
        {
            NamedPipesClient.Instance.InitializeAsync().ContinueWith(t => 
                MessageBox.Show($"Error while connecting to pipe server: {t.Exception}"),
                TaskContinuationOptions.OnlyOnFaulted);
        }

        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);
            
            notifyIcon = (TaskbarIcon)FindResource("NotifyIcon");
        }

        protected override void OnExit(ExitEventArgs e)
        {
            NamedPipesClient.Instance.Dispose();
            notifyIcon.Dispose();

            base.OnExit(e);
        }
    }
}

Testing Named Pipes Connection

At this stage, we should be able to connect from our tray icon to the Windows service. What we have achieved in our solution so far is:

  • Built a Windows service.
  • Created Common project which holds our model for communication.
  • Implemented a named pipes server within our Windows service.
  • Crafted a client app where can hide and show the main window from the tray icon’s context menu.
  • Added a named pipes client which can connect to our server.

– Let’s start the server and client project!

If we configure multiple startup projects for our solution, we can easily debug both projects simultaneously.

It works!

Feel free to get rid of those annoying messages by modifying NamedPipesClient.cs 😊

Now, that we have everything working we will continue by adding some functionality which requires admin privileges in the Windows service, which can be controlled from the client app.

Implementing Taskbar Corner Customizer

As mentioned earlier, a typical scenario for the Windows Service – Tray Icon app is to make it possible for a user without admin rights to perform some specific tasks which requires higher privileges.

In this example, we will implement the Developer Edition of Taskbar Corner Customizer. This solution makes it possible for developers to make sure the application’s tray icon is always visible in the Windows notification area aka taskbar corner. As this solution requires admin privileges (or local system) to work, it makes a good fit for the purpose of this guide.

First, we will implement TCC’s functionality in our service, and then continue by adding some buttons in the client app’s main window, where we can tell the service to perform those actions.

Please note: For this part of the guide to work properly, your client app project’s assembly name must be NamedPipesSample.TrayIcon. The TCC license attached for this guide is only valid for a tray icon process with this name.

Add Taskbar Corner Customizer package to Windows Service project

Start by downloading this file:

We are then going to add its contents to a folder called TCC in NamedPipesSample.WindowsService.

Select all files in the TCC folder, right click and select properties. Modify the Copy to Output Directory setting to Copy if newer.

TrayIconService in Windows Service

While Taskbar Corner Customizer (TCC) can be implemented in any code independent of language, the .NET implementation is a little bit easier and more powerful. First, we need to have a reference to the .NET lib in TCC.

Add a project reference to TaskbarCornerCustomizer.Shared.dll

Install Nuget package System.Management. This is required for TCC.

Create a new class TrayIconService in the Windows service project.

  • This class should implement IDisposable, so that we properly dispose the TrayManager from TCC.
  • We add a private field trayManager of type TrayManager. This field will hold our instance of TCC’s TrayManager.
  • In the constructor we first find the correct path to our TCC folder by:
    • Getting the path to our executing assembly
    • Getting the path to the folder of this assembly path
    • Combining this folder path with the TCC folder
  • A new instance of TrayManager is created and assigned to our private field. We pass the tccFolderPath as folderPath argument to our TrayManager.
  • If any errors occur when we create our TrayManager instance, we log this to the console.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using TaskbarCornerCustomizer.Shared;
using TaskbarCornerCustomizer.Shared.Models;

namespace NamedPipesSample.WindowsService
{
    internal class TrayIconService : IDisposable
    {
        private TrayManager? trayManager;

        public TrayIconService()
        {
            try
            {
                var tccFolderPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "TCC");
                trayManager = new TrayManager(folderPath: tccFolderPath);

            }
            catch (Exception ex)
            {
                Console.WriteLine($"Could not start tray manager: {ex}");
            }
        }

	//...
    }
}

We will expose two public methods in TrayIconService.cs which then use TCC to modify the visibility of our tray icon.

  • We define a private constant string TRAY_ICON_PROCESS_NAME which holds the name of our tray icon process.
  • ShowTrayIcon() iterates over user SIDs for all currently logged on users, and then calls SetConfig on our TrayManager with a single entry Dictionary which sets the visibility state of our tray icon to Visible.
  • HideTrayIcon() does the same, but sets the visibility state to Hidden.
private const string TRAY_ICON_PROCESS_NAME = "NamedPipesSample.TrayIcon";

public void ShowTrayIcon()
{
	var usersSids = trayManager.GetCurrentlyLoggedOnUsersSids().ToList();

	foreach (var userSid in usersSids)
	{
		trayManager.SetConfig(userSid, new Dictionary<string, TrayIconState>() { { TRAY_ICON_PROCESS_NAME, TrayIconState.Visible } });
	}
}

public void HideTrayIcon()
{
	var usersSids = trayManager.GetCurrentlyLoggedOnUsersSids().ToList();

	foreach (var userSid in usersSids)
	{
		trayManager.SetConfig(userSid, new Dictionary<string, TrayIconState>() { { TRAY_ICON_PROCESS_NAME, TrayIconState.Hidden } });
	}
}

Finally, we implement the Dispose method, where we dispose our TrayManager.

public void Dispose()
{
	trayManager?.Dispose();
}

Call TrayIconService in Pipe Server

Now it’s time to implement the actions for show and hide tray icon in NamedPipesServer.cs.

  • Add a private field called trayIconService of type TrayIconService. You guessed it, this field will hold our TrayIconService…
  • Create a constructor which takes 0 arguments, creates a new instance of TrayIconService and assigns it to our field.
private TrayIconService trayIconService;

public NamedPipesServer()
{
	trayIconService = new TrayIconService();
}

We also need to implement the calls in OnMessageReceived.

  • Modify the case for ActionType.ShowTrayIcon to call ShowTrayIcon on trayIconService. Add a break statement afterwards.
  • Do the same for ActionType.HideTrayIcon, but invoke HideTrayIcon() instead.
private void OnMessageReceived(PipeMessage message)
{
	switch(message.Action)
	{
		case ActionType.SendText:
			Console.WriteLine($"Text from client: {message.Text}");
			break;

		case ActionType.ShowTrayIcon:
			trayIconService.ShowTrayIcon();
			break;

		case ActionType.HideTrayIcon:
			trayIconService.HideTrayIcon();
			break;

		default:
			Console.WriteLine($"Unknown Action Type: {message.Action}");
			break;
	}
}

The Windows service project is now completed.

Show and Hide Tray Icon from Client App

The last thing we need to do is provide some UI where we can tell the service to do things.

– In NamedPipesClient.cs, add two async methods:

  • ShowTrayIconAsync will send a message to the server with an ActionType of ShowTrayIcon.
  • HideTrayIconAsync will do the same, but with an ActionType of HideTrayIcon.
public async Task ShowTrayIconAsync()
{
	await client.WriteAsync(new PipeMessage
	{
		Action = ActionType.ShowTrayIcon
	});
}

public async Task HideTrayIconAsync()
{
	await client.WriteAsync(new PipeMessage
	{
		Action = ActionType.HideTrayIcon
	});
}

– Edit MainWindow.xaml and add the xaml code below.

  • We set the title of our main window to NamedPipesSample
  • We add two buttons which invokes methods in our code behind.
<Window x:Name="MainWindow1" x:Class="NamedPipesSample.TrayIcon.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:NamedPipesSample.TrayIcon"
        mc:Ignorable="d"
        Title="NamedPipesSample" Height="450" Width="800">
    <Grid>
        <Button x:Name="ShowTrayIconBtn" Content="Show TrayIcon" HorizontalAlignment="Left" Margin="107,0,0,0" VerticalAlignment="Center" Height="73" Width="207" Click="ShowTrayIconBtn_Click"/>
        <Button x:Name="HideTrayIconBtn" Content="Hide TrayIcon" HorizontalAlignment="Left" Margin="472,0,0,0" VerticalAlignment="Center" Height="73" Width="207" Click="HideTrayIconBtn_Click"/>
    </Grid>
</Window>

– As a last step, we must implement the Click event methods for our buttons.

  • We make the methods async so that we don’t lock up the UI while the code executes.
  • ShowTrayIconBtn_Click calls ShowTrayIconAsync on the NamedPipesClient.
  • HideTrayIconBtc_Click calls HideTrayIconAsync on the NamedPipesClient.
using System.Windows;

namespace NamedPipesSample.TrayIcon
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private async void ShowTrayIconBtn_Click(object sender, RoutedEventArgs e)
        {
            await NamedPipesClient.Instance.ShowTrayIconAsync();
        }

        private async void HideTrayIconBtn_Click(object sender, RoutedEventArgs e)
        {
            await NamedPipesClient.Instance.HideTrayIconAsync();
        }
    }
}

Testing What We’ve Built

– Restart Visual Studio as Administrator. For the Taskbar Corner Customizer library to work, we need to run the Windows Service with admin rights.

– Start both the Windows service and the tray icon project. Now, we should be able to test the buttons in our main window.

– Clicking Show or Hide will then move our tray icon between the notification area aka taskbar corner, and the taskbar corner’s overflow menu.

Testing as a Windows Service

Up until now we have only been debugging our Windows service project. It’s time to test it in the wild.

Start a cmd (or PowerShell) prompt as admin.

– Create a new Windows service on your machine and specify the path to the exe of our service as binPath.

sc.exe create "NamedPipesSample" binPath="<PATH_TO_SOLUTION>\NamedPipesSample.WindowsService\bin\Debug\net6.0-windows\NamedPipesSample.WindowsService.exe"

If everything is working you should see a SUCCESS message.

Run services.msc as admin.

Right click, and select Start.

– Press F5 to refresh to verify that your service is still running and has not crashed.

Now we need to test the service with our tray icon.

– Start the tray icon project in Visual Studio.

– We can see that we are connected to our server and can receive messages.

All done.

You can delete the Windows service afterwards by running sc delete in an elevated command prompt.

sc delete "NamedPipesSample"

In a future post we will create an installer for our solution using WixSharp.

9 Replies to “Named Pipes in .NET 6 with Tray Icon and…”

  1. I would recommend using H.Pipes.AccessControl for the server, and making a call `server.AllowUsersReadWrite();` to avoid problems with pipe access from non-admin applications (and when debugging).

      1. Thank you. I added a comment about the BinaryFormatter vulnerability in the README. I also added alternative Formatters: H.Formatters.Newtonsoft.Json, H.Formatters.System.Text.Json
        and H.Formatters.Ceras

  2. Very interesting. The only point is that the [Serializable] attribute has been declared obsolete and will be removed. Accordingly, some different serialization attribute, such as Data Contract, etc. Should replace it.

  3. I was wondering, if we add an action type, Action.Kill, and in the client, we call Shutdown(), would that work? I believe there must be some disposal first but I am not sure since these are all managed and GC would find a way. Is this scenario possible?

Leave a Reply

Your email address will not be published. Required fields are marked *