Drag & Drop in WPF ... Explained end to end ..

Jaime Rodriguez
On Windows Phone, Windows Presentation Foundation, Silverlight and Windows 7

Drag & Drop in WPF ... Explained end to end ..

Rate This
  • Comments 6

How to do Drag& Drop in WPF is a question I hear often... 
I have seen some great samples out there, but most focus on either a big code sample or a niche scenario...  a couple of times I have ended up having to help some one who got stuck.
I hope the below write up is useful to explain the steps and decisions to get drag & drop done.. and it comes with sample snippets ...

-- ---- -------------------
From  [http://msdn2.microsoft.com/en-us/library/aa289508(vs.71).aspx]  Here is the sequence of events in a typical drag-and-drop operation:

  1. Dragging is initiated by calling the DoDragDrop method for the source control.

    The DoDragDrop method takes two parameters:

    • data, specifying the data to pass
    • allowedEffects, specifying which operations (copying and/or moving) are allowed

    A new DataObject object is automatically created.

  2. This in turn raises the GiveFeedback event. In most cases you do not need to worry about the GiveFeedback event, but if you wanted to display a custom mouse pointer during the drag, this is where you would add your code.
  3. Any control with its AllowDrop property set to True is a potential drop target. The AllowDrop property can be set in the Properties window at design time, or programmatically in the Form_Load event.
  4. As the mouse passes over each control, the DragEnter event for that control is raised. The GetDataPresent method is used to make sure that the format of the data is appropriate to the target control, and the Effect property is used to display the appropriate mouse pointer.
  5. If the user releases the mouse button over a valid drop target, the DragDrop event is raised. Code in the DragDrop event handler extracts the data from the DataObject object and displays it in the target control.

------------

Let's walk through it in WPF...

Detecting Drag & Drop.

Before the DoDragDrop is called, we must detect a mouse Drag operation on the source...  A mouse drag is usually a MouseLeftButtonDown + a MouseMove (before MouseLeftButton goes up) ...

So, our drag & drop source control needs to subscribe to these two events:

void Window1_Loaded(object sender, RoutedEventArgs e)
     {
         this.DragSource.PreviewMouseLeftButtonDown += new MouseButtonEventHandler(DragSource_PreviewMouseLeftButtonDown);
         this.DragSource.PreviewMouseMove += new MouseEventHandler(DragSource_PreviewMouseMove);
     }

To prevent from starting a false drag & drop operation where the user accidentally drags, you can use SystemParameters.MinimumHorizontalDragDistance  and SystemParameters.MinimumVerticalDragDistance

One way to do this is on MouseLeftButtonDown, record the starting position  and  onMouseMove check if the mouse has moved far enough..

        void DragSource_PreviewMouseMove(object sender, MouseEventArgs e)
        {
            if (e.LeftButton == MouseButtonState.Pressed && !IsDragging)
            {
                Point position = e.GetPosition(null);

                if (Math.Abs(position.X - _startPoint.X) > SystemParameters.MinimumHorizontalDragDistance ||
                    Math.Abs(position.Y - _startPoint.Y) > SystemParameters.MinimumVerticalDragDistance)
                {
                  StartDrag(e); 

                }
            }   
        }

        void DragSource_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            _startPoint = e.GetPosition(null);
        }
 

 

Its a Drag .. now what?

