You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
keep a rooted SUBCLASSPROC delegate per transparent window instead of a single process-wide slot
remove the rooted delegate when the subclassed window is destroyed
preserve the existing transparent-window behavior otherwise
Why
WindowUtilities.MakeTransparent() currently stores the subclass callback in a single static field. Once another window is subclassed, the older callback can lose its managed root while the native subclass is still active. On .NET 10 that now terminates the process with a fail-fast when Windows invokes the collected delegate.
This PR correctly addresses a real crash on .NET 10 where invoking a GC-collected delegate via native callback causes FailFast. The core approach — rooting one SUBCLASSPROC per window handle in a ConcurrentDictionary — is sound. Here are my observations:
What's good
Root cause is fixed: The single static field was the problem; the dictionary correctly keeps each delegate alive as long as its window exists.
Thread-safe: ConcurrentDictionary is the right choice here.
Static lambda in GetOrAdd: Avoids an unnecessary closure allocation. Good.
WM_NCDESTROY for cleanup: Correct choice — it's the last message a window receives, so it's the right place to release per-window resources.
Suggestions
1. Call RemoveWindowSubclass in the WM_NCDESTROY handler
"You should call RemoveWindowSubclass inside your subclass procedure while processing WM_NCDESTROY. Failure to do so could result in some memory leaks."
The current code removes the delegate from the dictionary but doesn't explicitly remove the Win32 subclass. Recommended:
(The window is being destroyed anyway, so this won't cause a functional regression, but it's defensive and avoids internal ComCtl32 state leaks.)
2. Use PInvoke constant for WM_NCDESTROY for consistency
The rest of the file uses CsWin32-generated constants (e.g. PInvoke.WM_ERASEBKGND). For consistency, add WM_NCDESTROY to NativeMethods.json and use PInvoke.WM_NCDESTROY instead of the raw magic number 0x0082. If that's not feasible, at minimum add a comment on the constant:
3. GetOrAdd factory can be called more than once under contention
ConcurrentDictionary.GetOrAdd does not guarantee the factory is called at most once — under contention, two threads may both invoke new SUBCLASSPROC(WindowSubClass) and one result is discarded. Since SUBCLASSPROC delegates here all point to the same static method, this is not a correctness problem (the discarded delegate is just an extra allocation). But if this ever changes to capture per-window state, it could become an issue. Worth a brief comment acknowledging this.
No tests
Understandable given the GC-timing nature of the bug (hard to reproduce deterministically), but a basic test that calls MakeTransparent on two different window handles, then forces a GC cycle, and verifies the dictionary still holds both delegates, would provide a regression guard. Optional, but would strengthen confidence.
Summary
The fix is correct and addresses a legitimate .NET 10 crash. The RemoveWindowSubclass omission is the only item I'd consider a real gap — everything else is style/robustness. Good catch on the bug.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
SUBCLASSPROCdelegate per transparent window instead of a single process-wide slotWhy
WindowUtilities.MakeTransparent()currently stores the subclass callback in a single static field. Once another window is subclassed, the older callback can lose its managed root while the native subclass is still active. On .NET 10 that now terminates the process with a fail-fast when Windows invokes the collected delegate.Validation
dotnet build src/libs/H.NotifyIcon/H.NotifyIcon.csproj --configuration Release /p:GeneratePackageOnBuild=falsedotnet build src/libs/H.NotifyIcon.WinUI/H.NotifyIcon.WinUI.csproj --configuration Release /p:AppxGeneratePriEnabled=false /p:GeneratePackageOnBuild=falseFixes #229