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..6d61a825b15 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 @@ -8,6 +8,7 @@ // // +using System.Runtime.InteropServices; using System.Windows; using System.Windows.Automation; using System.Windows.Automation.Provider; @@ -534,6 +535,31 @@ internal static ReferenceType AutomationInteropReferenceType #endregion Private Methods + //------------------------------------------------------ + // + // Internal Methods + // + //------------------------------------------------------ + + #region Internal Methods + + /// + /// Disconnects this proxy from UI Automation and severs the strong reference + /// to the automation peer so the peer (and the items it transitively roots) + /// can be garbage collected. Must NOT be invoked from inside a UIA callback; + /// callers defer this to a dispatcher operation. + /// + internal void Disconnect() + { + UiaDisconnectProvider(this); + _peer = null; + } + + [DllImport(DllImport.UIAutomationCore, EntryPoint = "UiaDisconnectProvider", CharSet = CharSet.Unicode)] + private static extern int UiaDisconnectProvider(IRawElementProviderSimple provider); + + #endregion Internal Methods + //------------------------------------------------------ // // Private Fields @@ -542,7 +568,7 @@ internal static ReferenceType AutomationInteropReferenceType #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..8a981d89acb 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 @@ -1483,6 +1483,7 @@ private void EnsureChildren() // UpdateSubtree is not called on it yet. if (!_childrenValid || _ancestorsInvalid) { + List oldChildren = _children; _children = GetChildrenCore(); if (_children != null) { @@ -1495,6 +1496,36 @@ private void EnsureChildren() } } _childrenValid = true; + + // Disconnect peers that were removed from the children list and were previously exposed to a UIA client (have an ElementProxy). + // The _elementProxyWeakReference gate keeps this allocation-free on the common path where no UIA client has walked this subtree. + if (oldChildren != null) + { + HashSet newChildrenSet = null; + bool newChildrenSetBuilt = false; + + for (int i = 0; i < oldChildren.Count; i++) + { + AutomationPeer old = oldChildren[i]; + if (old._elementProxyWeakReference == null) + continue; + + // Build a HashSet of the new children on first lookup so subsequent + // membership checks are O(1). Avoids O(n*m) when this peer has many + // children (e.g. the top-level peer of a large item host). + if (!newChildrenSetBuilt) + { + newChildrenSetBuilt = true; + if (_children != null && _children.Count > 0) + { + newChildrenSet = new HashSet(_children); + } + } + + if (newChildrenSet == null || !newChildrenSet.Contains(old)) + DisconnectPeerFromUia(old); + } + } } } @@ -1812,6 +1843,43 @@ 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. + /// + /// + /// This method intentionally does NOT recursively disconnect children. + /// In virtualized controls, a removed item peer's cached _children may reference container peers (e.g. DataGridCellAutomationPeer) that have + /// been recycled and are now serving new items. Disconnecting those would break accessibility on currently visible elements. + /// Children are disconnected naturally when their own parent's UpdateChildrenInternal runs and detects them as removed. + /// + private static void DisconnectPeerFromUia(AutomationPeer peer) + { + // Disconnect the peer's own ElementProxy CCW from UIA. + WeakReference proxyWeakRef = peer._elementProxyWeakReference; + if (proxyWeakRef?.Target is ElementProxy proxy) + { + // ElementProxy.Disconnect calls UiaDisconnectProvider and severs the proxy→peer reference. + // UiaDisconnectProvider must NOT be called during a UIA callback (e.g. during Navigate/FindAll + // handling). UpdateChildrenInternal is invoked from within UIA callbacks, so defer the whole + // disconnect to a dispatcher operation that runs after the current callback completes. + peer.Dispatcher.BeginInvoke(DispatcherPriority.Background, _disconnectProxyCallback, proxy); + } + + peer._elementProxyWeakReference = null; + + // Break the peer→children chain. Without this, a disconnected ItemAutomationPeer still roots cached cell peers via _children, + // which in turn root DataGridCell/DataGridRow containers via _owner, preventing the entire visual sub-tree from being GC`d. + peer._children = null; + peer._childrenValid = false; + } + + private static readonly DispatcherOperationCallback _disconnectProxyCallback = static arg => + { + ((ElementProxy)arg).Disconnect(); + return null; + }; + /// /// 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,7 +2001,14 @@ internal void UpdateChildrenInternal(int invalidateLimit) //calls for "bulk" notification, use per-child notification, otherwise use "bulk" int removedCount = (hs == null ? 0 : hs.Count); - if(removedCount + addedCount > invalidateLimit) //bilk invalidation + // Removed peers are already disconnected from UIA by EnsureChildren() above; + // this block only raises StructureChanged events. + + // 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) //bulk invalidation { StructureChangeType flags; @@ -1967,7 +2038,6 @@ internal void UpdateChildrenInternal(int invalidateLimit) IRawElementProviderSimple provider = ProviderFromPeerNoDelegation(this); if (provider != null) { - //hs contains removed children by now foreach (AutomationPeer removedChild in hs) { int[] rid = removedChild.GetRuntimeId(); @@ -1980,7 +2050,6 @@ internal void UpdateChildrenInternal(int invalidateLimit) } if (addedCount > 0) { - //hs contains removed children by now foreach (AutomationPeer addedChild in addedChildren) { //for children added, provider is the child itself