What's wrong
An invalidation travels up the tree by two routes at once:
UIElementBase.Invalidate() (TUI/Elements/UIElementBase.cs) calls Parent?.Invalidate() and raises Invalidated.
UIContainerBase subscribes child.Invalidated += OnChildInvalidated for every child it adds, and OnChildInvalidated calls Invalidate() again (TUI/Elements/UIContainerBase.cs:28, :153).
So every parent's Invalidate() runs twice for each child invalidation, and the count doubles at each level.
Repro
StackPanel root = new(); StackPanel cur = root;
for (int i = 0; i < 15; i++) { StackPanel n = new(); cur.AddChild(n); cur = n; }
TextElement leaf = new("x"); cur.AddChild(leaf);
int count = 0;
root.Invalidated += (_, _) => count++;
leaf.Text = "y";
// count == 65536 (2^16); expected 1
Why it matters
Every setter that invalidates, including Text, Position, Dimensions and IsVisible, goes through this. During arrange, every node in the tree has its Position/Dimensions set, so the cost of a relayout grows exponentially with depth. A typical 3–4-level UI already invalidates the root 8–16 times per change, and any handler on Invalidated fires that many times.
Suggested fix
Keep one propagation path:
- drop
Parent?.Invalidate() from UIElementBase.Invalidate() and rely on the container's event subscription, or
- drop the
OnChildInvalidated subscription and its unsubscribes in RemoveChild/ClearChildren.
Acceptance: a test that sets Text on a leaf nested N levels deep sees the root's Invalidated fire exactly once.
What's wrong
An invalidation travels up the tree by two routes at once:
UIElementBase.Invalidate()(TUI/Elements/UIElementBase.cs) callsParent?.Invalidate()and raisesInvalidated.UIContainerBasesubscribeschild.Invalidated += OnChildInvalidatedfor every child it adds, andOnChildInvalidatedcallsInvalidate()again (TUI/Elements/UIContainerBase.cs:28,:153).So every parent's
Invalidate()runs twice for each child invalidation, and the count doubles at each level.Repro
Why it matters
Every setter that invalidates, including
Text,Position,DimensionsandIsVisible, goes through this. During arrange, every node in the tree has itsPosition/Dimensionsset, so the cost of a relayout grows exponentially with depth. A typical 3–4-level UI already invalidates the root 8–16 times per change, and any handler onInvalidatedfires that many times.Suggested fix
Keep one propagation path:
Parent?.Invalidate()fromUIElementBase.Invalidate()and rely on the container's event subscription, orOnChildInvalidatedsubscription and its unsubscribes inRemoveChild/ClearChildren.Acceptance: a test that sets
Texton a leaf nested N levels deep sees the root'sInvalidatedfire exactly once.