Blog

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.

Quite Simple Memory Pool in C#

This article shows a quite simple memory pool to make a thread-unsafe library thread-safe without performance degradation in single-threaded programs. Same as the previous article, this article is about DynaJson.

Thread-safety requires overhead to allocate an instance by each invocation to isolate data being altered. Unless thread-safety is required, we can use a static class or a singleton to eliminate any additional allocation.

Continue reading "Quite Simple Memory Pool in C#"

[Unity] How to Use Physics.OverlapCapsule

Capsule and Cube Collider

You can use Physics.OverlapCapsule to get two Colliders overlapped with a CapsulCollider as above. This method takes the position and the size of a capsule and returns all Colliders overlapped by the capsule.

You can get the Colliders by configuring a Kinematic Rigidbody Trigger Collier and handling the OnTriggerEnter message, but the method is handy.

Continue reading "[Unity] How to Use Physics.OverlapCapsule"

How to Make JSON Parser Strict

I developed a JSON Parser for C# named DynaJson. It is very strict to the standard of RFC 8259. It accepts all conformant and rejects all non-conformant JSONs except for two exceptions.

One exception is trailing-commas. Another is leading 0 in numbers, for example, 02 and -02. The former is for practicality. The latter is for compatibility with DynamicJson.

JSON's grammar is simple, but carelessly implemented parsers don't accept all conformant and not reject non-conformant JSONs. An excellent article of Parsing JSON is a Minefield shows where mines are in implementing JSON parsers.

Continue reading "How to Make JSON Parser Strict"

Mount Any Windows Folder into Containers on Docker for Windows

In a native Docker environment, you can mount /source in a container host onto /destination in a container by docker run -v /source:/destination and access it from the container.

Well then, how can you mount C:\Source in Windows onto /destination in a container on Docker for Windows? You can't directly mount C:\Source in the VM host into the container, of course.

As the first step, you have to set up C:\Source as a shared folder for a VM when you create it with docker-machine. You can specify the shared folder with the --virtualbox-share-folder option of the VirtualBox deriver as follows.

docker-machine create --driver virtualbox --virtualbox-share-folder=C:\Source:Source

Continue reading "Mount Any Windows Folder into Containers on Docker for Windows"