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:
-
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.
-
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.
Describe the bug
Avalonia.Win32.WindowImpl.BeginMoveDragvalidatese.Pointer.IsPrimaryinside aDispatcher.UIThread.Postcontinuation rather than before posting:Two consequences:
The exception cannot be caught by anyone.
BeginMoveDraghas already returned by the timethe 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'sTask, so it ends up atTaskScheduler.UnobservedTaskException.The check does not need to be asynchronous.
e.Pointer.IsPrimaryis known at call time.Testing it before the
Postwould let the method either throw synchronously, where a caller canhandle 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
BeginMoveDragfromPointerPressedon a touch-capable Windows machine.Put a second finger on the title bar while the first is still down, so the pointer delivered to
BeginMoveDragis 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:
e.Pointer.IsPrimarybeforeDispatcher.UIThread.Postand throw synchronously, so thecaller has a chance to handle it; or
Either way the exception should not be raised from a continuation nobody can observe.
Avalonia version
12.1.2 (also present in the current
mastersource quoted above)OS
Windows
Additional context
x64, .NET 10, NativeAOT
Captured stack as reported by Sentry:
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.