Implementing a System Tray App with WPF and MVVM
Tags: csharp, wpf, mvvm
Published: 2021-04-27 11:23:20

This article illustrates the implementation of a system tray application with WPF and the MVVM pattern. The full source code is in [the GitHub repository](https://github.com/fujieda/SystemTrayApp.WPF/).

The implementation has two distinctive points. First, it **does not use** notable [WPF NotifyIcon](http://www.hardcodet.net/wpf-notifyicon) because the license, CPOL, isn't compatible with any OSS licenses. Then, the implementation obeys the MVVM pattern so it has **no code behind**.

## A Wrapper of NotifyIcon

The central part of the implementation is [NotifyIconWrapper](https://github.com/fujieda/SystemTrayApp.WPF/blob/main/SystemTrayApp.WPF/NotifyIconWrapper.cs), a wrapper of the [NotifyIcon](https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.notifyicon?view=net-5.0) class in WinForms. The wrapper has the dependency property `NotifyRequest` to invoke the `ShowBaloonTip` method.

```csharp
private static readonly DependencyProperty NotifyRequestProperty =
    DependencyProperty.Register("NotifyRequest", typeof(NotifyRequestRecord), typeof(NotifyIconWrapper),
        new PropertyMetadata(
            (d, e) =>;
            {
                var r = (NotifyRequestRecord)e.NewValue;
                ((NotifyIconWrapper)d)._notifyIcon?.
                    ShowBalloonTip(r.Duration, r.Title, r.Text, r.Icon);
            }));
```

When the application sets a `NotifyRequestRecord` to the bound property in the ViewModel, the callback function is invoked by the change of the value defined in `PropertyMetadata` invokes `ShowBaloonTip` based on the record.

```csharp
private void Notify(string message)
{
    NotifyRequest = new NotifyIconWrapper.NotifyRequestRecord
    {
        Title = "Notify",
        Text = message,
        Duration = 1000
    };
}
```

The following is the constructor. If the constructor is invoked not by the XAML editor, It creates the NotifyIcon and the context menu to which event handlers are attached.

```csharp
public NotifyIconWrapper()
{
    if (DesignerProperties.GetIsInDesignMode(this))
        return;
    _notifyIcon = new NotifyIcon
    {
        Icon = Icon.ExtractAssociatedIcon(Assembly.GetExecutingAssembly().Location),
        Visible = true,
        ContextMenuStrip = CreateContextMenu()
    };
    _notifyIcon.DoubleClick += OpenItemOnClick;
    Application.Current.Exit += (obj, args) => { _notifyIcon.Dispose(); };
}

private ContextMenuStrip CreateContextMenu()
{
    var openItem = new ToolStripMenuItem("Open");
    openItem.Click += OpenItemOnClick;
    var exitItem = new ToolStripMenuItem("Exit");
    exitItem.Click += ExitItemOnClick;
    var contextMenu = new ContextMenuStrip {Items = {openItem, exitItem}};
    return contextMenu;
}
```

NotifyIconWrapper defines the routed event `OpenSelected` and `ExitSelected` raised by the event handler `OpenItemOnClick` and `ExiteItemOnClick` shown above respectively.

The following XAML shows how to use the wrapper. The dependency property `NotifyRequest` is bound to the property mentioned above. Each routed event is bound to the corresponding routed command with [Xaml.Behaviors.WPF](https://github.com/microsoft/XamlBehaviorsWpf).

```xml
<Window x:Class="SystemTrayApp.WPF.MainWindow"
        xmlns:bh="http://schemas.microsoft.com/xaml/behaviors"
...
    <Grid>
        <local:NotifyIconWrapper NotifyRequest="{Binding NotifyRequest}">
            <bh:Interaction.Triggers>
                <bh:EventTrigger EventName="OpenSelected">
                    <bh:InvokeCommandAction Command="{Binding NotifyIconOpenCommand}">
                </bh:EventTrigger>
```

## Hiding and Restoring Window

The application implements hiding and restoring its window through data bindings. The following XAML bind WindowState and ShowInTaskbar to the properties in the ViewModel.

```xml
<Window x:Class="SystemTrayApp.WPF.MainWindow"
...
        ShowInTaskbar="{Binding ShowInTaskbar}"
        WindowState="{Binding WindowState}"
        Title="SystemTrayApp" Height="200" Width="300">
```

When the window gets minimized, the bound property `WindowState` is changed. The set accessor sets `ShowInTaskbar` false to hide the application from the taskbar. To restore the window, the ViewModel sets `WindowState.Normal` to the `WindowState` property.

```csharp
ublic WindowState WindowState
{
    get => _windowState;
    set
    {
        ShowInTaskbar = true;
        SetProperty(ref _windowState, value);
        ShowInTaskbar = value != WindowState.Minimized;
    }
}
```

The weird workaround `ShowInTaskbar = true` is to prevent the following window consisting only of the title from leaving at the bottom of the desktop on minimizing.

![TitleOnlyWindow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/201wum69s8l10uq641kj.png)

## Loaded and Closing Events

The application hides its window on starting up by handling the Loaded event of the window. Xaml.Behavior.WPF binds the event to the routed command `LoadedCommand`. The command sets `WindowState.Minimized` to `WindowState` to hide the window. In this approach, the window inevitably appears just for a moment on starting up.

```xml
XAML
<bh:Interaction.Triggers>
    <bh:EventTrigger EventName="Loaded">
        <bh:InvokeCommandAction Command="{Binding LoadedCommand}"/>
    </bh:EventTrigger>
```
```csharp
ViewModel
public MainWindowViewModel()
{
    LoadedCommand = new RelayCommand(Loaded);
...
}

public ICommand LoadedCommand { get; }

private void Loaded()
{
    WindowState = WindowState.Minimized;
}
```

When users click the close button on the title bar, the application must cancel the `Closing` event to prevents itself from existing. To realize it, the event handler needs to set true to the `Cancel` property of the event argument. Xaml.Behavior.WPF passes the argument to the routed command when `PassEventArgsToCommand` is true so that the command can do it.

```xml
    <bh:EventTrigger EventName="Closing">
        <bh:InvokeCommandAction Command="{Binding ClosingCommand}" PassEventArgsToCommand="True"/>
    </bh:EventTrigger>
</bh:Interaction.Triggers>
```

```csharp
ViewModel
public MainWindowViewModel()
{
...
    ClosingCommand = new RelayCommand<CancelEventArgs>(Closing);
...
}

public ICommand CloasingCommand { get; }

private void Closing(CancelEventArgs? e)
{
    if (e == null)
        return;
    e.Cancel = true;
    WindowState = WindowState.Minimized;
}
```

## Conclusion

This article explained the implementation of the system tray application in [the GitHub repository](https://github.com/fujieda/SystemTrayApp.WPF/). It depends on [Microsoft.Toolkit.Mvvm](https://docs.microsoft.com/en-us/windows/communitytoolkit/mvvm/introduction) but can be easily ported to other MVVM frameworks. The license is [0BSD](https://choosealicense.com/licenses/0bsd/), equal to the public domain. You can freely use the code to create another system tray application.