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