Skip to content

WindowImpl.BeginMoveDrag throws from a posted continuation, so the exception is uncatchable and surfaces as an unobserved task exception #22220

Description

@0x90d

Describe the bug

Avalonia.Win32.WindowImpl.BeginMoveDrag validates e.Pointer.IsPrimary inside a
Dispatcher.UIThread.Post continuation rather than before posting:

public void BeginMoveDrag(PointerPressedEventArgs e)
{
    e.Pointer.Capture(null);

    Dispatcher.UIThread.Post(() =>
    {
        if (e.Pointer.IsPrimary)
        {
            SendMessage(_hwnd, (int)WindowsMessage.WM_SYSCOMMAND, (IntPtr)SC_MOUSEMOVE, IntPtr.Zero);
            SendMessage(_hwnd, (int)WindowsMessage.WM_LBUTTONUP, IntPtr.Zero, IntPtr.Zero);
        }
        else
        {
            throw new InvalidOperationException("BeginMoveDrag Failed");
        }
    }, DispatcherPriority.Send);
}

Two consequences:

  1. The exception cannot be caught by anyone. BeginMoveDrag has already returned by the time
    the continuation runs, so neither application code nor the control that called it (in our case a
    third-party title bar) can wrap it in a try/catch. Nothing observes the posted operation's
    Task, so it ends up at TaskScheduler.UnobservedTaskException.

  2. The check does not need to be asynchronous. e.Pointer.IsPrimary is known at call time.
    Testing it before the Post would let the method either throw synchronously, where a caller can
    handle it, or return without starting a drag.

It is also worth questioning whether this should throw at all. A non-primary pointer - a second
touch contact, or a pen while a finger is already down - is a perfectly ordinary thing for a user to
do on a title bar. "The user put a second finger down" does not seem like an exceptional condition;
silently not starting a window drag looks like the more reasonable behaviour.

To Reproduce

Any control that calls BeginMoveDrag from PointerPressed on a touch-capable Windows machine.
Put a second finger on the title bar while the first is still down, so the pointer delivered to
BeginMoveDrag is not the primary one.

We did not reproduce this by hand - it arrived as production crash telemetry from end users, 70
occurrences over 90 days across 4 users, all on Windows.

Expected behavior

Either:

  • validate e.Pointer.IsPrimary before Dispatcher.UIThread.Post and throw synchronously, so the
    caller has a chance to handle it; or
  • treat a non-primary pointer as "no drag" and return without throwing.

Either way the exception should not be raised from a continuation nobody can observe.

Avalonia version

12.1.2 (also present in the current master source quoted above)

OS

Windows

Additional context

x64, .NET 10, NativeAOT

Captured stack as reported by Sentry:

System.InvalidOperationException: BeginMoveDrag Failed
  at Avalonia.Win32.WindowImpl.<>c__DisplayClass___.<BeginMoveDrag>b__0()
  at Avalonia.Threading.DispatcherOperation.InvokeCore()
     [mechanism: UnobservedTaskException]

Not fatal - .NET ignores unobserved task exceptions by default - but it is unsuppressable from the
application side, so it reaches crash reporting as noise that the application cannot act on.

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