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);
}));
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
};
}
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;
}
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>
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 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;
}
}
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.
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>
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.
<bh:EventTrigger EventName="Closing">
<bh:InvokeCommandAction Command="{Binding ClosingCommand}" PassEventArgsToCommand="True"/>
</bh:EventTrigger>
</bh:Interaction.Triggers>
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. 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.
Top comments (0)