| File: System\Windows\Forms\Accessibility\LabelEditNativeWindow.cs | Web Access |
| Project: ..\..\..\src\winforms\src\System.Windows.Forms\System.Windows.Forms.csproj (System.Windows.Forms) |
// 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.InteropServices; using Windows.Win32.UI.Accessibility; namespace System.Windows.Forms; internal class LabelEditNativeWindow : NativeWindow { private const uint TextSelectionChanged = 0x8014; private readonly WeakReference<Control> _owningControl; private WINEVENTPROC? _winEventProcCallback; private HWINEVENTHOOK _valueChangeHook; private HWINEVENTHOOK _textSelectionChangedHook; private bool _winEventHooksInstalled; private bool _isReleasing; private delegate void WINEVENTPROC( HWINEVENTHOOK hWinEventHook, uint @event, HWND hwnd, int idObject, int idChild, uint idEventThread, uint dwmsEventTime); public LabelEditNativeWindow(Control owningControl) { ArgumentNullException.ThrowIfNull(owningControl); _owningControl = new(owningControl); } public virtual AccessibleObject? AccessibilityObject { get; set; } private unsafe void InstallWinEventHooks() { if (!PInvoke.UiaClientsAreListening()) { return; } _winEventProcCallback = WinEventProcCallback; var functionPointer = (delegate* unmanaged[Stdcall]<HWINEVENTHOOK, uint, HWND, int, int, uint, uint, void>) (void*)Marshal.GetFunctionPointerForDelegate(_winEventProcCallback); _valueChangeHook = PInvoke.SetWinEventHook( (uint)AccessibleEvents.ValueChange, (uint)AccessibleEvents.ValueChange, PInvoke.GetModuleHandle((PCWSTR)null), functionPointer, (uint)Environment.ProcessId, PInvokeCore.GetCurrentThreadId(), PInvoke.WINEVENT_INCONTEXT); _textSelectionChangedHook = PInvoke.SetWinEventHook( TextSelectionChanged, TextSelectionChanged, PInvoke.GetModuleHandle((PCWSTR)null), functionPointer, (uint)Environment.ProcessId, PInvokeCore.GetCurrentThreadId(), PInvoke.WINEVENT_INCONTEXT); _winEventHooksInstalled = true; } private bool IsAccessibilityObjectCreated => AccessibilityObject is not null; public bool IsHandleCreated => Handle != HWND.Null; protected override void OnHandleChange() { base.OnHandleChange(); // Prevent reentrant hook installation during ReleaseHandle. // Without this guard, UnhookWinEvent can trigger OnHandleChange (via NativeWindow.Callback), // causing hooks to be reinstalled on a window that is being destroyed. if (_isReleasing) { return; } if (!IsHandleCreated) { // The native window handle was destroyed (e.g., Windows sent WM_NCDESTROY and NativeWindow // cleared the handle without going through our ReleaseHandle override). Unhook the WinEvent // hooks immediately so the WINEVENT_INCONTEXT hook cannot fire after _winEventProcCallback is // garbage collected. Without this, the LabelEditNativeWindow is removed from the NativeWindow // handle table and becomes eligible for GC while the hooks remain registered. The next // CallWindowProc on any window on this thread (e.g. GridViewTextBox.DefWndProc) would then // invoke the GC'd delegate and trigger a FailFast. UnhookWinEventHooks(); return; } // Install winevent hooks *only* when the parent Control 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. if (_owningControl.TryGetTarget(out Control? target) && !target.IsAccessibilityObjectCreated) { return; } InstallWinEventHooks(); } public override unsafe void ReleaseHandle() { _isReleasing = true; try { UnhookWinEventHooks(); if (IsHandleCreated) { // When a window that previously returned providers has been destroyed, // you should notify UI Automation by calling the UiaReturnRawElementProvider // as follows: UiaReturnRawElementProvider(hwnd, 0, 0, NULL). This call tells // UI Automation that it can safely remove all map entries that refer to the specified window. PInvoke.UiaReturnRawElementProvider(HWND, wParam: 0, lParam: 0, (IRawElementProviderSimple*)null); } PInvoke.UiaDisconnectProvider(AccessibilityObject); AccessibilityObject = null; base.ReleaseHandle(); } finally { _isReleasing = false; } } private void UnhookWinEventHooks() { if (_winEventHooksInstalled) { PInvoke.UnhookWinEvent(_valueChangeHook); PInvoke.UnhookWinEvent(_textSelectionChangedHook); _winEventHooksInstalled = false; // Allow the delegate to be garbage collected now that no hook holds a reference // to its underlying unmanaged function pointer. _winEventProcCallback = null; } } private void WinEventProcCallback(HWINEVENTHOOK hWinEventHook, uint eventId, HWND hwnd, int idObject, int idChild, uint idEventThread, uint dwmsEventTime) { // Late-arriving callbacks should be ignored if we are releasing or hooks are already uninstalled. if (_isReleasing || !_winEventHooksInstalled) { return; } if (hwnd != Handle || idObject != (int)OBJECT_IDENTIFIER.OBJID_CLIENT || !IsAccessibilityObjectCreated) { return; } switch (eventId) { case (uint)AccessibleEvents.ValueChange: AccessibilityObject?.RaiseAutomationEvent(UIA_EVENT_ID.UIA_Text_TextChangedEventId); break; case TextSelectionChanged: AccessibilityObject?.RaiseAutomationEvent(UIA_EVENT_ID.UIA_Text_TextSelectionChangedEventId); break; } } private void WmGetObject(ref Message m) { AccessibleObject? EnsureWinEventHooksInstalledAndGetAccessibilityObject() { AccessibleObject? accessibilityObject = AccessibilityObject; // Accessibility object was likely requested by an assistive tech (which sent WM_GETOBJECT message). // We may need to install winevent hooks to produce the automation events related to the text pattern. // However, if we are in the process of releasing the handle, skip hook installation to avoid // creating new hooks on a window that is being destroyed. if (!_winEventHooksInstalled && !_isReleasing) { InstallWinEventHooks(); } return accessibilityObject; } if (m.LParamInternal == PInvoke.UiaRootObjectId) { // If the requested object identifier is UiaRootObjectId, // we should return an UI Automation provider using the UiaReturnRawElementProvider function. m.ResultInternal = PInvoke.UiaReturnRawElementProvider( this, m.WParamInternal, m.LParamInternal, EnsureWinEventHooksInstalledAndGetAccessibilityObject()); return; } if ((int)m.LParamInternal != (int)OBJECT_IDENTIFIER.OBJID_CLIENT) { DefWndProc(ref m); return; } try { m.ResultInternal = EnsureWinEventHooksInstalledAndGetAccessibilityObject()!.GetLRESULT(m.WParamInternal); } catch (Exception ex) { throw new InvalidOperationException(SR.RichControlLresult, ex); } } protected override void WndProc(ref Message m) { switch (m.MsgInternal) { case PInvokeCore.WM_GETOBJECT: WmGetObject(ref m); return; default: base.WndProc(ref m); return; } } }