Drag & Drop in WPF.. part 3 .. the results and code...

Here is where we take every thing learned in Part1 (the drag) and Part2  (the drop)  and package it into more realistic scenarios...  

Let's first begin by walking through a few items we skipped in earlier parts for brevity:

Detecting DragDropEffects...

When you start a drag, you should check if Ctrl  or Shift  are pressed...    Each one of these being pressed implies a different action for our drag operation...

  DragDropEffects effects = DragDropEffects.None;bool ctrl = Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);bool shift = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift);if (ctrl && shift && AllowsLink)        effects |= DragDropEffects.Link;else if (ctrl && AllowsCopy)       effects |= DragDropEffects.Copy;else if (AllowsMove)  // the default is to move in my sample ..        effects |= DragDropEffects.Move;

 

Detecting what needs to be dragged:

Deciding what needs to be dragged is sucky because you almost always have to put app knowledge into your drag & drop code..   There is not a one size fits all..

For example:  <Button Content="Test" />    will generate a tree that looks like:     Button - > Chrome - > Content Presenter - > TextBlock   ...
So, if you hitTest on what is visible, you might get the  TextBlock  when what you wanted was the button ....

I don't have a great answer for this. As I mentioned, my approach is to lazily shoot for the OriginalSource element in the MouseMove event handler..   This seems to work slightly better than doing HitTesting (where I could get an item lower in the tree like the TextBlock ..

For those that do want to do hittesting, the code is trivial:

  UIElement GetDragElementFromHitTest(UIElement dragSourceContainer, MouseEventArgs args)        {            HitTestResult hr = VisualTreeHelper.HitTest( dragSourceContainer, args.GetPosition((IInputElement) dragSourceContainer));            return hr.VisualHit as UIElement;        }

 

Doing Drag & drop should be about data:

I have to emphasize d&d was meant for data..   In the demo below I make the mistake of letting you drag UIElements and the like.. This is for illustration purposes... imho if you find yourself trying to drag any thing other than data, you should evaluate decoupling presentation and data.. 

 

Now onto a real sample...  

What I have done for my demos is create a library with:

  • DragHelper class:  given a source  (any UIElement) will subscribe to handle the required events to initiate a Drag& Drop operation.
  • DropHelper class: given a target ( any UIElement) will set properties needed  & subscribe to required events to complete a Drop operation...
  • DragAdorner -- same as previous examples, the class to draw the Adorner while element drags..

 

I have also defined an interface that a class can implement to aid in the dragOperation...  

 
     public interface IDataDropObjectProvider    {        //Flag of actions sypported by implementation of  IDataDropObjectProvider        DragDropProviderActions SupportedActions { get; }        // Called before StartDrag () to get the Data () to be used in the DataObject         object GetData(MouseEventArgs e);        // Called to get the visual ( UIElement visual brush of the object being dragged..         UIElement GetVisual(MouseEventArgs e);        // Gives feedback during Drag         void GiveFeedback(GiveFeedbackEventArgs args);        // implements ContinueDrag -- to canceld the D&D..         void ContinueDrag(QueryContinueDragEventArgs args);        // called by the TARGET object .. this will attempt to "unparent" the current child so we can add it a child some where else..         bool UnParent();    } 
 This interface gets called back from either DragHelper or Drophelper  (whenever needed)
 A few things to notice: 
  •  the interface is not necessarily implemented by either the source or the target. it is de-coupled from both.  Any other class can implement it and it can be generic. 
    
  •  The interface is optional, if you do not implement it, the helpers classes will do their best to help..  My advise is always implement the GetData ( ) part of the interface. 
    
  •  You can implement just part of the interface.  DragDropProviderActions is a flag that says what actions the interface supports.. 
    
  

Wiring the 'helpers'

Is pretty trivial ...   the constructor for DragHelper looks like this:

   public DragHelper(UIElement source, IDataDropObjectProvider callback, UIElement dragScope)
   // source is the drag source control ...     // callback is the interface  optional, pass null if you don't want to implement for call backs to GetData () , GetVisual(), etc.   /*  dragScope is the hack we use if we are going to use DragOver...    Even though DragOver is a drop target event, we need to know about it in the drag ... because we have to wire up for it before calling DoDragDrop () ....   */

 

Here are examples of using the constructors...    ( DropHelper I did not explain above beccuse the only parameter in is the drop target control itself..

 dragHelper = new DragHelper(this.canvas, null, null);dropHelper = new DropHelper(this.dropTargetPanel1 );dragHelperWScope = new DragHelper(this.canvas1,  null, (UIElement)this.BorderForScope);dropHelperWScope = new DropHelper(this.dropTargetPanel2);

For the most part, these classes do what we discussed .. the exception is the drop..

Common drop issues:

Drop of data is straight forward...   there is no generic recipe for all scenarios but two that I use often are:  dropping to ContentControl or dropping to ItemsControl..  in which case I set appropriate property or add the data to the items collection ..

Drop of UIElements is slightly trickier mostly because you need to re-parent your element to the target.. .  The sample classes use the IDataDropObjectProvider to Unparent () the UIElement before dropping it..

Note that this assumes every thing is in process, the interface passes UIElements and other stuff around ...     To implement an Out of process handler for passing UI, you can make COM objects and pass that around..

 

The sample

Is not pretty ...  but quite functional. .. Here is the first half ..  

image The app shows the two approaches: The Top grid  allows you to drag the rectangle and button any where you want... If you drag in the app to the TargetPanel, it will drag as UIElement If you drag in the app to the RichTextBox, it will drag as Data (Text, hardcoded "abcd" ).. You can also use that to drag out of process ( Word is that I used)... You can drag Text from word into the RichTextbox  (duh ) and into the Target Panel ( Creates a TextBlock for you ) ... The bottom grid, does the same than top, but won't let you drag out of the Red border's scope...  It how ever does not limit the drop, so you can drop from any where ( word, or top grid)...

 

The second half of the app is even uglier, it is in Window2.xaml  but it is a common scenario...

image

This does drag & drop across listboxes.. it uses Data, but provides a Visual Adorner for feedback... The things to notice is that it implements the IDataDropObjectProvider interface ... so you can see how that works.. You can click one of the planets and drag into Word or other external app that allows for text drops to get the XML representation of the planet..

The listbox that you are dropping into does not allow duplicates.. so don't try dropping same thing twice..  it will quietly smirk at you..

 

OK that covers my D&D walk through ...  The source for the sample in part 3 is here..

{let me know if you find bugs or issues }