The data!  You need to find out what is under the mouse when dragging.
I will omit take the easy way out and assume that whoever is triggering the MouseMove is what I want to drag .. so look at MouseEventArgs.OriginalSource..   [or you could do some 2D HitTesting using VisualTreeHelper .. In Part3 of this write up will try to walk you through hit testing the listbox -which is the other common scenario I encounter-.

Once you have the object to drag, you will need to package what you are a sending into a DataObject that describes the data you are passing around. 
DataObject is a wrapper to push generic data (identified with extensible formats) into drag/drop..  As long as both the source and destination understand the format, you will be set.  As such, DataObject has a couple interesting methods:

  • SetData (  Type format, object data )    /// format is the "format" of the day you are passing ( e.g. Formats.Text,  Formats.Image, etc.. ) you can pass any custom types.
  • GetDataPresent (  Type format )  /// is what the drop target will use to inquire and extract the data .. if it is a type it can handle, it will call GetData () and handle it ..

Not much interesting stuff here..  In the sample I just hard-coded my data to be of type string... this makes it easier to paste into external containers (for example Word, which you can use to test this part of the write-up).   I do have to stress that drag & dropping should be about the data ... 

Providing visual feedback during the drag & drop operation..

Before we call DoDragDrop () we have a few 'choices' to make around the feedback we want to provide and the 'scope' of the d&d.  

  • Do we want a custom cursor to display while we are doing the Drag operation ?  If we want a cursor, what should it be??
  • How far do we want to drag?? within the app or across windows apps?

 

Simplest scenario:  No custom cursor and we want it to drag across apps: 

If you don't want a fancy cursor, you are done!!  You can call DoDragDrop directly ...

 private void StartDrag(MouseEventArgs e)
        {
            IsDragging = true;
            DataObject data = new DataObject(System.Windows.DataFormats.Text.ToString(), "abcd");
            DragDropEffects de = DragDrop.DoDragDrop(this.DragSource, data, DragDropEffects.Move);
            IsDragging = false;
        }

Note: this code allows you to drag & drop across processes, it uses the default operating system feedback ( e.g. + for copy).. 

 

Next scenario: We want a pre-defined custom cursor...    

Say we had a .cur file and embedded it on to our application as a resource ( see sample code).   We can subscribe to GiveFeedback () and wire our cursor there..

private void StartDragCustomCursor(MouseEventArgs e)
        {

            GiveFeedbackEventHandler handler = new GiveFeedbackEventHandler(DragSource_GiveFeedback);
            this.DragSource.GiveFeedback += handler; 
            IsDragging = true;
            DataObject data = new DataObject(System.Windows.DataFormats.Text.ToString(), "abcd");
            DragDropEffects de = DragDrop.DoDragDrop(this.DragSource, data, DragDropEffects.Move);
            this.DragSource.GiveFeedback -= handler; 
            IsDragging = false;
        }

Our handler for feedback looks like this:

void DragSource_GiveFeedback(object sender, GiveFeedbackEventArgs e)
        {
                try
                {
                    //This loads the cursor from a stream .. 
                    if (_allOpsCursor == null)
                    {
                        using (Stream cursorStream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(
"SimplestDragDrop.DDIcon.cur")) { _allOpsCursor = new Cursor(cursorStream); } } Mouse.SetCursor(_allOpsCursor); e.UseDefaultCursors = false; e.Handled = true; } finally { } }

Two things to notice: 
1) I cached the cursor...  GiveFeedback will be called many times  as the mousemoves so I cached it..  and

2) though I did not handle it, I called it "_allOpsCursor" because GiveFeedbackEventArgs will tell you the possible operation for the cursor (e.Effects)...  I could have used multiple cursors, one for each effect.

 

Next scenario: Getting fancy and using the Visual we are dragging for feedback [instead of a cursor]

The first thing you will need is to an Adorner; in my case I chose and adorner that contains a VisualBrush of the Element being dragged...      you can go with RenderTargetBitmap, or possibly reparent the object directly ... but I like VisualBrush in case the drag is cancelled..  

The constructor for the adorner class is where most of the action happens:

public DragAdorner(UIElement owner, UIElement adornElement, bool useVisualBrush, double opacity)
            : base(owner)
        {
            _owner = owner;
            if (useVisualBrush)
            {
                VisualBrush _brush = new VisualBrush(adornElement);
                _brush.Opacity = opacity;
                Rectangle r = new Rectangle();
                r.RadiusX = 3;
                r.RadiusY = 3;
                r.Width = adornElement.DesiredSize.Width;
                r.Height = adornElement.DesiredSize.Height;

                XCenter = adornElement.DesiredSize.Width / 2;
                YCenter = adornElement.DesiredSize.Height / 2;

                r.Fill = _brush;
                _child = r;

            }
            else
                _child = adornElement;

        }

 //There is more code in DragAdorner, but mostly used for positioning the adorner as the drag is happening... please refer to the sample...

 

Now, that we have our custom adorner ready, the tricky part is wiring it so it follows the cursor position.  There are two options here:

  • If we want drag & drop across apps, we are going to have to call Win32's GetCursorPos () ...   This is trivial to write but requires full-trust ...  (which you likely had if you needed to drag & drop with other apps anyway )...
  • If we want to drag & drop inside our app only or inside a specific 'scope' with in the app, there is a hucky workaround that I often use to avoid the interop code..

 

Using Visual for Feedback.1 : D&D across apps using GetCursorPos () ... 

First we have to import Win32's code using DllImport ....  [trivial stuff, refer to sample code in Win32.cs ]

Next we create an instance of a Window, which will contain a visual brush of the element we are dragging ... 

private Window _dragdropWindow = null;
        private void CreateDragDropWindow(Visual dragElement)
        {
            System.Diagnostics.Debug.Assert(this._dragdropWindow == null);
            System.Diagnostics.Debug.Assert(dragElement != null);
            // TODO: FE? or UIE??   FE cause I am lazy on size . 
            System.Diagnostics.Debug.Assert(dragElement is FrameworkElement); 

            this._dragdropWindow = new Window();
            _dragdropWindow.WindowStyle = WindowStyle.None;
            _dragdropWindow.AllowsTransparency = true;
            _dragdropWindow.AllowDrop = false;
            _dragdropWindow.Background = null;
            _dragdropWindow.IsHitTestVisible = false;
            _dragdropWindow.SizeToContent = SizeToContent.WidthAndHeight;
            _dragdropWindow.Topmost = true;
            _dragdropWindow.ShowInTaskbar = false;

            _dragdropWindow.SourceInitialized += new EventHandler(
            delegate(object sender, EventArgs args)
            {

                //TODO assert that we can do this.. 
                PresentationSource windowSource = PresentationSource.FromVisual(this._dragdropWindow);
                IntPtr handle = ((System.Windows.Interop.HwndSource)windowSource).Handle;

                Int32 styles = Win32.GetWindowLong(handle, Win32.GWL_EXSTYLE);
                Win32.SetWindowLong(handle, Win32.GWL_EXSTYLE, 
styles | Win32.WS_EX_LAYERED | Win32.WS_EX_TRANSPARENT); }); Rectangle r = new Rectangle(); r.Width = ((FrameworkElement)dragElement).ActualWidth; r.Height = ((FrameworkElement)dragElement).ActualHeight; r.Fill = new VisualBrush(dragElement); this._dragdropWindow.Content = r; // put the window in the right place to start UpdateWindowLocation(); }

Notice:

1) I set the style to Transparent, layered window (this is ok since the window is small and it is only used for drag & drop )..  and 

2) the call to UpdateWindowLocation () this is the code that positions the Window wherever the cursor is now..

3) I likely need more error checking  

The code in UpdateWindowLocation is straight forward:

 void UpdateWindowLocation()
        {
            if (this._dragdropWindow != null)
            {
                Win32.POINT p;
                if (!Win32.GetCursorPos(out p))
                {
                    return;
                }
                this._dragdropWindow.Left = (double)p.X;
                this._dragdropWindow.Top = (double)p.Y;
            }
        }

This UpdateLocation code of course needs to be called whenever the cursor moves...  so we need some kind of callback during the drag operation.. We will use QueryContinueDrag for that..

So, I go back to  the code in StartDrag ()  and wire up the event, as well as some code to show the window and destroy it after drag & drop:

 private void StartDragWindow(MouseEventArgs e)
        {

            GiveFeedbackEventHandler feedbackhandler = new GiveFeedbackEventHandler(DragSource_GiveFeedback); ;
            this.DragSource.GiveFeedback += feedbackhandler; 
            QueryContinueDragEventHandler queryhandler = new QueryContinueDragEventHandler(DragSource_QueryContinueDrag);
            this.DragSource.QueryContinueDrag += queryhandler; 
            IsDragging = true;
            CreateDragDropWindow(this.dragElement); 
            DataObject data = new DataObject(System.Windows.DataFormats.Text.ToString(), "abcd");
            this._dragdropWindow.Show(); 
            DragDropEffects de = DragDrop.DoDragDrop(this.DragSource, data, DragDropEffects.Move);
            DestroyDragDropWindow(); 
            IsDragging = false;
            this.DragSource.GiveFeedback -= feedbackhandler;
            this.DragSource.QueryContinueDrag -= queryhandler; 
        }

The one thing to notice is that I still have GiveFeedbackHandler wired.. Why ?? We are no longer using the cursor...  but we still have to tell Drag & Drop not to use the default cursors..

 

Using Visual for Feedback.2: Using DragOver to avoid the interop code and/or to limit dragging scope to my app.. 

There is a slightly different approach you can use if you are drag & dropping just inside your app or have a smaller scope ... I some times use this approach because it allows me to avoid interop, avoid creating extra windows, and better control the scope of the drag...  


Here is the full explanation of how it works and why it feels like hackalicious.

