dotnet / dotnet/winforms

UiaProvider implementation forces virtual ListView to retrieve ALL ListViewItems at WM_DESTROY

Open
#13,922 6 comments 1 reaction 2 assignees Claimed by @KlausLoeffelmann View on GitHub
area-controls-ListView tenet-accessibility tenet-accessibility-performance
Dominant language
C#
Stars
4.9k
Forks
1.1k
Avg merge
20h 23m
Merged PRs (30d)
103

Description

### .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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.