-
Notifications
You must be signed in to change notification settings - Fork 1k
Improving accessible object for label edit in ListView #6309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improving accessible object for label edit in ListView #6309
Conversation
7dc0326
to
16670aa
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. I added cleanup-y comments only.
I did not review text patterns and pinvoke definitions yet.
@@ -6481,6 +6490,13 @@ private unsafe void WmReflectNotify(ref Message m) | |||
|
|||
case (int)LVN.ENDLABELEDITW: | |||
{ | |||
Debug.Assert(_childLabelEdit is not null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider a symmetric assert in BEGINLABELEDIT
case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though I would throw here if _childLabelEdit
is null.
src/System.Windows.Forms/src/System/Windows/Forms/ListView.ListViewAccessibleObject.cs
Outdated
Show resolved
Hide resolved
src/System.Windows.Forms/src/System/Windows/Forms/ListView.ListViewAccessibleObject.cs
Outdated
Show resolved
Hide resolved
src/System.Windows.Forms/src/System/Windows/Forms/ListView.ListViewAccessibleObject.cs
Outdated
Show resolved
Hide resolved
src/System.Windows.Forms/src/System/Windows/Forms/ListViewChildLabelEditNativeWindow.cs
Outdated
Show resolved
Hide resolved
src/System.Windows.Forms/src/System/Windows/Forms/ListViewChildLabelEditNativeWindow.cs
Outdated
Show resolved
Hide resolved
src/System.Windows.Forms/src/System/Windows/Forms/ListViewChildLabelEditNativeWindow.cs
Outdated
Show resolved
Hide resolved
|
||
public ListViewChildLabelEditNativeWindow(ListView owner) | ||
{ | ||
_owner = owner; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
THrow is argument is null.
src/System.Windows.Forms/src/System/Windows/Forms/ListViewChildLabelEditNativeWindow.cs
Outdated
Show resolved
Hide resolved
src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.SetWinEventHook.cs
Show resolved
Hide resolved
src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.SetWinEventHook.cs
Outdated
Show resolved
Hide resolved
src/System.Windows.Forms/src/System/Windows/Forms/ListViewChildLabelEditNativeWindow.cs
Outdated
Show resolved
Hide resolved
16670aa
to
7a046e6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not finish the review
src/System.Windows.Forms/src/System/Windows/Forms/ListViewChildLabelEditUiaTextProvider.cs
Outdated
Show resolved
Hide resolved
src/System.Windows.Forms/src/System/Windows/Forms/ListViewChildLabelEditUiaTextProvider.cs
Outdated
Show resolved
Hide resolved
int index = (int)SendMessageW(_owningChildEdit, (WM)EM.CHARFROMPOS, 0, PARAM.FromPoint(pt)); | ||
index = PARAM.LOWORD(index); | ||
|
||
if (index < 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the case when the point is outside the control, I don't see why we are returning the first char.
src/System.Windows.Forms/src/System/Windows/Forms/ListViewChildLabelEditUiaTextProvider.cs
Outdated
Show resolved
Hide resolved
internal static partial class User32 | ||
{ | ||
[DllImport(Libraries.User32, ExactSpelling = true, SetLastError = true)] | ||
public static extern nint SetWinEventHook( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this method used outside this file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It indeed doesn't so I made it private.
src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.SetWinEventHook.cs
Outdated
Show resolved
Hide resolved
src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.WINEVENT.cs
Outdated
Show resolved
Hide resolved
src/System.Windows.Forms/src/System/Windows/Forms/ListViewChildLabelEditUiaTextProvider.cs
Outdated
Show resolved
Hide resolved
src/System.Windows.Forms/src/System/Windows/Forms/ListViewChildLabelEditUiaTextProvider.cs
Outdated
Show resolved
Hide resolved
|
||
private readonly ListView _owningListView; | ||
|
||
public ListViewChildLabelEditUiaTextProvider(ListView owner, IHandle childEdit, AccessibleObject childEditAccessibilityObject) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
childEditAccessibilityObject
must either be NRT annotated or checked for null below.
NRT annotations as such do not enforce nullability, they only act as compiler hints. This means it is possible to pass a null
here and have the app crash at runtime, hence we still need to enforce the contract.
Whilst this is a non public API (and hence we control the input parameters) we had a number of NRE related issues in the accessibility implementations, and @vladimir-krestov and @SergeySmirnov-Akvelon spent a significant amount of time fixing our implementations. So in the interest of consistency and to reduce the maintenance overhead of these already complex API please continue null checking .ctor input args.
src/System.Windows.Forms/src/System/Windows/Forms/ListViewChildLabelEditUiaTextProvider.cs
Outdated
Show resolved
Hide resolved
public ListViewLabelEditAccessibleObject(ListView owner, IHandle handle) | ||
{ | ||
_owner = owner; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
owner
must either be NRT annotated or checked for null. Please make sure to provide test coverage for all such scenarios.
public ListViewLabelEditNativeWindow(ListView owner) | ||
{ | ||
_owner = owner; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto here.
src/System.Windows.Forms/src/System/Windows/Forms/ListViewLabelEditNativeWindow.cs
Outdated
Show resolved
Hide resolved
70da02f
to
58ab17c
Compare
fc5fe56
to
434d304
Compare
src/System.Windows.Forms/src/System/Windows/Forms/ListViewLabelEditNativeWindow.cs
Outdated
Show resolved
Hide resolved
private ListView _owner; | ||
|
||
private AccessibleObject? _accessibilityObject; | ||
|
||
private User32.WINEVENTPROC? _winEventProc; | ||
|
||
private nint _valueChangeHook; | ||
private nint _textSelectionChangedHook; | ||
|
||
private bool _winEventHooksInstalled; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please keep these together
private IntPtr _handle; | ||
private int[]? _runtimeId; | ||
|
||
public ListViewLabelEditAccessibleObject(ListView owner, IHandle handle) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I vaguely remember a discussion with @vladimir-krestov and @SergeySmirnov-Akvelon about prefixing the first parameter on non public types with "owning", e.g. owningListView
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we decided to make every owning field in one style - _owning[Control]
, eg. _owningListView
src/System.Windows.Forms/src/System/Windows/Forms/ListViewLabelEditAccessibleObject.cs
Show resolved
Hide resolved
// Install winevent hooks only in the case when the parent listview already has an accessible object created. | ||
// If we won't install hooks at the label edit startup then Narrator won't announce the text pattern for it. | ||
// If we will just check for UiaClientsAreListening then we will have hooks installed even if Narrator isn't currently run. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did I get correctly the intent?
// Install winevent hooks only in the case when the parent listview already has an accessible object created. | |
// If we won't install hooks at the label edit startup then Narrator won't announce the text pattern for it. | |
// If we will just check for UiaClientsAreListening then we will have hooks installed even if Narrator isn't currently run. | |
// Install winevent hooks *only* when the parent listview has an existing accessible object. | |
// If we don't install hooks at the label edit startup then assistive tech (e.g. Narrator) won't announce the text pattern for it. | |
// By invoking UiaClientsAreListening we will have hooks installed even if the assistive tech isn't currently run. |
Also "...then assistive tech (e.g. Narrator) won't announce the text pattern for it." What is "it" in this context?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for cleaning up the texts. I assume "it" means the label edit control.
|
||
private AccessibleObject RequestAccessibilityObject() | ||
{ | ||
AccessibleObject result = AccessibilityObject; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is it called result
?
_winEventHooksInstalled = true; | ||
} | ||
|
||
private AccessibleObject RequestAccessibilityObject() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am finding the method name at odds with its implementation. The name suggests it sends or initiate a request somewhere to get an accessibility object. In reality it just deferences AccessibilityObject
property and install winhooks. The last bit is a side effect, which has an element of surprise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I renamed it and also made it a local function inside WmGetObject
because it's used only there, and the comment makes more sense within WmGetObject
.
src/System.Windows.Forms/src/System/Windows/Forms/ListViewLabelEditNativeWindow.cs
Outdated
Show resolved
Hide resolved
src/System.Windows.Forms/src/System/Windows/Forms/ListViewLabelEditNativeWindow.cs
Outdated
Show resolved
Hide resolved
src/System.Windows.Forms/src/System/Windows/Forms/ListViewLabelEditNativeWindow.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good work!
src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.SetWinEventHook.cs
Outdated
Show resolved
Hide resolved
private IntPtr _handle; | ||
private int[]? _runtimeId; | ||
|
||
public ListViewLabelEditAccessibleObject(ListView owner, IHandle handle) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we decided to make every owning field in one style - _owning[Control]
, eg. _owningListView
src/System.Windows.Forms/src/System/Windows/Forms/ListViewLabelEditAccessibleObject.cs
Outdated
Show resolved
Hide resolved
src/System.Windows.Forms/src/System/Windows/Forms/ListViewLabelEditAccessibleObject.cs
Show resolved
Hide resolved
src/System.Windows.Forms/src/System/Windows/Forms/ListViewLabelEditAccessibleObject.cs
Show resolved
Hide resolved
_winEventHooksInstalled = false; | ||
} | ||
|
||
if (_accessibilityObject is not null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would create IsAccessibilityObjectCreated created for readability, like we did for ControlAO
|
||
private void WinEventProc(nint hWinEventHook, uint eventId, nint hwnd, int idObject, int idChild, uint idEventThread, uint dwmsEventTime) | ||
{ | ||
if (hwnd != Handle || idObject != User32.OBJID.CLIENT) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to check if AO is created before?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added check for IsAccessibilityObjectCreated
src/System.Windows.Forms/src/System/Windows/Forms/ListViewLabelEditUiaTextProvider.cs
Show resolved
Hide resolved
src/System.Windows.Forms/src/System/Windows/Forms/ListViewLabelEditUiaTextProvider.cs
Show resolved
Hide resolved
...Windows.Forms/tests/UnitTests/System/Windows/Forms/ListViewLabelEditAccessibleObjectTests.cs
Show resolved
Hide resolved
@dmitrii-drobotov - the placement of edit box seems incorrect to me in the file explorer (and this PR). DGV placement looks more logical. However priority-wise this is not as high as other issues, since File Explorer had not fixed it in win11. Please open a new issue with this info - #6309 (comment) and we'll triage it. |
c9a3481
to
166e165
Compare
166e165
to
b691179
Compare
b691179
to
0001879
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please address comment about the uint in the enum type. When had this been tested by the CTIs, should it be re-tested or was the delta since then minor?
src/System.Windows.Forms/src/System/Windows/Forms/ListViewLabelEditNativeWindow.cs
Show resolved
Hide resolved
src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.WINEVENT.cs
Outdated
Show resolved
Hide resolved
This PR actually hasn't been tested by the CTI yet. I'll make a request once I address these code review points. |
Fixes dotnet#4810. Implements an accessible object for label edit control in ListView. This object now is the last child of the ListView (the same behavior as in the Windows Explorer). The object now also has a non-empty name according to its value (Windows Explorer behavior). The original object wasn't producing automation events related to the text, it was done by the UIAutomationCore proxy -- this was also implemented along with the text pattern support for the new accessible object, so the Narrator announces its text correctly.
* Update ListViewLabelEditAccessibleObject after TextProvider refactoring * Add new unit tests for constructor arguments
0001879
to
b9b4c7e
Compare
There were 2 issues in the CTI test report, but both of them are related to Inspect not updating its UIA tree automatically and would be resolved after making a refresh. It's also the same behavior as in other Winforms controls with edit like TreeView or DataGridView and in Windows file explorer. Therefore I can say that both issues are expected and don't need to be fixed. There are no other issues found, so this PR passes the testing. |
@dmitrii-drobotov this is causing the main to fail with
|
Fixes #4810. Implements an accessible object for label edit control in ListView. This object now is the last child of the ListView (the same behavior as in the Windows Explorer). The object now also has a non-empty name according to its value (Windows Explorer behavior). The original object wasn't producing automation events related to the text, it was done by the UIAutomationCore proxy -- this was also implemented along with the text pattern support for the new accessible object, so the Narrator announces its text correctly.
Proposed changes
Customer Impact
Regression?
Risk
Screenshots
Before
After
Test methodology
Test environment(s)
Microsoft Reviewers: Open in CodeFlow