Implementing a System Tray App with WPF and MVVM

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.

The implementation has two distinctive points. First, it does not use notable WPF NotifyIcon because the license, CPOL, isn't compatible with any OSS licenses. Then, the implementation obeys the MVVM pattern and has no code behind.

A Wrapper of NotifyIcon

The central part of the implementation is NotifyIconWrapper, a wrapper of the NotifyIcon class in WinForms. The wrapper has the dependency property NotifyRequest to invoke the ShowBaloonTip method.

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, as follows, the callback function invoked by the change of the value defined in PropertyMetadata invokes ShowBaloonTip based on the record.

public NotifyIconWrapper.NotifyRequestRecord? NotifyRequest
{
    get => _notifyRequest;
    set => SetProperty(ref _notifyRequest, value);
}

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.

NotifyIconWrapper defines the routed event OpenSelected and ExitSelected raised by the event handler OpenItemOnClock and ExitItemOnClick 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.

<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.

<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 set ShowInTaskbar false to hide the application from the taskbar. To restore the window, the ViewModel sets WindowState.Normal to the WindowState property.

public 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.

A window consisting of only the titlebar leaves at the bottom of the screen.

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.

XAML ViewModel

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.

XAML ViewModel

Conclusion

This article explained the implementation of the system tray application in the GitHub repository. It depends on Microsoft.Toolkit.Mvvm but can be easily ported to other MVVM frameworks. The license is 0BSD, equal to the public domain. You can freely use the code to create another system tray application.