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
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._dropPointstored is not in the coordinate system ofDragEventArgs._targetthat used to perform transformation.Root cause:
wpf/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/DragDrop.cs
Lines 955 to 1035 in 6ebec62
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
_lastTargetandtargetPoint.However,
targetPointwas calced withtargetby callingGetCurrentTargetin line 963, which meanstargetPointis associated withtarget.And that explains why Case A happened.
Then take a look at the function
GetCurrentTarget.wpf/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/DragDrop.cs
Lines 1288 to 1366 in 6ebec62
If the potential
targetisAllowDrop="false"(Line 1314), the function will settargettonull. And when thetargetisnull, the rawtargetPointwill not be translated (Line 1360). This is the reason for Case B.Btw, there is another way to raise a
DragLeaveevent, which located in funcitonOleDragLeave. But this is not part of our issue.Reproduction Steps (Case A for example)
Create a new .NET 10 WPF proj
MainWindow.xaml
MainWindow.xaml.cs
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:
Expected behavior
DragLeaveshould make sure that_dropPointis based on_target.Actual behavior
DragLeave's_dropPointdoesn't associated with_targetmost of the time.Regression?
No
Known Workarounds
No response
Impact
No response
Configuration
No response
Other information
No response