Skip to content

Win32: WM_NCHITTEST on a closed window with extended client area throws ArgumentNullException (Parameter 'visual') from HitTestChromeElement #22324

Description

@0x90d

Describe the bug

After a Window with an extended client area has closed, its HWND can still receive non-client mouse messages (WM_NCHITTEST, WM_NCMOUSEMOVE, WM_NCPOINTER*, ...). WindowImpl.CustomCaptionProc routes them to HitTestVisual, which calls IInputRoot.HitTestChromeElement, and that throws because PresentationSource.RootVisual is already null:

System.ArgumentNullException: Value cannot be null. (Parameter 'visual')
   at Avalonia.Utilities.ThrowHelper.<ThrowIfNull>g__ThrowArgumentNullException|0_0(String paramName)
   at Avalonia.VisualTree.VisualExtensions.GetVisualAt(Visual visual, Point p, Func`2 filter)
   at Avalonia.Controls.PresentationSource.Avalonia.Input.IInputRoot.HitTestChromeElement(Point point)
   at Avalonia.Win32.WindowImpl.HitTestVisual(IntPtr lParam)
   at Avalonia.Win32.WindowImpl.CustomCaptionProc(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam, Boolean& callDwp)
   at Avalonia.Win32.WindowImpl.WndProc(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam)
   at Avalonia.Win32.WindowImpl.WndProcMessageHandler(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam)

Because this throws inside the window procedure, Dispatcher.UIThread.UnhandledException never sees it and the process dies. We see it in production telemetry (56 events from 14 users across our releases on 12.1.x). Those events have no app frames: the message comes from the OS while a window is closing.

Cause

  • TopLevel.HandleClosed() sets _source.RootVisual = null (and PlatformImpl = null) before raising Closed.
  • The Win32 WindowImpl._owner input root is set once in SetInputRoot and never cleared, so HitTestVisual still calls into the PresentationSource.
  • PresentationSource.HitTestChromeElement dereferences RootVisual without a null check:
    WindowDecorationsElementRole? IInputRoot.HitTestChromeElement(Point point)
    {
        var visual = RootVisual.GetVisualAt(point, ChromeHitTestFilter);
        return GetChromeRoleFromVisual(visual);
    }
    This is unchanged on master.

X11 and Wayland call the same HitTestChromeElement, so they are probably affected too (not verified).

To Reproduce

Minimal app (Avalonia 12.1.2, Windows 11). The explicit SendMessage stands in for the OS delivering WM_NCHITTEST while the mouse is over a closing window:

var w = new Window { Width = 400, Height = 300, ExtendClientAreaToDecorationsHint = true };
nint h = 0;
w.Opened += (_, _) =>
{
    h = w.TryGetPlatformHandle()!.Handle;
    DispatcherTimer.RunOnce(() => w.Close(), TimeSpan.FromMilliseconds(500));
};
w.Closed += (_, _) =>
{
    GetWindowRect(h, out var r);
    SendMessage(h, 0x0084 /* WM_NCHITTEST */, 0, (nint)(((r.Top + 10) << 16) | ((r.Left + 100) & 0xFFFF)));
    // -> ArgumentNullException above
};

repro.zip

Expected behavior

No exception. A closed window has nothing to hit-test, so HitTestChromeElement should return null when RootVisual is null:

WindowDecorationsElementRole? IInputRoot.HitTestChromeElement(Point point)
    => RootVisual is { } root ? GetChromeRoleFromVisual(root.GetVisualAt(point, ChromeHitTestFilter)) : null;

Alternatively, WindowImpl could stop hit-testing (or clear _owner) once the top level has closed.

Avalonia version

12.1.2, still present on master

OS

Windows

Additional context

Workaround

A Win32Properties.AddWndProcHookCallback hook on each window that claims the non-client mouse messages (returning HTNOWHERE for WM_NCHITTEST) once TopLevel.PlatformImpl is null. The hook runs before WndProc.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions