diff --git a/src/System.Windows.Forms/System/Windows/Forms/Control.cs b/src/System.Windows.Forms/System/Windows/Forms/Control.cs index cb67f146d3e..e500ea5e2d1 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Control.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Control.cs @@ -4438,7 +4438,7 @@ protected internal void AccessibilityNotifyClients(AccessibleEvents accEvent, in [EditorBrowsable(EditorBrowsableState.Advanced)] protected void AccessibilityNotifyClients(AccessibleEvents accEvent, int objectID, int childID) { - if (IsHandleCreated && AppContextSwitches.NoClientNotifications) + if (IsHandleCreated && !AppContextSwitches.NoClientNotifications) { PInvoke.NotifyWinEvent((uint)accEvent, this, objectID, childID + 1); } diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/ControlTests.AccessibilityNotifyClients.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ControlTests.AccessibilityNotifyClients.cs new file mode 100644 index 00000000000..c21fcdd9239 --- /dev/null +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ControlTests.AccessibilityNotifyClients.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Windows.Win32.UI.Accessibility; + +namespace System.Windows.Forms.Tests; + +public partial class ControlTests +{ + [WinFormsFact] + public void Control_AccessibilityNotifyClients_WithHandle_RaisesWinEvent() + { + using NoClientNotificationsScope scope = new(enable: false); + using SubControl control = new(); + Assert.NotEqual(IntPtr.Zero, control.Handle); + + using WinEventListener listener = new(AccessibleEvents.Focus); + control.AccessibilityNotifyClients(AccessibleEvents.Focus, 2); + + WinEventListener.WinEvent winEvent = Assert.Single(listener.EventsOf(control)); + Assert.Equal((uint)AccessibleEvents.Focus, winEvent.Event); + Assert.Equal((int)OBJECT_IDENTIFIER.OBJID_CLIENT, winEvent.ObjectId); + + // MSAA child ids are 1-based. + Assert.Equal(3, winEvent.ChildId); + } + + [WinFormsFact] + public void Control_AccessibilityNotifyClients_ObjectId_WithHandle_RaisesWinEvent() + { + using NoClientNotificationsScope scope = new(enable: false); + using SubControl control = new(); + Assert.NotEqual(IntPtr.Zero, control.Handle); + + using WinEventListener listener = new(AccessibleEvents.SystemMenuStart); + control.AccessibilityNotifyClients(AccessibleEvents.SystemMenuStart, (int)OBJECT_IDENTIFIER.OBJID_MENU, -1); + + WinEventListener.WinEvent winEvent = Assert.Single(listener.EventsOf(control)); + Assert.Equal((int)OBJECT_IDENTIFIER.OBJID_MENU, winEvent.ObjectId); + Assert.Equal(0, winEvent.ChildId); + } + + [WinFormsFact] + public void Control_AccessibilityNotifyClients_NoClientNotifications_DoesNotRaiseWinEvent() + { + using NoClientNotificationsScope scope = new(enable: true); + using SubControl control = new(); + Assert.NotEqual(IntPtr.Zero, control.Handle); + + using WinEventListener listener = new(AccessibleEvents.Focus); + control.AccessibilityNotifyClients(AccessibleEvents.Focus, 2); + + Assert.Empty(listener.EventsOf(control)); + } + + /// + /// Listens, on the current thread, to the WinEvents of one kind raised by this process. + /// The hook is in-context, so the callback runs synchronously inside NotifyWinEvent. + /// + private sealed unsafe class WinEventListener : IDisposable + { + public readonly record struct WinEvent(uint Event, HWND Hwnd, int ObjectId, int ChildId); + + // The hook is installed for the current thread only, and WinForms tests do not run in parallel. + private static List? s_events; + + private readonly HWINEVENTHOOK _hook; + + public WinEventListener(AccessibleEvents accEvent) + { + s_events = []; + _hook = PInvoke.SetWinEventHook( + (uint)accEvent, + (uint)accEvent, + PInvoke.GetModuleHandle((PCWSTR)null), + &WinEventProc, + (uint)Environment.ProcessId, + PInvokeCore.GetCurrentThreadId(), + PInvoke.WINEVENT_INCONTEXT); + + Assert.False(_hook.IsNull); + } + + public IEnumerable EventsOf(Control control) + { + HWND hwnd = control.HWND; + return s_events!.Where(e => e.Hwnd == hwnd); + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])] + private static void WinEventProc( + HWINEVENTHOOK hook, + uint @event, + HWND hwnd, + int idObject, + int idChild, + uint idEventThread, + uint dwmsEventTime) => s_events?.Add(new(@event, hwnd, idObject, idChild)); + + public void Dispose() + { + PInvoke.UnhookWinEvent(_hook); + s_events = null; + } + } +}