Skip to content

Expose accessible wrappers for system dialog UI (e.g. TaskDialog) — TaskDialogAccessibleObject / DialogAccessibleObject #14229

@memoarfaa

Description

@memoarfaa

Background and motivation

Many modern system dialogs (TaskDialog and newer Open/Save dialogs) present fairly rich UI composed by the OS (main instruction, content text, command links, radio groups, checkboxes, expanders, embedded hyperlinks, etc.). When a WinForms app shows these dialogs, assistive technologies and automation tests sometimes do not receive a complete, stable, or semantically accurate UIA/MSAA representation of dialog content. This makes it harder for app authors and accessibility consumers to:

Provide deterministic screen-reader experiences,
Automate dialog interactions reliably (UIA-based tests),
Diagnose and correct accessibility regressions.
This proposal introduces a managed-side accessible wrapper (DialogAccessibleObject) and a TaskDialog-specific specialization (TaskDialogAccessibleObject) to expose dialog semantics and patterns in a consistent, testable way for WinForms-hosted dialogs. The implementation will use only public APIs (UI Automation / MSAA / documented Win32 entry points), be defensive about platform differences, and expose minimal public surface initially.

API Proposal

namespace System.Windows.Forms
 {
+    // New: top-level dialog accessible helper
+    public static class DialogAccessibleObject
+    {
+        // Create an AccessibleObject for an HWND-backed dialog (factory)
+        public static AccessibleObject CreateForDialog(IntPtr dialogHwnd);
+
+        // Optional overload for creating from a UIA AutomationElement (managed wrapper)
+        public static AccessibleObject CreateForDialog(object automationElement);
+    }
+
+    // New: generic dialog accessible object
+    public class DialogAccessibleObject : AccessibleObject
+    {
+        public DialogAccessibleObject(IntPtr dialogHwnd);
+        public DialogAccessibleObject(object automationElement);
+
+        // Expose the HWND if available (nullable)
+        public IntPtr DialogHwnd { get; }
+
+        // Override to provide dialog-specific fragment navigation, patterns, etc.
+        public override string Name { get; }
+        public override AccessibleRole Role { get; }
+        // ... other overrides / helpers as required
+    }
+
+    // New: TaskDialog specialization
+    public sealed class TaskDialogAccessibleObject : DialogAccessibleObject
+    {
+        public TaskDialogAccessibleObject(TaskDialog owner);
+        public TaskDialog Owner { get; }
+    }
 }

API Usage

Example code showing intended consumption and testing.

1- TaskDialog instance exposes an AccessibleObject:

var dlg = new TaskDialog()
{
    MainInstruction = "Save changes?",
    Content = "Do you want to save changes before closing?",
    Buttons = { TaskDialogButtons.OK, TaskDialogButtons.Cancel }
};

// Synchronous show
dlg.Show();

AccessibleObject acc = dlg.AccessibleObject;
Console.WriteLine(acc.Name); // "Save changes?"

// Find OK button child and invoke via accessible API
AccessibleObject okButton = acc.GetChild(0);
okButton.DoDefaultAction();

2- Factory usage for HWND-based dialog (advanced / automation harness):

IntPtr dialogHwnd = /* discovered via UIA or hook (see below) */;
AccessibleObject acc = DialogAccessibleObject.CreateForDialog(dialogHwnd);

// Query properties or patterns:
var name = acc.Name;
var role = acc.Role;

Alternative Designs

  • Do nothing: continue to rely on OS-provided providers only and accept variability across Windows versions (insufficient for consistent app-level behavior).
  • Pure UIA client adapter (no AccessibleObject): use UIA AutomationElement directly from tests/tools. Downside: WinForms would not expose a managed AccessibleObject to code that expects AccessibleObject / legacy MSAA helpers.
  • Expose only an internal hook/extension point so third-party or app-level code can plug providers. This is lighter but shifts responsibility to consumers and fragments the solution.
  • Full native integration with platform internals: rejected because it would rely on undocumented Windows internals and is brittle.
    Preferred approach: provide a WinForms-side AccessibleObject mapping that (a) consumes public UIA/MSAA info where available, (b) provides managed fallbacks when OS info is insufficient, and (c) exposes a small, testable, documented public API.

How to obtain the dialog HWND / AutomationElement (implementation note)
Modern "Vista" dialogs (IFileDialog) do not invoke the legacy HookProc flow; we must discover the dialog HWND or its AutomationElement before attaching a DialogAccessibleObject. Two supported approaches:

  1. UIA discovery (recommended)
  • Start a short-lived background poller (2–5s) before calling IFileDialog::Show that searches AutomationElement.RootElement for a top-level Window in the current process whose Name == Title (set in OnBeforeVistaDialog) and ControlType == Window. Use AutomationElement.Current.NativeWindowHandle to obtain HWND if needed. This uses public UIA and provides direct managed automation data.
  • Example (conceptual):
