winforms icon indicating copy to clipboard operation
winforms copied to clipboard

UiaProvider implementation forces virtual ListView to retrieve ALL ListViewItems at WM_DESTROY

Open gerhard17 opened this issue 3 months ago • 6 comments

.NET version

9.0

Did it work in .NET Framework?

Not tested/verified

Did it work in any of the earlier releases of .NET Core or .NET 5+?

I saw in the source code a dependency to OsVersion.IsWindows8OrGreater()

Issue description

I am using a ListView with VirtualMode = true to display a list a few thousands items. When the ListView is destroyed, the OnRetrieveVirtualItem() is called for each index (even never retrieved ones). This kills somehow the performance of the control.

Analyses as far I saw it in the debugger:

ListView overrides the internal bool SupportsUiaProviders => true; First problem: I cannot change that because it's internal.

WM_DESTROY invokes on the Control class

private void WmDestroy(ref Message m)
{
	if (!RecreatingHandle && !Disposing && !IsDisposed && GetState(States.TrackingMouseEvent))
	{
		OnMouseLeave(EventArgs.Empty);
		UnhookMouseEvent();
	}
	if (SupportsUiaProviders)  // <--- THIS RETURNS TRUE
	{
		ReleaseUiaProvider(HWNDInternal);  // <--- THIS GETS CALLED
	}
	OnHandleDestroyed(EventArgs.Empty);
	if (!Disposing)
	{
		if (!RecreatingHandle)
		{
			SetState(States.Created, value: false);
		}
	}
	else
	{
		SetState(States.Visible, value: false);
	}
	DefWndProc(ref m);
}

This invokes on the ListView class

internal override bool SupportsUiaProviders => true;

internal override void ReleaseUiaProvider(HWND handle)
{
	if (!OsVersion.IsWindows8OrGreater())
	{
		return;
	}
	for (int i = 0; i < Items.Count; i++)  // <--- THIS ITERATES OVER ALL INDICES IN THE VIRTUAL LIST VIEW
	{
		Items.GetItemByIndex(i)?.ReleaseUiaProvider(); // <--- THIS INVOKES OnRetrieveVirtualItem(e)
	}
	if (_defaultGroup != null)
	{
		DefaultGroup.ReleaseUiaProvider();
	}
	foreach (ListViewGroup group in Groups)
	{
		group.ReleaseUiaProvider();
	}
	foreach (ColumnHeader column in Columns)
	{
		column.ReleaseUiaProvider();
	}
	base.ReleaseUiaProvider(handle);
}

After this super expensive retrieval, ListViewItem then does nothing

internal void ReleaseUiaProvider()
{
	if (!IsAccessibilityObjectCreated)  // <--- RETURNS FALSE
	{
		return;
	}
	if (OsVersion.IsWindows8OrGreater())
	{
		if (_accessibilityObject is ListViewItemBaseAccessibleObject listViewItemBaseAccessibleObject)
		{
			listViewItemBaseAccessibleObject.ReleaseChildUiaProviders();
		}
		PInvoke.UiaDisconnectProvider(_accessibilityObject, skipOSCheck: true);
	}
	_accessibilityObject = null;
}

Possible Resolutions

  • Make SupportsUiaProviders protected overrideable.
  • Move the test if (!IsAccessibilityObjectCreated) higher up in the call hierachy.
  • Invoke the ReleaseUiaProvider() only for really created ListViewItems.

Disclaimer I'm not profient with UI Automation, so I do not know, what a proper implementation should excatly look like. In my case I do not use this feature explicitly.

Steps to reproduce

An example is difficult to post. Please see the analysis above.

gerhard17 avatar Oct 01 '25 17:10 gerhard17

@LeafShi1 can you investigate and let us know what you find?

merriemcgaw avatar Oct 06 '25 21:10 merriemcgaw

Example project: TestProject.zip

