diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationCore/MS/internal/Automation/ElementProxy.cs b/src/Microsoft.DotNet.Wpf/src/PresentationCore/MS/internal/Automation/ElementProxy.cs index cbc1e67b909..da66a8e47cf 100644 --- a/src/Microsoft.DotNet.Wpf/src/PresentationCore/MS/internal/Automation/ElementProxy.cs +++ b/src/Microsoft.DotNet.Wpf/src/PresentationCore/MS/internal/Automation/ElementProxy.cs @@ -540,9 +540,22 @@ internal static ReferenceType AutomationInteropReferenceType // //------------------------------------------------------ + #region Internal Methods for Disconnect + + // Called during UIA disconnect to sever the strong reference from this + // proxy to the automation peer. This allows the peer (and its associated + // data items) to be garbage collected even if UIA/client still holds a + // COM reference to this CCW temporarily. + internal void ClearPeer() + { + _peer = null; + } + + #endregion Internal Methods for Disconnect + #region Private Fields - private readonly object _peer; + private object _peer; #endregion Private Fields } diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Automation/Peers/AutomationPeer.cs b/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Automation/Peers/AutomationPeer.cs index 5d4ce668f6c..278b58fbc2d 100644 --- a/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Automation/Peers/AutomationPeer.cs +++ b/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Automation/Peers/AutomationPeer.cs @@ -4,6 +4,7 @@ //#define ENABLE_AUTOMATIONPEER_LOGGING // uncomment to include logging of various activities using System.Collections; +using System.Runtime.InteropServices; using System.Windows.Threading; using System.Windows.Automation.Provider; using MS.Internal; @@ -1483,6 +1484,7 @@ private void EnsureChildren() // UpdateSubtree is not called on it yet. if (!_childrenValid || _ancestorsInvalid) { + List oldChildren = _children; _children = GetChildrenCore(); if (_children != null) { @@ -1495,6 +1497,19 @@ private void EnsureChildren() } } _childrenValid = true; + + // Disconnect removed peers (same logic as UpdateChildrenInternal) + if (oldChildren != null) + { + HashSet newSet = (_children != null) ? new HashSet(_children) : null; + foreach (var old in oldChildren) + { + if (newSet == null || !newSet.Contains(old)) + { + DisconnectPeerFromUia(old); + } + } + } } } @@ -1812,6 +1827,59 @@ private IRawElementProviderSimple ProviderFromPeerNoDelegation(AutomationPeer pe return ElementProxy.StaticWrap(peer, referencePeer); } + /// + /// Disconnects a peer from the UI Automation framework by calling + /// UiaDisconnectProvider on its ElementProxy CCW. + /// This causes the UIA client-side to release its COM references, allowing + /// the CCW ref count to drop to zero so the managed objects can be GC'd. + /// + /// + /// After disconnecting a peer from UIA, this method also clears the peer's + /// _children list to sever references to child peers. Without this, a + /// disconnected ItemAutomationPeer would still root its cached cell peers, + /// which in turn root DataGridCell/DataGridRow containers via their _owner + /// field — preventing GC of recycled/discarded containers. + /// We do NOT call UiaDisconnectProvider on children (they may be shared with + /// recycled containers that are still live), but clearing the parent's + /// _children list removes the strong reference chain. + /// + private static void DisconnectPeerFromUia(AutomationPeer peer) + { + if (peer == null) + return; + + // UiaDisconnectProvider MUST NOT be called during a UIA callback + // (e.g., during Navigate/FindAll handling). EnsureChildren and + // UpdateChildrenInternal are invoked from within UIA callbacks, so + // we must defer the actual disconnect to a separate dispatcher operation. + WeakReference proxyWeakRef = peer._elementProxyWeakReference; + if (proxyWeakRef?.Target is ElementProxy proxy) + { + // Sever the strong reference from proxy back to the peer immediately. + // This allows the peer (and its data items) to be GC'd even before + // the deferred UiaDisconnectProvider call executes. + proxy.ClearPeer(); + + // Defer the UIA disconnect to run outside the UIA callback context. + peer.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Background, + new Action(() => + { + UiaDisconnectProvider(proxy); + })); + } + + peer._elementProxyWeakReference = null; + + // Sever the reference from this peer to its cached children. + // This breaks the chain: disconnected peer → child peers → _owner → UI containers, + // allowing old containers (DataGridRow/Cell) to be collected. + peer._children = null; + peer._childrenValid = false; + } + + [DllImport("UIAutomationCore.dll", EntryPoint = "UiaDisconnectProvider", CharSet = CharSet.Unicode)] + private static extern int UiaDisconnectProvider(IRawElementProviderSimple provider); + /// /// When one AutomationPeer is using the pattern of another AutomationPeer instead of exposing /// it in the children collection (example - ListBox exposes IScrollProvider from internal ScrollViewer @@ -1892,10 +1960,6 @@ internal void UpdateChildrenInternal(int invalidateLimit) _childrenValid = false; EnsureChildren(); - // Callers have only checked if automation clients are present so filter for any interest in this particular event. - if (!EventMap.HasRegisteredEvent(AutomationEvents.StructureChanged)) - return; - //store old children in a hashset if(oldChildren != null) { @@ -1937,6 +2001,23 @@ internal void UpdateChildrenInternal(int invalidateLimit) //calls for "bulk" notification, use per-child notification, otherwise use "bulk" int removedCount = (hs == null ? 0 : hs.Count); + // Disconnect removed children from UIA so the client-side releases its + // COM references to the ElementProxy CCWs. Without this the CCW ref count + // never drops to zero, which prevents the managed peer (and its entire + // visual sub-tree) from being garbage collected. + // This must happen regardless of StructureChanged event registration. + if (removedCount > 0) + { + foreach (AutomationPeer removedChild in hs) + { + DisconnectPeerFromUia(removedChild); + } + } + + // Callers have only checked if automation clients are present so filter for any interest in this particular event. + if (!EventMap.HasRegisteredEvent(AutomationEvents.StructureChanged)) + return; + if(removedCount + addedCount > invalidateLimit) //bilk invalidation { StructureChangeType flags;