When you call DoDragDrop, there is no Mouse or Cursor Events being fired in your WPF app...  OLE does the work for you and it moves cursor directly :(...  however, all of the Drag events are being fired... 

We already know of the two events we can tap into from the source: GiveFeedback and QueryContinueDrag...    however neither of these events gives us access to the mouse or cursor position during the drag operation :( ...   We can however tap into the  Dragover  event; DragOverEventArgs has a GetPosition ( ) method that does the trick...    DragOver however is fired in the target, not the source.

So, how would we do it??  Well , DragEvents are routed events.. they bubble up.. if we define a "Drag Scope"  within our app that we know is guaranteed to bubble the DragOver, then we can listen for it ...   the obvious choice for that scope is our Application's Window; this gives us access to any thing in our app; the scope could be smaller of course... 


Here is how we wire that: 

private void StartDragInProcAdorner(MouseEventArgs e)
        {

            // Let's define our DragScope .. In this case it is every thing inside our main window .. 
            DragScope = Application.Current.MainWindow.Content as FrameworkElement;
            System.Diagnostics.Debug.Assert(DragScope != null);

            // We enable Drag & Drop in our scope ...  We are not implementing Drop, so it is OK, but this allows us to get DragOver 
            bool previousDrop = DragScope.AllowDrop;
            DragScope.AllowDrop = true;            

            // Let's wire our usual events.. 
            // GiveFeedback just tells it to use no standard cursors..  

            GiveFeedbackEventHandler feedbackhandler = new GiveFeedbackEventHandler(DragSource_GiveFeedback);
            this.DragSource.GiveFeedback += feedbackhandler;

            // The DragOver event ... 
            DragEventHandler draghandler = new DragEventHandler(Window1_DragOver);
            DragScope.PreviewDragOver += draghandler; 

            // Drag Leave is optional, but write up explains why I like it .. 
            DragEventHandler dragleavehandler = new DragEventHandler(DragScope_DragLeave);
            DragScope.DragLeave += dragleavehandler; 

            // QueryContinue Drag goes with drag leave... 
            QueryContinueDragEventHandler queryhandler = new QueryContinueDragEventHandler(DragScope_QueryContinueDrag);
            DragScope.QueryContinueDrag += queryhandler; 

            //Here we create our adorner.. 
            _adorner = new DragAdorner(DragScope, (UIElement)this.dragElement, true, 0.5);
            _layer = AdornerLayer.GetAdornerLayer(DragScope as Visual);
            _layer.Add(_adorner);


            IsDragging = true;
            _dragHasLeftScope = false; 
            //Finally lets drag drop 
            DataObject data = new DataObject(System.Windows.DataFormats.Text.ToString(), "abcd");
            DragDropEffects de = DragDrop.DoDragDrop(this.DragSource, data, DragDropEffects.Move);

             // Clean up our mess :) 
            DragScope.AllowDrop = previousDrop;
            AdornerLayer.GetAdornerLayer(DragScope).Remove(_adorner);
            _adorner = null;

            DragSource.GiveFeedback -= feedbackhandler;
            DragScope.DragLeave -= dragleavehandler;
            DragScope.QueryContinueDrag -= queryhandler;
            DragScope.PreviewDragOver -= draghandler;  

            IsDragging = false;
        }
  

Explanations:

  • GiveFeedback is the same than before we use it to set no default cursor ..
  • Dragover on our DragScope  is what will let us move the cursor around..  These events are wired in the Drop target, not in the source control..
  • DragLeave is optional; the reason I wired it is because when the mouse leaves the scope, I want to cancel the Drag operation altogether, nix it!  So I subscribe to DragLeave to know when mouse left.. Unfortunately, I can't cancel the drag in DragLeave, so I set a flag to be read in QueryContinueHandler. QCH reads this flag and when set to true,  it sets the Action to Cancel in the drag to nix it..
  • The rest is creating our adorner, and the drag drop ..  plus all the clean up ... 

There is a common gotcha with the DragLeave part of this scenario. The scope tends to always be a panel,grid, etc.. ( a container) and if the container has background to null, it is not hittesting, so you won't get the dragleave...   You have to explicitly set the Background="Transparent" to make sure you get it...   (you can see it in my sample with the Grid)..

That is it for Drag ...   I hope I explained how to do the Drag part  of a drag  & drop.     I want to cut part 1 here so that you have a pretty clean sample of the "drag" .. 

The source for every thing above is here.   

 

You will have to tweak the MouseMove function to select which drag approach to use.. Just make sure you have at most one of these functions uncommented at any time..

// StartDrag(e);
//  StartDragCustomCursor(e);
// StartDragWindow(e);
StartDragInProcAdorner(e);

 

Since I did not wire a Drop handler, for testing this, just "Drop" into some thing that handles Text like Microsoft Word..

In part 2, I will cover the drop .. and in part 3 I will share the complete code with a couple of extra things that I omitted here to try to keep it clean  (some of them might be in the code sample)

  • Cookieless Session with ASP.NET Ajax and Web Services [Via: derek ] Displaying Extended Details in a...

  • Great stuff. I found the Using Visual for Feedback.1 : D&D across apps using GetCursorPos() section helpful. To achieve the same goal as you did with your helper classes, I've created custom controls that wrap others and encapsulate the drag and drop functionality so you can implement it declaratively. If you're interested I can send you the code and a sample -- maybe you could give me some feedback.

  • Wow, that's really a complete explanation. I wish I'd had that back then :) But we had some code... :)

    On the D&D across apps... would you think it could work with Popup-Controls? That might help get around interop... not sure if there's a caveat or not.

    cheers

    Florian

  • Dr Tim Sneath gives a overview of what is new in WPF 3.5 http://blogs.msdn.com/tims/archive/2007/07/27

  • Apologies for belated reply.. I was on vacation..

    Florian, I have not tried Popup; technically it is same than window, in terms of it requiring FullTrust..  

    Another possible disadvantage of Popup is the slightly less control you will have when creating the window (or Popup) itself.. I doubt you can set the right style.. you could doing interop(SetWindowx)  of course..

    Let me know how it goes if you try Popup.

  • 本文讲述了在一个ItemsControl内部,ListView内部,以及两者之间互相拖动item的实现。涉及到了DataTemplate、Style、Adorner、ContentPresenter、...

Page 1 of 1 (6 items)