1
votes

I am trying to create a custom control in WPF that is an ItemsControl (with a customizable data template) that supports dragging items from one container to another. The dragging logic is pretty straight forward and I have managed to get that working.

The problem is that I'm trying to show a simple drag adorner (that's essentially a screenshot of the item/datatemplate being dragged). While I have managed to display the adorner and get it to follow the mouse cursor, it is extremely laggy. I have played around with two methods of building the adorner - first one would be attaching a content presenter to my custom adorner; the second would be actually overriding the OnRender method and drawing it myself. Both methods feature really poor performance.

This is how I've implemented my adorner:

public class ActionDragAdorner: Adorner
{
    private VisualCollection _Visuals;
    private ContentPresenter _ContentPresenter;
    private Rectangle _rect;

    public FrameworkElement AdornedElement { get; protected set; }
    public Point InitialClickLocation { get; set; }

    public Point CentralOffset
    {
        get
        {
            return new Point(-_rect.Width / 2, -_rect.Height / 2);
        }
    }

    public ActionDragAdorner(FrameworkElement adornedElement) 
        : base(adornedElement)
    {
        _Visuals = new VisualCollection(this);
        _ContentPresenter = new ContentPresenter();
        _Visuals.Add(_ContentPresenter);

        AdornedElement = adornedElement;

        _rect = new Rectangle();
        _rect.Width = adornedElement.ActualWidth;
        _rect.Height = adornedElement.ActualHeight;
        _rect.Fill = new VisualBrush(adornedElement);
        IsHitTestVisible = false;
        Content = _rect;

        _ContentPresenter.Arrange(new Rect(0, 0, _rect.Width, _rect.Height));

        this.Width = _rect.Width;
        this.Height = _rect.Height;
    }

    public ActionDragAdorner(FrameworkElement adornedElement, Visual content) 
        : this(adornedElement)
    { 
        Content = content; 
    }

    protected override Size MeasureOverride(Size constraint)
    {
        _ContentPresenter.Measure(constraint);
        return new Size(AdornedElement.ActualWidth, AdornedElement.ActualHeight);
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        _ContentPresenter.Arrange(new Rect(0, 0,
                finalSize.Width, finalSize.Height));
        return new Size(AdornedElement.ActualWidth, AdornedElement.ActualHeight);
    }

    protected override Visual GetVisualChild(int index)
    { return _Visuals[index]; }

    protected override int VisualChildrenCount
    { get { return _Visuals.Count; } }

    public object Content
    {
        get { return _ContentPresenter.Content; }
        set { _ContentPresenter.Content = value; }
    }
}

I'm starting the dragging operation in the PreviewMouseMove event, when the left button is pressed. Due to the fact the the DragDrop.DoDragDrop is blocking, the only way to update the position of the adorner (to track the mouse cursor) is by overriding the OnGiveFeedback event for my custom control:

protected override void OnGiveFeedback(GiveFeedbackEventArgs e)
{
    base.OnGiveFeedback(e);
    GetCursorPos(ref pointRef);

    Point relPos = this.PointFromScreen(pointRef.GetPoint());
    Point elementPos = dragAdorner.AdornedElement.TranslatePoint(new Point(0, 0), this);
    Point initialClick = dragAdorner.InitialClickLocation;
    Point pos = new Point(relPos.X - initialClick.X,
        relPos.Y - elementPos.Y - initialClick.Y);
    Rect target = new Rect(pos, dragAdorner.DesiredSize);
                
    dragAdorner.Arrange(target);
}

Focusing on user experience, we need to have a fluid adorner following the cursor - especially since the adorner itself is really simple - a border with a textblock inside. During performance profiling it seems that the UI thread has zero FPS dropouts that seem to be caused by too many layout updates (due to the Arrange call used to reposition the adorner). I've tried everything I can think of, including manually doing the render transformations. If dragging the item slowly, performance seems to be ok - however if I move the mouse faster the UI thread drops to zero FPS - probably trying to do too many layout updates too quickly; this is also backed up by the performance profiler, as during these zero FPS moments, only layout update calls are handled (with no render calls)

I've also looked at other samples online that feature drag and drop operations using adorners, but these also seem to be laggy.

The quesion: How can I make the adorner follow the mouse cursor in a fluent fashion, without choppy actions and decent FPS?

2

2 Answers

0
votes

My first guess would be that it's not your adorner that is slow, but the whole app. It's interacting with the adorner that is being dragged, and loads of events are being triggered and many layers of your UI is being involved. That's why when dragging slowly, then everything is OK.

To check the hypothesis - apply BitmapCache to the window you're dragging over. Here is an example how simple it is: https://stackoverflow.com/a/62635978/275330.

0
votes

After long hours of research and testing, I gave up on trying to display the drag adorner in a fluent manner and switched to displaying the adorner as a separate, borderless window using a visual brush. This bypasses the numerous useless layout calculations and offers a snappy UX.

For inspiration I used the following thread: https://stackoverflow.com/a/27975085/15010804

In order to make it behave similar to adorners, I had to do a couple of modifications. Hope this helps others with this problem:

  1. Create a new object that extends the WPF Window

  2. Adjust the constructor to the following

public ActionDragAdornerWindow(Visual dragElement) : base()
{
    WindowStyle = WindowStyle.None;
    AllowsTransparency = true;
    AllowDrop = false;
    Background = null;
    IsHitTestVisible = false;
    SizeToContent = SizeToContent.WidthAndHeight;
    Topmost = true;
    ShowInTaskbar = false;
    Opacity = 0.75;
    ShowActivated = false;

    Rectangle r = new Rectangle();
    r.Width = ((FrameworkElement)dragElement).ActualWidth;
    r.Height = ((FrameworkElement)dragElement).ActualHeight;
    r.IsHitTestVisible = false;
    r.Fill = new VisualBrush(dragElement);
    Content = r;       
}
  1. Use PInvoke to make the window trully transparent (so it would interfere with the drop event) - and call it in the overridden OnSourceInitialized method.
public const int WS_EX_TRANSPARENT = 0x00000020;
public const int GWL_EXSTYLE = (-20);

[DllImport("user32.dll")]
public static extern int GetWindowLong(IntPtr hwnd, int index);

[DllImport("user32.dll")]
public static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);

protected override void OnSourceInitialized(EventArgs e)
{
    base.OnSourceInitialized(e);

    // Get this window's handle
    IntPtr hwnd = new WindowInteropHelper(this).Handle;

    // Change the extended window style to include WS_EX_TRANSPARENT
    int extendedStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
    SetWindowLong(hwnd, GWL_EXSTYLE, extendedStyle | WS_EX_TRANSPARENT);
}

 4. Make sure to update the adorner/window position in the GiveFeedback event, by capturing the mouse using GetCursorPos (PInvoke). Depending on the use cases and desired effect, some coordinate transformations will be required.