// Background poller started before dialog.Show(...)
Task.Run(() =>
{
    var timeout = TimeSpan.FromSeconds(5);
    var sw = Stopwatch.StartNew();
    Condition cond = new AndCondition(
        new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Window),
        new PropertyCondition(AutomationElement.ProcessIdProperty, Process.GetCurrentProcess().Id),
        new PropertyCondition(AutomationElement.NameProperty, expectedTitle));

    while (sw.Elapsed < timeout)
    {
        var root = AutomationElement.RootElement;
        var wins = root.FindAll(TreeScope.Children, cond);
        for (int i = 0; i < wins.Count; i++)
        {
            var ae = wins[i];
            int hwnd = ae.Current.NativeWindowHandle;
            if (hwnd != 0)
            {
                // capture and attach wrapper
                _dialogHwnd = (IntPtr)hwnd;
                _dialogAutomationElement = ae;
                return;
            }
        }
        Thread.Sleep(50);
    }
});
  1. Thread-local CBT hook (alternative)
    Install a thread-local CBT hook (WH_CBT / HCBT_CREATEWND) on the creating thread immediately before Show. On HCBT_CREATEWND inspect title/class to capture the dialog HWND and unhook. Deterministic but requires careful P/Invoke hook management and unhooking.
    Integration guidance: start discovery after OnBeforeVistaDialog and before dialog.Show, store discovered HWND/AutomationElement into FileDialog fields (e.g., _dialogHwnd, _dialogAutomationElement) and attach DialogAccessibleObject in finally cleanup. Prefer UIA discovery as primary; provide CBT hook approach as alternate fallback when deterministic capture is required.
  • Integration sketch:
// PInvoke: SetWindowsHookEx, UnhookWindowsHookEx, GetCurrentThreadId, GetWindowText, GetClassName, etc.
// Hook constants: WH_CBT = 5, HCBT_CREATEWND = 3

private static IntPtr s_dialogHwnd = IntPtr.Zero;
private static IntPtr s_hookHandle = IntPtr.Zero;
private static CBTProc s_cbtDelegate; // keep alive

private delegate IntPtr CBTProc(int nCode, IntPtr wParam, IntPtr lParam);

private static IntPtr CbtProc(int nCode, IntPtr wParam, IntPtr lParam)
{
    if (nCode == /*HCBT_CREATEWND*/ 3)
    {
        IntPtr hwnd = wParam;
        // Optionally check window text or class name:
        string title = GetWindowText(hwnd);
        if (!string.IsNullOrEmpty(title) && title == expectedTitle)
        {
            Interlocked.Exchange(ref s_dialogHwnd, hwnd);
            // optionally unhook immediately
            if (s_hookHandle != IntPtr.Zero)
            {
                UnhookWindowsHookEx(s_hookHandle);
                s_hookHandle = IntPtr.Zero;
            }
        }
    }

    return CallNextHookEx(s_hookHandle, nCode, wParam, lParam);
}

private static void StartCbtHook(string expectedTitle)
{
    s_cbtDelegate = CbtProc;
    uint threadId = GetCurrentThreadId();
    s_hookHandle = SetWindowsHookEx(WH_CBT, s_cbtDelegate, IntPtr.Zero, threadId);
}

private static void StopCbtHook()
{
    if (s_hookHandle != IntPtr.Zero)
    {
        UnhookWindowsHookEx(s_hookHandle);
        s_hookHandle = IntPtr.Zero;
    }
}
// Before Show:
// Integration idea for FileDialog.Vista.cs (where TryRunDialogVista calls Show)

// Before dialog.Value->Show(hWndOwner):

StartCbtHook(expectedTitle: Title);
dialog.Value->Advise(events, out uint eventCookie);
try
{
    returnValue = dialog.Value->Show(hWndOwner) == HRESULT.S_OK;
    // dialog closed; s_dialogHwnd will be filled if found
    if (s_dialogHwnd != IntPtr.Zero)
    {
        _dialogHWnd = (HWND)s_dialogHwnd;
        // create or attach accessible wrapper here
    }
    return true;
}
finally
{
    dialog.Value->Unadvise(eventCookie);
    StopCbtHook();
    s_dialogHwnd = IntPtr.Zero;
}

Risks

  • Platform variability: dialog composition and automation provider behavior may vary across Windows versions, SKUs, or future OS updates. Mitigation: fallbacks, defensive coding, and keep public surface minimal initially.

  • Performance: polling or hooking must be short-lived and non-blocking; UIA queries should be on a background thread.

  • Complexity / maintainability: exposing additional providers adds responsibilities for lifecycle management (UiaDisconnectProvider, COM release). Tests and clear disposal rules mitigate this.

  • Designer impact: exposing new accessible types could affect tools that reflect on types; minimize breaking surface and keep default behavior unchanged.

Will this feature affect UI controls?

Yes — this feature specifically affects dialog accessibility and how controls within system dialogs are exposed to automation.

  • Will VS Designer need to support the feature?

Not in the MVP. The change is about runtime accessibility surface for dialogs; Designer updates are not required initially.

  • What impact will it have on accessibility?
    Positive: provides deterministic and richer UIA/MSAA surface for dialog elements (better screen-reader output, better automation behavior). Must ensure sensitive content is not overexposed.

  • Will this feature need to be localized or be localizable?
    No new localization strings required for the accessibility wrappers themselves. Accessible text content is from dialog content (already localized by app or OS).

attatched TaskDialog Inspect Object output

DirectUI_ac.txt

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-suggestion(1) Early API idea and discussion, it is NOT ready for implementationneeds-area-labeluntriagedThe team needs to look at this issue in the next triage

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions