Improve Windows DevFlow dialog automation#284
Conversation
Add native Windows UIA probing and action support for DevFlow agents so modal dialogs can be discovered and controlled without depending on the app UI dispatcher. Backport related Windows driver UIA helpers and add Windows dialog integration coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Expert Code Review: 6 findings posted inline (1 critical, 4 moderate, 1 minor). See the summary comment for full methodology and discarded findings.
Generated by Expert Code Review (auto) for issue #284 · ● 30.2M
There was a problem hiding this comment.
Pull request overview
This PR improves DevFlow’s Windows automation reliability when synchronous native dialogs are open by adding a native UI Automation (UIA) probe that can inspect and interact with native dialog UI off the MAUI UI thread, then merging those native results into the existing tree/query/action flows.
Changes:
- Add a Windows-native UIA probe (
NativeWindowProbe) and integrate native element discovery/actions into the agent’s tree/query/element/action endpoints. - Extend the Windows driver UIA interop and add native fallbacks/fast paths for tap/fill/clear/focus/scroll plus a Win32+Skia screenshot fallback.
- Add Windows-only integration tests for native dialog discovery/actions and improve WPF sample automation IDs.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/DevFlow/Microsoft.Maui.DevFlow.Driver/WindowsAppDriver.cs | Adds UIA/Win32 fallback paths for actions and screenshots; updates alert handling to use resolved windows. |
| src/DevFlow/Microsoft.Maui.DevFlow.Driver/Windows/UIAutomationInterop.cs | Expands UIA helpers (bounds, focus/value/scroll, window lookup, element search, label normalization). |
| src/DevFlow/Microsoft.Maui.DevFlow.Driver/AppDriverFactory.cs | Allows selecting Windows driver via wpf platform alias. |
| src/DevFlow/Microsoft.Maui.DevFlow.Driver/AppDriverBase.cs | Makes core action methods virtual so platform drivers can override. |
| src/DevFlow/Microsoft.Maui.DevFlow.Agent/Windows/NativeWindowProbe.cs | New native UIA discovery + action helper for dialogs/native windows. |
| src/DevFlow/Microsoft.Maui.DevFlow.Agent/VisualTreeWalker.cs | WinUI tree walker now supports native elements via NativeWindowProbe. |
| src/DevFlow/Microsoft.Maui.DevFlow.Agent/Microsoft.Maui.DevFlow.Agent.csproj | Adds Microsoft.WindowsDesktop.App framework reference for Windows UIA types. |
| src/DevFlow/Microsoft.Maui.DevFlow.Agent/DevFlowAgentService.cs | Schedules native invoke for button taps before MAUI inline click dispatch on Windows. |
| src/DevFlow/Microsoft.Maui.DevFlow.Agent.WPF/WpfVisualTreeWalker.cs | Adds native element support to WPF walker using the shared probe. |
| src/DevFlow/Microsoft.Maui.DevFlow.Agent.WPF/WpfAgentService.cs | Schedules native invoke/toggle/click dispatch prior to managed click handling in WPF. |
| src/DevFlow/Microsoft.Maui.DevFlow.Agent.WPF/Microsoft.Maui.DevFlow.Agent.WPF.csproj | Links NativeWindowProbe into the WPF agent build. |
| src/DevFlow/Microsoft.Maui.DevFlow.Agent.IntegrationTests/UiInspectionTests.cs | Adds Windows-only integration test validating native dialog elements appear in tree. |
| src/DevFlow/Microsoft.Maui.DevFlow.Agent.IntegrationTests/UiActionTests.cs | Adds Windows-only integration test validating tapping a native dialog button works. |
| src/DevFlow/Microsoft.Maui.DevFlow.Agent.Core/VisualTreeWalker.cs | Introduces native-element discovery/query/action abstraction points for platform walkers. |
| src/DevFlow/Microsoft.Maui.DevFlow.Agent.Core/DevFlowAgentService.cs | Merges native probe results into tree/query and adds native: fast paths for actions. |
| platforms/Windows.WPF/samples/Windows.WPF.Sample/Pages/AlertsPage.cs | Adds AutomationIds to dialog sample controls for testability. |
- NativeWindowProbe: replace unbounded FindAll(TreeScope.Descendants) in FindDialogCandidates with a bounded BFS over TreeScope.Children, capping total nodes scanned and walk depth to keep dialog discovery from dominating tree/query latency on large WinUI apps. - DevFlowAgentService.CaptureUiOrNativeAsync: share a CancellationTokenSource across the two probe timer races so surviving Task.Delay timers are cancelled promptly under high query throughput. - DevFlowAgentService.CaptureUiOrNativeAsync: bound the final native task await with NativeUiProbeTimeoutMs so a frozen app's UIA tree walk cannot block the HTTP request indefinitely. - DevFlowAgentService.CaptureUiOrNativeAsync: observe the abandoned uiTask on timeout so a later fault doesn't trigger UnobservedTaskException. - TryScheduleNativeTapFirst: document fire-and-forget semantics so callers understand the queued-vs-completed contract. - NativeWindowProbe: add TryBuildCachedElementInfo helper; use it from PlatformVisualTreeWalker and WpfVisualTreeWalker so GetNativeElementInfoById rebuilds the cached element instead of re-enumerating every same-process window on each native: id lookup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- WindowsAppDriver.ClickPoint: cast inputs.Length to uint when comparing
against SendInput's uint return value so the Windows build compiles.
- AppDriverFactory: include 'wpf' in the ArgumentException message so the
error reflects the actual accepted platform list.
- DevFlowAgentService.CaptureUiOrNativeAsync: add a single in-flight gate
(_pendingCaptureUiTask) so repeated tree/query calls don't accumulate
unbounded queued UI-dispatch work while the dispatcher is blocked.
Subsequent callers fall straight through to a native-only probe until
the previously queued uiTask drains.
- NativeWindowProbe: add ExtractHwndsFromId helper that parses the embedded
HWND from a 'native:hwnd:0x..' element id.
- PlatformVisualTreeWalker and WpfVisualTreeWalker: on cache miss, re-walk
the native tree seeded with the HWND parsed from the requested id so
':dialog:{n}' prefixes from AppendKnownWindowDialogSubtrees are
regenerated and id resolution remains stable across calls.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Expert Code Review — PR #284Methodology: 3 independent reviewers with adversarial consensus Findings Summary8 inline comments posted (1 🔴 critical, 5 🟡 moderate, 1 🟢 minor):
† = consensus reached via follow-up dispute resolution Discarded Findings (single-reviewer, no consensus)
CI Status✅ All CI checks passed — Test CoverageThe PR adds integration tests (
|
There was a problem hiding this comment.
Expert Code Review — Native Windows UIA Dialog Automation
Reviewed the full source for NativeWindowProbe.cs, WindowsAppDriver.cs, UIAutomationInterop.cs, DevFlowAgentService.cs (Core and platform), WpfVisualTreeWalker.cs, and PlatformVisualTreeWalker.cs.
Findings
1. 🟡 MODERATE — Race condition: TryScheduleNativeTapFirst returns "ok" without confirming the action completed
- File:
src/DevFlow/Microsoft.Maui.DevFlow.Agent.Core/DevFlowAgentService.cs, line ~1532 - Scenario:
TryScheduleNativeTapFirst(btn)posts an asynchronousBeginInvoke/TryEnqueueand returnstrueimmediately. The handler returns"ok"to the caller (HTTP response). If the enqueued invoke fails (e.g., element disposed, or the enqueue is dropped because the dispatcher is shutting down), the caller believes the tap succeeded but it never fired. On WPF,BeginInvokecan silently swallow exceptions. - Recommendation: Consider returning a sentinel like
"ok:scheduled"so callers can distinguish confirmed vs. fire-and-forget actions, or log a diagnostic when the scheduled action fails internally (wrap the delegate body in try/catch with diagnostics).
2. 🟡 MODERATE — CaptureUiOrNativeAsync leaks unobserved Task.Delay timers
- File:
src/DevFlow/Microsoft.Maui.DevFlow.Agent.Core/DevFlowAgentService.cs, lines 800 and 816 - Scenario: Two
Task.Delay(NativeUiProbeTimeoutMs)calls create timers. When the real task completes before the timeout, the delay timer continues running and itsTaskis never awaited. While this won't throw (unobserved empty delays), it creates timer pressure under high call frequency. On a hot tree/query loop, thousands of abandoned timers accumulate until GC collects them. - Recommendation: Use
CancellationTokenSource+ cancel the delay when the real task completes, or use a shared timer pattern. Example:using var cts = new CancellationTokenSource(); var winner = await Task.WhenAny(hwndSource.Task, Task.Delay(NativeUiProbeTimeoutMs, cts.Token)); cts.Cancel(); // cancel the unused delay
3. 🟡 MODERATE — NativeWindowHandle is int (32-bit) in System.Windows.Automation.AutomationElement.Current, compared to IntPtr.ToInt64() (64-bit)
- File:
src/DevFlow/Microsoft.Maui.DevFlow.Agent/Windows/NativeWindowProbe.cs, line 275 - Scenario:
current.NativeWindowHandleis typedintin the .NET UIA API. On 64-bit Windows, window handles above0x7FFFFFFFwill appear as negative integers. The comparisoncurrent.NativeWindowHandle == rootHwnd.ToInt64()will fail to match if the handle'sintrepresentation is negative (e.g.,intvalue =-1234vs.longvalue =0xFFFFF...), causing the dialog to not be excluded from candidates. - Recommendation: Compare using:
(long)(int)current.NativeWindowHandle == rootHwnd.ToInt64()— or better, cast both to the same unsigned representation:(uint)current.NativeWindowHandle == (uint)(int)rootHwnd.ToInt32().
4. 🟡 MODERATE — IsAncestor can be O(depth2) per candidate × O(n) candidates = potentially quadratic in FindDialogCandidates
- File:
src/DevFlow/Microsoft.Maui.DevFlow.Agent/Windows/NativeWindowProbe.cs, lines 247–258 - Scenario: For each element in
descendants(potentially thousands for a complex app),IsAncestorwalks up the tree for each existing candidate. This nested loop over UIA cross-process calls can cause multi-second freezes when a large app with many UI elements has an open dialog. All of this runs on aTask.Runworker but still blocks the response untilnativeTaskcompletes (bounded by the 1500ms timeout, which provides some mitigation). - Recommendation: This is partially mitigated by the timeout. For future hardening, consider limiting
descendantsto elements of specific control types (Window, Pane, Dialog, Group) rather thanTrueCondition, or use a tree scope ofChildrenrecursed only a few levels deep.
5. 🟡 MODERATE — CaptureScreenRectangle potential integer overflow on very large DPI-scaled windows
- File:
src/DevFlow/Microsoft.Maui.DevFlow.Driver/WindowsAppDriver.cs, line 657 - Scenario:
var bytes = new byte[rect.Width * rect.Height * 4]— for a 4K monitor (3840×2160), this is ~33MB which is fine. But at high DPI scales (e.g., 300%), Width and Height fromGetWindowRectare in physical pixels and could be7680 * 4320 * 4 = ~132MB. The multiplicationrect.Width * rect.Height * 4usesintarithmetic and could overflow if the combined pixel count exceedsint.MaxValue / 4. This would produce a small or negative buffer, thenMarshal.Copywould corrupt memory or throw. - Recommendation: Add a sanity check:
if ((long)rect.Width * rect.Height * 4 > int.MaxValue) return null;or usecheckedarithmetic.
6. 🟢 MINOR — ClickPoint uses SetCursorPos + SendInput without coordinate normalization
- File:
src/DevFlow/Microsoft.Maui.DevFlow.Driver/WindowsAppDriver.cs, lines 713–723 - Scenario:
SetCursorPos(x, y)uses pixel coordinates from UIA'sBoundingRectangle(which reports in physical pixels).SendInputwithMOUSEEVENTF_LEFTDOWNwithoutMOUSEEVENTF_ABSOLUTEsends a relative mouse-down at the current cursor position. If another process moves the cursor betweenSetCursorPosandSendInput, the click lands at the wrong location. This is a known TOCTOU race in screen automation but could cause flaky test results. - Recommendation: Use
MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_LEFTDOWNwith normalized coordinates (65535-based) in theMOUSEINPUT.dx/dyfields for atomic positioned clicks. This eliminates the race between SetCursorPos and SendInput.
7. 🟢 MINOR — FindFirstByAutomationIdOrName uses CreateTrueCondition for full-tree enumeration
- File:
src/DevFlow/Microsoft.Maui.DevFlow.Driver/Windows/UIAutomationInterop.cs, line 289 - Scenario: This loads the entire UIA subtree of each window into memory just to linear-search for a matching AutomationId or Name. For large apps (e.g., Visual Studio), this can return tens of thousands of elements per window and take seconds.
- Recommendation: Use
FindFirstwith anOrConditioncombining a property condition for AutomationId and one for Name. This lets the UIA provider optimize the search server-side rather than returning everything to the client.
8. 🟢 MINOR — _nativeObjects dictionary reference replacement under lock is safe but WalkNativeTree is not thread-safe with itself
- File:
src/DevFlow/Microsoft.Maui.DevFlow.Agent/VisualTreeWalker.cs, lines 648–658 (and WPF equivalent) - Scenario: If two concurrent HTTP requests trigger
GetNativeElementByIdsimultaneously, both may callWalkNativeTree(Array.Empty<IntPtr>()). Each creates its ownnativeObjectsdictionary and assigns it under lock. The last writer wins, potentially losing entries from the other's probe. This could make an element found by one probe unfindable by a subsequent lookup. - Recommendation: This is low-severity since the window of concurrent access is small and the element can be re-found on retry. No action needed now, but could add a simple gate (e.g.,
Interlocked.CompareExchangepattern) to skip redundant walks.
Summary
No critical (🔴) issues found. The code is well-structured with good defensive exception handling around UIA COM calls. The most impactful items are the timer leak under load (#2) and the TryScheduleNativeTapFirst fire-and-forget semantics (#1). The IntPtr/int comparison mismatch (#3) is a correctness bug that could manifest on systems with high window handle values.
Generated by Expert Code Review (auto) for issue #284 · ● 28.8M
- NativeWindowProbe: zero-extend HWND ints to avoid sign-extension when comparing to MAUI-supplied window handles (line 64 and IsDialogCandidate) - NativeWindowProbe: disambiguate BuildId collisions by appending the visual path when an id already exists in nativeObjects - WpfAgentService.TryScheduleNativeTapFirst: wrap dispatched Invoke/Toggle/RaiseEvent lambdas in try/catch so a stale or disabled element does not surface via DispatcherUnhandledException - DevFlowAgentService (WinUI): wrap DispatcherQueue.TryEnqueue lambda in try/catch to avoid CoreApplication.UnhandledErrorDetected on stale invoke targets - WindowsAppDriver.ClickPoint: capture and restore the cursor position around the synthetic click to avoid permanently relocating the user's cursor Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Windows integration tests need the macos workload to compile shared code that references Apple TFMs. Standardize all four DevFlow integration jobs (android, apple, maccatalyst, windows) on the same broad workload set: maui ios macos maccatalyst tvos android wasm-tools. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Windows DevFlow currently depends on the app UI dispatcher for tree/query/action routes, which can block when synchronous native Windows dialogs are open. This change ports the relevant Ailoha Windows dialog automation work so WinUI and WPF agents can discover and interact with native UIA dialogs off the UI thread.
Summary
native:fast paths for tap, fill, clear, focus, and scroll.Notes
The MAUI WinUI Windows target compiles through CoreCompile on macOS, then fails at the expected Windows-only
MakePri.exestep. Live Windows dialog integration still needs a Windows host or CI run.Validation
dotnet test src/DevFlow/Microsoft.Maui.DevFlow.Tests/Microsoft.Maui.DevFlow.Tests.csprojdotnet build src/DevFlow/Microsoft.Maui.DevFlow.Agent.IntegrationTests/Microsoft.Maui.DevFlow.Agent.IntegrationTests.csproj --no-restoredotnet build src/DevFlow/Microsoft.Maui.DevFlow.Agent.WPF/Microsoft.Maui.DevFlow.Agent.WPF.csproj -p:EnableWindowsTargeting=truedotnet build src/DevFlow/Microsoft.Maui.DevFlow.Driver/Microsoft.Maui.DevFlow.Driver.csprojgit diff --check