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..04b08405c82 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; @@ -1812,6 +1813,39 @@ 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) + { + if (peer == null) + return; + + // Disconnect the peer's own ElementProxy CCW from UIA. + WeakReference proxyWeakRef = peer._elementProxyWeakReference; + if (proxyWeakRef?.Target is ElementProxy proxy) + { + UiaDisconnectProvider(proxy); + } + + peer._elementProxyWeakReference = null; + } + + [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 +1926,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 +1967,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;