Summary
When building a custom WinForms control with child accessible objects (e.g. a grid where each cell is a CellAccessibleObject : AccessibleObject), overriding GetFocused() on a ControlAccessibleObject subclass correctly exposes the focused child to MSAA clients. However, UIA clients, including NVDA, never see it.
Root cause
IRawElementProviderFragmentRoot.GetFocus() is implemented in AccessibleObject by calling an internal virtual method that returns null by default. ControlAccessibleObject does not override it, so UIA always reports the control itself as focused, regardless of what GetFocused() returns. DataGridView works correctly because it overrides GetFocus() from inside the WinForms assembly:
internal override IRawElementProviderFragment.Interface? GetFocus() => GetFocused();
External code has no equivalent escape hatch. GetFocused() is public and functional, but the UIA path never reaches it. To repro:
- Create a custom control with a ControlAccessibleObject subclass
- Override GetFocused() to return a child AccessibleObject
- Focus the control with NVDA running
- Press NVDA+Tab to read the focused element
Actual result: NVDA reports the control, not the child cell.
Expected result: NVDA reports the focused child (the cell accessible object returned by GetFocused())
Proposed fix
Either make GetFocus() protected virtual so external subclasses can override it, or have ControlAccessibleObject implement it as return GetFocused() as IRawElementProviderFragment.Interface; by default, matching what DataGridView already does internally.
Summary
When building a custom WinForms control with child accessible objects (e.g. a grid where each cell is a
CellAccessibleObject : AccessibleObject), overridingGetFocused()on aControlAccessibleObjectsubclass correctly exposes the focused child to MSAA clients. However, UIA clients, including NVDA, never see it.Root cause
IRawElementProviderFragmentRoot.GetFocus()is implemented inAccessibleObjectby calling aninternal virtualmethod that returnsnullby default.ControlAccessibleObjectdoes not override it, so UIA always reports the control itself as focused, regardless of whatGetFocused()returns.DataGridViewworks correctly because it overridesGetFocus()from inside the WinForms assembly:External code has no equivalent escape hatch. GetFocused() is public and functional, but the UIA path never reaches it. To repro:
Actual result: NVDA reports the control, not the child cell.
Expected result: NVDA reports the focused child (the cell accessible object returned by GetFocused())
Proposed fix
Either make GetFocus() protected virtual so external subclasses can override it, or have ControlAccessibleObject implement it as return GetFocused() as IRawElementProviderFragment.Interface; by default, matching what DataGridView already does internally.