Describe the bug
On Windows, ISelectionProvider.GetSelection() never reaches the UI Automation client. A client reading SelectionPattern.GetCurrentSelection() on any selecting control gets a zero-length array, even when the control has a selection and the selected item is realized, visible, and correctly reports SelectionItemPattern.IsSelected == true.
The Avalonia peers are fine. The loss is in SafeArrayRef.CreateFromObjects in src/Windows/Avalonia.Win32.Automation/Marshalling/SafeArrayRef.cs.
There are two defects there.
1. The SAFEARRAY is sized to the pooled buffer, not the input.
static SafeArrayRef CreateFromObjects(IReadOnlyList<object> objects, VarEnum varEnum)
{
var pointers = ArrayPool<IntPtr>.Shared.Rent(objects.Count);
...
return CreateFromSpan<IntPtr>(pointers, varEnum); // whole rented buffer
}
CreateFromStrings and CreateFromBools both slice with .AsSpan(0, count); this one does not. A one-element selection becomes a 16-element SAFEARRAY. The rented buffer is not cleared either, so stale pointers from an earlier rent can leak into the array.
2. Entries are only filled when a COM wrapper already exists.
ComWrappers.TryGetComInstance(objects[i], out var pointer) returns false when the object has no live wrapper yet, and the slot is then silently left as IntPtr.Zero rather than a wrapper being created. For selection this is the normal case: the AutomationNodes handed back by GetSelection have no wrapper at that point.
Net effect: UIA receives a 16-element SAFEARRAY of NULLs, discards them all, and the client reports an empty selection.
To Reproduce
A window with a ListBox that has two items and SelectedIndex = 0, plus a ComboBox with SelectedIndex = 0. Read SelectionPattern.GetCurrentSelection() on each from an out-of-process UIA client (Interop.UIAutomationClient, or Inspect.exe).
Both report a zero-length selection. Their items' SelectionItemPattern.IsSelected is correct, and reading the peers in-process shows the peers themselves are right:
presetComboBox: peer=ComboBoxAutomationPeer
ISelectionProvider = ComboBoxAutomationPeer
GetSelection() = 1
UnrealizedSelectionPeer 'Preset 0' node=AutomationNode hasComWrapper=False
listBox: peer=ListBoxAutomationPeer
ISelectionProvider = ListBoxAutomationPeer
GetSelection() = 1
ListItemAutomationPeer 'List 0' node=AutomationNode hasComWrapper=False
The marshaller can also be exercised on its own. Calling SafeArrayRef.TryCreate with an N-element object array and reading the result back:
input count=1 TryCreate=True varEnum=VT_UNKNOWN
SAFEARRAY length = 16
entries = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
input count=2 TryCreate=True varEnum=VT_UNKNOWN
SAFEARRAY length = 16
entries = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Expected behavior
SelectionPattern.GetCurrentSelection() returns one element per selected item, matching what the peer's GetSelection() returns.
Additional context
A ComboBox masks the symptom in practice because Avalonia also exposes ValuePattern with the selection text, so assistive technology usually still gets something. A ListBox has no such fallback, and neither does any embedder whose ComboBox peer does not implement ValuePattern.
VT_UNKNOWN looks like the only SAFEARRAY element type this path produces, so nothing else exercises it — GetRuntimeId and friends go through the integer path, which is correct.
Environment
- Avalonia 12.1.2
- Windows 11, .NET 10, x64
Describe the bug
On Windows,
ISelectionProvider.GetSelection()never reaches the UI Automation client. A client readingSelectionPattern.GetCurrentSelection()on any selecting control gets a zero-length array, even when the control has a selection and the selected item is realized, visible, and correctly reportsSelectionItemPattern.IsSelected == true.The Avalonia peers are fine. The loss is in
SafeArrayRef.CreateFromObjectsinsrc/Windows/Avalonia.Win32.Automation/Marshalling/SafeArrayRef.cs.There are two defects there.
1. The SAFEARRAY is sized to the pooled buffer, not the input.
CreateFromStringsandCreateFromBoolsboth slice with.AsSpan(0, count); this one does not. A one-element selection becomes a 16-element SAFEARRAY. The rented buffer is not cleared either, so stale pointers from an earlier rent can leak into the array.2. Entries are only filled when a COM wrapper already exists.
ComWrappers.TryGetComInstance(objects[i], out var pointer)returns false when the object has no live wrapper yet, and the slot is then silently left asIntPtr.Zerorather than a wrapper being created. For selection this is the normal case: theAutomationNodes handed back byGetSelectionhave no wrapper at that point.Net effect: UIA receives a 16-element SAFEARRAY of NULLs, discards them all, and the client reports an empty selection.
To Reproduce
A window with a
ListBoxthat has two items andSelectedIndex = 0, plus aComboBoxwithSelectedIndex = 0. ReadSelectionPattern.GetCurrentSelection()on each from an out-of-process UIA client (Interop.UIAutomationClient, or Inspect.exe).Both report a zero-length selection. Their items'
SelectionItemPattern.IsSelectedis correct, and reading the peers in-process shows the peers themselves are right:The marshaller can also be exercised on its own. Calling
SafeArrayRef.TryCreatewith an N-element object array and reading the result back:Expected behavior
SelectionPattern.GetCurrentSelection()returns one element per selected item, matching what the peer'sGetSelection()returns.Additional context
A
ComboBoxmasks the symptom in practice because Avalonia also exposesValuePatternwith the selection text, so assistive technology usually still gets something. AListBoxhas no such fallback, and neither does any embedder whose ComboBox peer does not implementValuePattern.VT_UNKNOWNlooks like the only SAFEARRAY element type this path produces, so nothing else exercises it —GetRuntimeIdand friends go through the integer path, which is correct.Environment