Skip to content

Fixes FluidMoveBehavior static dictionary memory leaks - #198

Merged
Brian Lagunas (brianlagunas) merged 6 commits into
mainfrom
memory-leak-fix
Apr 7, 2026
Merged

Fixes FluidMoveBehavior static dictionary memory leaks#198
Brian Lagunas (brianlagunas) merged 6 commits into
mainfrom
memory-leak-fix

Conversation

@brianlagunas

Copy link
Copy Markdown
Collaborator

Description of Change

Addresses memory leaks in FluidMoveBehavior's static TagDictionary and TransitionStoryboardDictionary. Previously, these dictionaries could hold strong references to UIElements and Storyboards even after the associated elements were removed from the visual tree or garbage collected, leading to memory accumulation over time.

This change introduces several improvements:

  • The TagData structure now uses WeakReference for Child and Parent properties, allowing elements to be garbage collected when no other strong references exist.
  • Implements a throttled purge mechanism in OnLayoutUpdated to regularly clean up TagDictionary entries whose associated child elements are no longer alive or are unloaded from the visual tree.
  • Adds a PurgeDeadStoryboards method to proactively remove Storyboard entries from TransitionStoryboardDictionary if their corresponding FrameworkElement key is unloaded, if the associated TagData's child is dead, or if the TagDictionary entry is missing.
  • Incorporates eager removal of dead entries within UpdateTagData and UpdateLayoutTransitionCore to prevent re-adding already-collected elements.
  • Ensures Storyboards are explicitly stopped and removed when a FluidMoveBehavior is detached or its tracked children are removed from the panel.
  • Includes a new comprehensive unit test suite, FluidMoveBehaviorTest.cs, to validate the weak reference behavior, dictionary purging, and overall retention logic in various scenarios (element removal, reparenting).

Note on ConditionalWeakTable: I considered replacing the static TagDictionary with ConditionalWeakTable to eliminate strong-key retention, but I don’t think it is a safe drop-in change here. While it works well for reference-identity keys like FrameworkElement and some DataContext cases, FluidMoveBehavior also supports TagPath, which means tags can be scalar values or strings and currently rely on normal Dictionary<object, ...> equality semantics (Equals / GetHashCode). ConditionalWeakTable uses reference identity, not value equality, so switching to it could break matching behavior for TagPath-based tags even when the values are logically equal. It also would not help much with transitionStoryboardDictionary, which still needs explicit enumeration and cleanup for stopping storyboards and tearing down overlay state. Because of that, I think ConditionalWeakTable is appealing for leak prevention but too risky here unless the behavior is redesigned around reference-identity-only tags.

Bugs Fixed

  • FluidMoveBehavior memory leak #38 - Fixes memory leaks in FluidMoveBehavior where static dictionaries retained references to UI elements and associated storyboards after they were no longer active.

API Changes

Added:

  • TagData.IsAlive property (internal)
  • FluidMoveBehaviorBase.ResetPurgeThrottle() method (internal)
  • FluidMoveBehavior.PurgeDeadStoryboards() method (internal)
  • FluidMoveBehavior.InjectStoryboardEntry() method (internal)
  • FluidMoveBehavior.StoryboardDictionaryContainsKey() method (internal)
  • FluidMoveBehavior.ClearStoryboardDictionary() method (internal)

Changed:

  • TagData.Child and TagData.Parent now use WeakReference internally.
  • FluidMoveBehavior.OnDetaching() behavior to include storyboard cleanup.
  • Internal purge logic within FluidMoveBehaviorBase.OnLayoutUpdated.

Removed:

  • TagData.Timestamp property.

Behavioral Changes

Applications using FluidMoveBehavior will experience improved memory management, particularly in scenarios involving dynamic UI where elements are frequently added, removed, or reparented. Elements and their associated Storyboards will now be correctly garbage collected, preventing a steady increase in memory footprint that could occur under previous implementations.

PR Checklist

  • Has tests (new FluidMoveBehaviorTest.cs file provides extensive unit tests)
  • Rebased on top of master at time of PR
  • Changes adhere to coding standard

…ionStoryboardDictionary

Replace strong FrameworkElement references in TagData with WeakReference<FrameworkElement>
to prevent elements removed from the visual tree from being retained indefinitely.

- TagData.Child/Parent now use WeakReference-backed properties with unchanged API
- Remove unreliable timestamp-based purge; replace with throttled liveness check
  that detects dead WeakReferences and unloaded element-type keys
- Add PurgeDeadStoryboards to clean TransitionStoryboardDictionary for both
  element-type and DataContext-type keys
- Add OnDetaching to FluidMoveBehavior to stop/remove active storyboards on detach
- Null-guard tagData.Child reads in UpdateLayoutTransitionCore (cache in local var)
- Add unit tests for WeakReference behavior, dictionary purge, and storyboard cleanup

Fixes #38
Comment thread src/Microsoft.Xaml.Behaviors/Layout/FluidMoveBehavior.cs
Comment thread src/Microsoft.Xaml.Behaviors/Layout/FluidMoveBehavior.cs Outdated
Comment thread src/Microsoft.Xaml.Behaviors/Layout/FluidMoveBehavior.cs
Comment thread src/Microsoft.Xaml.Behaviors/Layout/FluidMoveBehavior.cs
Comment thread src/Microsoft.Xaml.Behaviors/Layout/FluidMoveBehavior.cs Outdated
Comment thread src/Microsoft.Xaml.Behaviors/Layout/FluidMoveBehavior.cs Outdated
Comment thread Test/UnitTests/FluidMoveBehaviorTest.cs
Comment thread Test/UnitTests/FluidMoveBehaviorTest.cs Outdated
Comment thread Test/UnitTests/FluidMoveBehaviorTest.cs
@brianlagunas
Brian Lagunas (brianlagunas) merged commit b219a7c into main Apr 7, 2026
5 checks passed
@brianlagunas
Brian Lagunas (brianlagunas) deleted the memory-leak-fix branch April 7, 2026 14:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants