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:
- 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.
- data, specifying the data to pass
- 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.
- 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.
- 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.
- 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)
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..
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 ...
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..
Our handler for feedback looks like this:
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:
//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 ...
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:
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)
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:
- 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" ..
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..
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)