Touch001 Solving Tray Icon and minimalize UI problem on Arch Linux with C# in Avalonia
Tags: csharp, avalonia, trayicon, linux
Published: 2022-03-31 20:23:44

When I started to think about features for my project it came to my mind, that when the user minimalizes the application it should be hidden from the taskbar, and when I was thinking deeply then I figured out that only hiding will not be enough. The user needs to be able somehow to return from the hidden application. And the solution is to have a Tray Icon, on Windows, it is always on the taskbar on the right side, where you can find some tray icons like Network or Volume. I was curious if it will work on Linux too. Because I am using Endeavour with Plasma flavour, I knew there it should be possible somehow, but I was not sure if the Avalonia framework supports it. I googled this problem and found out there are some git projects solving this problem too. So I decided to open IDE and write some code. Firstly I started with a minimalizing problem because I thought it will be easier. When I started coding this, I easily figured out that it will not be so easy. So I googled a little bit for the first problem in official Avalonia Sources. I found out that I can do it in a way that I can register to PropertyChanged event on MainWindow, filter my specific property and then hide MainWindow. So I tried it like bellow

```csharp
private void MyMainWindow_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
    if (sender is MainWindow && e.NewValue is WindowState windowState && windowState == WindowState.Minimized)
    {
        myMainWindow?.Hide();
    }
}
```

I felt there should be an easier solution like this. So I googled a little bit more. I ended on google around page 10, where I find an Avalonion Gitter post on how I can try it. The post was telling about overriding the HandleWindowStateChanged method. So I tried it, and it worked properly. So then I tried it on Endeavour Linux and easily figured it out, that on Linux Task Bar Icon is not hiding after minimalize. I also saw some glitches on Windows and Linux, when you returned back to MainWindow it was not shown properly. The same post suggested calling some methods. I tried it and it worked on both platforms. And final code for Minimalize, Hide app and Hide Icon Bar is written below. Maybe it will help someone someday.

```csharp
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    protected override void HandleWindowStateChanged(WindowState state)
    {
        if (state == WindowState.Minimized)
        {
            ShowInTaskbar = false;
            Hide();
        }

        if(state == WindowState.Normal)
        {
            ShowInTaskbar = true;
            this.BringIntoView();
            Activate();
            Focus();
            base.HandleWindowStateChanged(state);
        }
    }
}
```

Right now I do not know how to write tests for this behaviour, but at least it can be the next Touch article for it.

At this moment I still had not implemented a solution for Tray Icon. On the official Avalonia webpage, I found that Avalonia supports Tray Icon already, so I do not need to use some solution from git. But unlucky there was no documentation or Wiki for this how to use it. So I experimented.

At the end I had a code similar to this.

```csharp
public partial class App : Application
{
    private MainWindow? myMainWindow;

    public override void Initialize()
    {
        AvaloniaXamlLoader.Load(this);
    }

    public override void OnFrameworkInitializationCompleted()
    {
        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
        {
            myMainWindow = new MainWindow
            {
                DataContext = new MainWindowViewModel(),
            };
            desktop.MainWindow = myMainWindow;

            RegisterTrayIcon();
        }

        base.OnFrameworkInitializationCompleted();
    }

    private void RegisterTrayIcon()
    {
        var trayIcon = new TrayIcon
        {
            IsVisible = true,
            ToolTipText = "TestToolTipText",
            Command = ReactiveCommand.Create(ShowApplication),
            Icon = new WindowIcon("/Assets/avalonia-logo.ico")
        };
    }

    private void ShowApplication()
    {
        if(myMainWindow != null)
        {
            myMainWindow.WindowState = WindowState.Normal;
            myMainWindow.Show();
        }
    }
}
```

And It was not working. I used also the ReactiveUI library for easy Command creation as shown below. 

```csharp
Command = ReactiveCommand.Create(ShowApplication),
```

Where I also implemented behaviour what will happen when the user clicks on Tray Icon, for us it will show the application back.

```csharp
private void ShowApplication()
{
    if(myMainWindow != null)
    {
        myMainWindow.WindowState = WindowState.Normal;
        myMainWindow.Show();
    }
}
```

It failed during runtime when it was loading Icon. I figured it out that for this I cannot use .ico, but some transparent png image. So I needed to load it as Bitmap and then create WindowIcon. So I replaced this line with the code below.

```csharp
Icon = new WindowIcon(new Bitmap("C:/Icons/test.png"))
```

After this change, the code worked correctly. The app was hiding, when I clicked on Tray Icon it was shown back. but when I closed an application it failed during closing. Luckily exception thrown at this moment helped me to solve this problem. It was saying that I did not set Tray Icon value as Attached property. So I implemented it and yes, it works how it is expected on both platforms.

So the final code is shown below. Feel free to use and comment below the article. What do you think about this?

```csharp
public partial class App : Application
{
    private MainWindow? myMainWindow;

    public override void Initialize()
    {
        AvaloniaXamlLoader.Load(this);
    }

    public override void OnFrameworkInitializationCompleted()
    {
        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
        {
            myMainWindow = new MainWindow
            {
                DataContext = new MainWindowViewModel(),
            };
            desktop.MainWindow = myMainWindow;

            RegisterTrayIcon();
        }

        base.OnFrameworkInitializationCompleted();
    }

    private void RegisterTrayIcon()
    {
        var trayIcon = new TrayIcon
        {
            IsVisible = true,
            ToolTipText = "TestToolTipText",
            Command = ReactiveCommand.Create(ShowApplication),
            Icon = new WindowIcon(new Bitmap("C:/Icons/test.png"))
        };

        var trayIcons = new TrayIcons
        {
            trayIcon
        };

        SetValue(TrayIcon.IconsProperty, trayIcons);
    }

    private void ShowApplication()
    {
        if(myMainWindow != null)
        {
            myMainWindow.WindowState = WindowState.Normal;
            myMainWindow.Show();
        }
    }
}
```