Implementing a System Tray App with WPF and MVVM

fujieda

Kazuhiro Fujieda

Posted on April 27, 2021

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 so it 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);
            }));
Enter fullscreen mode Exit fullscreen mode

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.

private void Notify(string message)
{
    NotifyRequest = new NotifyIconWrapper.NotifyRequestRecord
    {
        Title = "Notify",
        Text = message,
        Duration = 1000
    };
}
Enter fullscreen mode Exit fullscreen mode

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.

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

<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>
Enter fullscreen mode Exit fullscreen mode

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">
Enter fullscreen mode Exit fullscreen mode

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.

ublic WindowState WindowState
{
    get => _windowState;
    set
    {
        ShowInTaskbar = true;
        SetProperty(ref _windowState, value);
        ShowInTaskbar = value != WindowState.Minimized;
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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
<bh:Interaction.Triggers>
    <bh:EventTrigger EventName="Loaded">
        <bh:InvokeCommandAction Command="{Binding LoadedCommand}"/>
    </bh:EventTrigger>
Enter fullscreen mode Exit fullscreen mode
ViewModel
public MainWindowViewModel()
{
    LoadedCommand = new RelayCommand(Loaded);
...
}

public ICommand LoadedCommand { get; }

private void Loaded()
{
    WindowState = WindowState.Minimized;
}
Enter fullscreen mode Exit fullscreen mode

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.

    <bh:EventTrigger EventName="Closing">
        <bh:InvokeCommandAction Command="{Binding ClosingCommand}" PassEventArgsToCommand="True"/>
    </bh:EventTrigger>
</bh:Interaction.Triggers>
Enter fullscreen mode Exit fullscreen mode
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;
}
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
fujieda
Kazuhiro Fujieda

Posted on April 27, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related