The method ReleaseUiaProvider of the ListView.cs was added in commit https://github.com/dotnet/winforms/commit/944b45935bab343df71442ed0ccc6bd351a90c2f to fix ListView memory leak issue.

Root Cause: ReleaseUiaProvider() does not distinguish between VirtualMode and normal mode, nor does it have a mechanism to determine which items actually created the UIA Provider, resulting in forced creation of all virtual items when destroyed.

LeafShi1 avatar Oct 11 '25 08:10 LeafShi1

The problem is, that when we have a REAL big virtualized list, and we track the correlating UIAProviders of the items by holding their parent ListViewItems in an additional list with a strong reference, those items can never get collected under memory pressure. So, unless I am missing something, FWIW the current PR is not the correct approach.

But then also the question for me is here: Why has this become a problem, when it has not been one 5, 10, 15 or 20 years ago?

What was the approach for virtual Lists before .NET 8 or still is in .NET Framework? Do we have a performance issue there, also? Can we test this first?

KlausLoeffelmann avatar Oct 15 '25 20:10 KlausLoeffelmann

But then also the question for me is here: Why has this become a problem, when it has not been one 5, 10, 15 or 20 years ago?

What was the approach for virtual Lists before .NET 8 or still is in .NET Framework? Do we have a performance issue there, also? Can we test this first?

This issue appears in method ReleaseUiaProvider(), which was added in .net7.0 in commit https://github.com/dotnet/winforms/commit/944b45935bab343df71442ed0ccc6bd351a90c2f,

However, in another commit https://github.com/dotnet/winforms/commit/07e5729a04f6ca2bb39adc534bf0a6c471791546 in .net8.0, the condition !IsAccessibilityObjectCreated was removed, and since then, this problem has been exposed.

Image

If the ListView contains 10,000 items, the following call stack will be executed 100,00 times during the control's destruction

ScratchProject.dll!ScratchProject.Form1.ListView1_RetrieveVirtualItem(object sender, System.Windows.Forms.RetrieveVirtualItemEventArgs e) Line 18	C#
System.Windows.Forms.dll!System.Windows.Forms.ListView.OnRetrieveVirtualItem(System.Windows.Forms.RetrieveVirtualItemEventArgs e) Line 4863	C#
System.Windows.Forms.dll!System.Windows.Forms.ListView.ListViewNativeItemCollection.GetItemByIndexInternal(int index, bool throwInVirtualMode) Line 70	C#
System.Windows.Forms.dll!System.Windows.Forms.ListView.ListViewNativeItemCollection.GetItemByIndex(int index) Line 60	C#
System.Windows.Forms.dll!System.Windows.Forms.ListView.ListViewItemCollection.GetItemByIndex(int index) Line 81	C#
System.Windows.Forms.dll!System.Windows.Forms.ListView.ReleaseUiaProvider(Windows.Win32.Foundation.HWND handle) Line 5109	C#
System.Windows.Forms.dll!System.Windows.Forms.Control.WmDestroy(ref System.Windows.Forms.Message m) Line 11441	C#

LeafShi1 avatar Oct 16 '25 02:10 LeafShi1

The problem is, that when we have a REAL big virtualized list, and we track the correlating UIAProviders of the items by holding their parent ListViewItems in an additional list with a strong reference, those items can never get collected under memory pressure. So, unless I am missing something, FWIW the current PR is not the correct approach.

You are right, caching the ListViewItem instance of the AccessibilityObject that has been created will inevitably lead to "unvirtualization", even if it is saved in an additional list of weak references, it will also lead to "unvirtualization", it seems that this method does not work.

LeafShi1 avatar Oct 16 '25 10:10 LeafShi1

@LeafShi1: I will try to find some time this week, to look at the ListView implementation itself. I'll ping you, when I get to it.

@merriemcgaw FYI.

KlausLoeffelmann avatar Oct 27 '25 16:10 KlausLoeffelmann