Skip to content

DragLeave constructs DragEventArgs with coordinates relative to the new target instead of the event target #11774

Description

@mosyy098

Description

In WPF's drag-and-drop, when the mouse moves from an AllowDrop element to another element, DragLeave event will be raised for the previous element. However, the DragEventArgs passed contains Point that not calculated based on the event target (element which mouse left).
Case A: When the new element (which mouse gonna enter) is AllowDrop="true", that Point is calculated based on the new element.
Case B: If the new element is AllowDrop='false', that Point will be raw point in screen coordinates.

This bug causes DragEventArgs.GetPosition(IInputElement) to return meaningless Point when you get such a bad DragEventArgs. Since the DragEventArgs._dropPoint stored is not in the coordinate system of DragEventArgs._target that used to perform transformation.

Root cause:

int UnsafeNativeMethods.IOleDropTarget.OleDragOver(int dragDropKeyStates, long point, ref int effects)
{
DependencyObject target;
Point targetPoint;
Invariant.Assert(_dataObject != null);
// Get the current target from the mouse drag point that is based on screen.
target = GetCurrentTarget(point, out targetPoint);
// Raise DragOver event to the target to get DragDrop effect status from the target.
if (target != null)
{
// Avalon apps can have only one window handle, so we need to generate DragLeave and
// DragEnter event to target when target is changed by the mouse dragging.
// If the current target is the same as the last target, just raise DragOver event to the target.
if (target != _lastTarget)
{
try
{
if (_lastTarget != null)
{
// Raise DragLeave event to the last target.
RaiseDragEvent(
DragDrop.DragLeaveEvent,
dragDropKeyStates,
ref effects,
_lastTarget,
targetPoint);
}
// Raise DragEnter event to the new target.
RaiseDragEvent(
DragDrop.DragEnterEvent,
dragDropKeyStates,
ref effects,
target,
targetPoint);
}
finally
{
// Reset the last target element to check it with the next current element.
_lastTarget = target;
}
}
else
{
// Raise DragOver event to the target.
RaiseDragEvent(
DragDrop.DragOverEvent,
dragDropKeyStates,
ref effects,
target,
targetPoint);
}
}
else
{
try
{
if (_lastTarget != null)
{
// Raise DragLeave event to the last target.
RaiseDragEvent(
DragDrop.DragLeaveEvent,
dragDropKeyStates,
ref effects,
_lastTarget,
targetPoint);
}
}
finally
{
// Update the last target element as the current target element.
_lastTarget = target;
effects = (int)DragDropEffects.None;
}
}
return NativeMethods.S_OK;
}

As you can see, there are two way to raise a DragDrop.DragLeaveEvent, located in line 978 and line 1018 separately.
Both of them used variable _lastTarget and targetPoint.
However, targetPoint was calced with target by calling GetCurrentTarget in line 963, which means targetPoint is associated with target.
And that explains why Case A happened.

Then take a look at the function GetCurrentTarget.

