Skip to content

Win32: ISelectionProvider.GetSelection() never reaches the UIA client (SafeArrayRef.CreateFromObjects) #22150

Description

@grokys

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions