Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
//
//

using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Automation;
using System.Windows.Automation.Provider;
Expand Down Expand Up @@ -534,6 +535,31 @@ internal static ReferenceType AutomationInteropReferenceType

#endregion Private Methods

//------------------------------------------------------
//
// Internal Methods
//
//------------------------------------------------------

#region Internal Methods

/// <summary>
/// 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.
/// </summary>
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
Expand All @@ -542,7 +568,7 @@ internal static ReferenceType AutomationInteropReferenceType

#region Private Fields

private readonly object _peer;
private object _peer;

#endregion Private Fields
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1483,6 +1483,7 @@ private void EnsureChildren()
// UpdateSubtree is not called on it yet.
if (!_childrenValid || _ancestorsInvalid)
{
List<AutomationPeer> oldChildren = _children;
_children = GetChildrenCore();
if (_children != null)
{
Expand All @@ -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<AutomationPeer> 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<AutomationPeer>(_children);
}
}

if (newChildrenSet == null || !newChildrenSet.Contains(old))
DisconnectPeerFromUia(old);
}
}
}
}

Expand Down Expand Up @@ -1812,6 +1843,43 @@ private IRawElementProviderSimple ProviderFromPeerNoDelegation(AutomationPeer pe
return ElementProxy.StaticWrap(peer, referencePeer);
}

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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;
};

///<Summary>
/// 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
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand All @@ -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
Expand Down