Skip to content

Fix TrayIcon attachment lifecycle - #22118

Open
Linlccc wants to merge 6 commits into
AvaloniaUI:mainfrom
Linlccc:fix-trayicon-attach
Open

Fix TrayIcon attachment lifecycle#22118
Linlccc wants to merge 6 commits into
AvaloniaUI:mainfrom
Linlccc:fix-trayicon-attach

Conversation

@Linlccc

@Linlccc Linlccc commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

What does the pull request do?

Changes the TrayIcon lifecycle so that constructing a TrayIcon does not immediately register it with the operating system.

A native tray icon is now created only after the TrayIcon is attached to an Application through TrayIcon.Icons.

What is the current behavior?

Constructing a TrayIcon immediately creates its platform implementation.

On macOS, creating the platform implementation also creates an NSStatusItem, causing the icon to appear in the menu bar even when TrayIcon.SetIcons has not been called and the icon has not been added to a TrayIcons collection.

Because IsVisible defaults to true, a tray icon may also be displayed before all of its properties have been initialized.

Additionally:

  • Replaced TrayIcons collections remain subscribed to CollectionChanged.
  • Adding items to an attached TrayIcons collection does not explicitly attach them.
  • Clearing a TrayIcons collection does not expose the removed items through OldItems, so their platform implementations cannot be detached.
  • Tray icons created outside the application-owned collection can escape application shutdown cleanup.

What is the updated/expected behavior with this PR?

Constructing a TrayIcon only creates the managed object. It does not create or display a native tray icon.

The native platform implementation is created only when the TrayIcon is:

  • Included in a TrayIcons collection assigned through TrayIcon.SetIcons.
  • Added to an already attached TrayIcons collection.

When attached, the previously configured icon, tooltip, menu, visibility, and macOS template-icon properties are synchronized with the newly created platform implementation.

When a tray icon is removed, the collection is cleared, or the collection is replaced, its platform implementation is detached and disposed.

All tray icons attached to the application are disposed during dispatcher shutdown.

How was the solution implemented (if it's not obvious)?

An explicit attach/detach lifecycle was added to TrayIcon.

  • Platform implementation creation was moved from the constructor to the attach operation.
  • TrayIcon.Icons property changes attach icons from the new collection and detach icons from the previous collection.
  • Collection changes now handle both added and removed icons.
  • Previous collections are unsubscribed from CollectionChanged when replaced.
  • TrayIcons uses remove notifications when cleared so the removed icons are available for cleanup.
  • Configured properties are synchronized when the platform implementation is created.
  • Shutdown cleanup is registered against the active UI dispatcher.

Checklist

  • Added unit tests (if possible)?

Fixed issues

Fixes #18548

- Create the native tray icon only after it is attached to Application
- Unsubscribe from the previous TrayIcons collection when it is replaced
- Ensure tray icons cannot escape cleanup during application shutdown
@MrJul MrJul added bug backport-candidate-12.1.x Consider this PR for backporting to 12.1 branch labels Sep 1, 2026
@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.2.999-cibuild0069250-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.2.999-cibuild0069295-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@Linlccc

Linlccc commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

The initial SetIsVisible(false) call was added to prevent the tray icon from briefly appearing in a partially initialized state while its icon, tooltip, and menu were being configured.

However, on macOS, CreateTrayIcon() immediately creates a visible NSStatusItem. This causes a visible → hidden → visible transition during attachment. The transition appears to affect AppKit's native window ordering and changes NSWindow.orderedIndex, causing the unrelated window-order integration test to fail.

Removing the initial visibility toggle makes the macOS integration test pass.

@Linlccc

Linlccc commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Regardless, I believe the test should verify the relative ordering of the modal and parent windows rather than asserting an absolute orderedIndex.

@Linlccc

Linlccc commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

@MrJul
The latest CI run passed after removing the initial _impl.SetIsVisible(false) call. However, I also manually tested the same modal-window ordering scenario locally on macOS with that call present, and the modal window consistently reported orderedIndex == 1.

Could someone please rerun the failed macOS integration job from build 69251? I would like to determine whether the original failure is reproducible or was a flaky/environment-specific result.

@MrJul MrJul left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've only left a small style comment, otherwise the implementation itself looks very good to me.

Comment thread src/Avalonia.Controls/TrayIcon.cs Outdated

private void Attach()
{
if (_isAttached || _isDisposed) return;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style nit: put the return on its own line. Applicable to all changes made in this file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay.

@MrJul

MrJul commented Sep 2, 2026

Copy link
Copy Markdown
Member

Regardless, I believe the test should verify the relative ordering of the modal and parent windows rather than asserting an absolute orderedIndex.

Yes, it probably should.

Could someone please rerun the failed macOS integration job from build 69251? I would like to determine whether the original failure is reproducible or was a flaky/environment-specific result.

I've re-run it: https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/results?buildId=69251&view=results edit: and it passes.

@Linlccc

Linlccc commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

I've re-run it: https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/results?buildId=69251&view=results edit: and it passes.

using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.Manual))
{
mainWindow.Click();
var secondaryWindowIndex = GetWindowOrder("SecondaryWindow");
Thread.Sleep(300); // sync with timer
Assert.Equal(1, secondaryWindowIndex);
}

_orderTextBox = CurrentOrder;
_timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) };
_timer.Tick += TimerOnTick;
_timer.Start();

Yes, I think I've found the reason. The wait here is ineffective; it should wait first and then retrieve.

@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.2.999-cibuild0069346-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@MrJul

MrJul commented Sep 4, 2026

Copy link
Copy Markdown
Member

On Windows and macOS there's no issue, the PR works as expected.

On Linux, the app crashes everytime the application exits, in DBusTrayIconImpl. The IsVisible rapid toggle makes the underlying async code behave wrongly. The existing code lacks robustness: #21980 should fix it. I'll wait for it to be merged before re-testing and approving this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-candidate-12.1.x Consider this PR for backporting to 12.1 branch bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect tray icon

3 participants