private DependencyObject GetCurrentTarget(long dragPoint, out Point targetPoint)
{
HwndSource source;
DependencyObject target;
// Initialize the target as null.
target = null;
// Get the source from the source to hit-test and translate point.
source = HwndSource.FromHwnd(_windowHandle);
// Get the client point from the screen point.
targetPoint = GetClientPointFromScreenPoint(dragPoint, source);
if (source != null)
{
UIElement targetUIElement;
// Hit-Testing to get the target object from the current mouse dragging point.
// LocalHitTest() will get the hit-tested object from the mouse dragging point after
// conversion the pixel to the measure unit.
target = MouseDevice.LocalHitTest(targetPoint, source) as DependencyObject;
targetUIElement = target as UIElement;
if (targetUIElement != null)
{
if (targetUIElement.AllowDrop)
{
// Assign the target as the UIElement.
target = targetUIElement;
}
else
{
target = null;
}
}
else
{
ContentElement targetContentElement;
targetContentElement = target as ContentElement;
if (targetContentElement != null)
{
if (targetContentElement.AllowDrop)
{
// Assign the target as the ContentElement.
target = targetContentElement;
}
else
{
target = null;
}
}
else
{
UIElement3D targetUIElement3D;
targetUIElement3D = target as UIElement3D;
if (targetUIElement3D != null)
{
if (targetUIElement3D.AllowDrop)
{
target = targetUIElement3D;
}
else
{
target = null;
}
}
}
}
if (target != null)
{
// Translate the client point to the root point and then translate it to target point.
targetPoint = PointUtil.ClientToRoot(targetPoint, source);
targetPoint = InputElement.TranslatePoint(targetPoint, source.RootVisual, target);
}
}

If the potential target is AllowDrop="false" (Line 1314), the function will set target to null. And when the target is null, the raw targetPoint will not be translated (Line 1360). This is the reason for Case B.

Btw, there is another way to raise a DragLeave event, which located in funciton OleDragLeave. But this is not part of our issue.

Reproduction Steps (Case A for example)

Create a new .NET 10 WPF proj

MainWindow.xaml

<Window x:Class="DragLeaveFix.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="450" Width="600">

    <!-- Element below the "rect" must be AllowDrop="True". AllowDrop="False" will be another bug case. -->
    <Grid x:Name="grid" AllowDrop="True" Background="Transparent">
        <Rectangle x:Name="rect" AllowDrop="True" DragLeave="Rectangle_DragLeave"
                   Fill="Gray" Width="100" HorizontalAlignment="Right"/>
    </Grid>
</Window>

MainWindow.xaml.cs

using System.Diagnostics;
using System.Reflection;
using System.Windows;

namespace DragLeaveFix
{
    public partial class MainWindow : Window
    {
        public MainWindow() => InitializeComponent();
        private readonly FieldInfo DragEventArgsField_dropPoint = typeof(DragEventArgs).GetField("_dropPoint", BindingFlags.Instance | BindingFlags.NonPublic)!;
        private readonly FieldInfo DragEventArgsField_target = typeof(DragEventArgs).GetField("_target", BindingFlags.Instance | BindingFlags.NonPublic)!;
        private void Rectangle_DragLeave(object sender, DragEventArgs e)
        {
            // if the event was raised by OleDragLeave, e.KeyStates will be DragDropKeyStates.None
            // so make sure it was raised by OleDragOver
            if (e.KeyStates != DragDropKeyStates.None)
            {
                // use "grid" to contain "rect" instead of using Window directly
                // bcuz constrain size for the element inside the window doesn't equal to window's actual size
                var gridRect = new Rect(0, 0, grid.ActualWidth, grid.ActualHeight);

                var gridWidth   = grid.ActualWidth;
                var rectWidth   = rect.ActualWidth;
                var rectPosX    = rect.TranslatePoint(new(0, 0), grid).X;
                var e_dropPoint = (Point)DragEventArgsField_dropPoint.GetValue(e)!;
                var e_target    = (UIElement)DragEventArgsField_target.GetValue(e)!;
                var e_PosInGrid = e.GetPosition(grid);

                // As the issue said, e_dropPoint is based on "grid" (which your mouse gonna move into), not the e_target ("rect")
                if (gridRect.Contains(e_dropPoint))
                {
                    Debug.WriteLine("\n\n\n");
                    Debug.WriteLine("===== Element Info =====");
                    Debug.WriteLine($"'grid' Width: {gridWidth}");
                    Debug.WriteLine($"'rect' Width: {rectWidth}");
                    Debug.WriteLine($"'rect' Pos X (relative to 'grid'): {rectPosX}");

                    Debug.WriteLine("===== Args Info =====");
                    Debug.WriteLine($"e._dropPoint: {e_dropPoint}");
                    Debug.WriteLine($"e._target: {e_target}");
                    Debug.WriteLine($"e.GetPosition(grid).X: {e_PosInGrid.X}");

                    Debug.WriteLine("===== issue =====");
                    Debug.WriteLine($"e.GetPosition(grid) inside 'grid'? (Expected: true, Actual: false): {gridRect.Contains(e_PosInGrid)}");
                    Debug.WriteLine($"How do we get this: rectPosX + e._dropPoint.X == e.GetPosition(grid).X -> {rectPosX + e_dropPoint.X} == {e_PosInGrid.X}");
                }
            }
        }
    }
}

Run the App in debug mode, drag something into the Gray Area (e.g. files on your desktop). Then drag you mouse to the White Area without releasing mouse button.
You can see something like this in debug output:

===== Element Info =====
'grid' Width: 585.6
'rect' Width: 100
'rect' Pos X (relative to 'grid'): 485.6
===== Args Info =====
e._dropPoint: 481.6,228
e._target: System.Windows.Shapes.Rectangle
e.GetPosition(grid).X: 967.2
===== issue =====
e.GetPosition(grid) inside 'grid'? (Expected: true, Actual: false): False
How do we get this: rectPosX + e._dropPoint.X == e.GetPosition(grid).X -> 967.20 == 967.20

Expected behavior

DragLeave should make sure that _dropPoint is based on _target.

Actual behavior

DragLeave's _dropPoint doesn't associated with _target most of the time.

Regression?

No

Known Workarounds

No response

Impact

No response

Configuration

No response

Other information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugProduct bug (most likely)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions