WPF “Drag and Drop” – doing it the MVVM way

I have been recently involved in a WPF/Prism project where one of the requirements was to allow the user to move rather large sums of money with the mouse. Think of moving the money from one account to another but with potential of serious loss if you drop it in between. When I trawled the net for examples of drag & drop in WPF, I noticed that the vast majority of them deal with the visual aspects of the operation, mixing business logic with the code behind which in turn results in code which is pretty much un-testable (in a unit testing sense). If  you take a closer look at any of the drag and drop code, it becomes obvious that the operation has a number of distinct aspects to it, some of which can be delegated to the “business logic component” offering potentially better testability:

  1. Drag and drop is initiated when the user presses the mouse and moves it by a certain distance. This requires event handling somewhere in the code behind. This code has to be potentially repeated over and over again for different controls.
  2. At the time the drag and drop is initiated, we hand over the mouse handling to the OS, but we still need to provide the data and we need to indicate what can be done with the object (Move, Copy etc). This would be best handled by the view model.
  3. While the mouse is being dragged we may need to provide some visual feedback as to what is going to happen when the object will get dropped. This again would be best handled by the view model but needs to be initiated from an event handler.
  4. Once the object is dropped, we just need to consume it but the operation requires Drop event handler in the code behind.

After a bit of head scratching and various discussions with John, I managed to come up wit ha solution which allows the drag and drop logic to be both reusable and testable. The main actors are as follows:

  1. DragSourceBehaviour  implements the event handling required to initiate the drag operation
  2. An object implementing IDragSource interface provides the data to be dragged
  3. DropTargetBehaviour implements drop related event handlers
  4. Object implementing IDropTarget handles the business logic of the “drop”
  5. Helper classes provide shortcuts for implementing both IDropTarget and IDragSource

The rest of this post discusses details of the implementation.

Handling the “Drag”

The start the drag & drop we need two event handlers: one to handle PreviewMouseButtonDown event and record the position and another one to handle PreviewMouseMove to see if the mouse have moved far enough to initiate drag & drop. Implementing those handlers over and over again in the code behind is not my idea of fun, so obviously another solution is required and a WPF behaviour fits the bill nicely. The drag and drop operation also needs a piece of data that will be dragged and we need to know what sort of drag operation will be supported: as indicated earlier this would be best handled by the view model.

To glue the event handling aspects and the data handling aspect together, I came up with the attached property of IDragSource type exposed by the DragSourceBehaviour. Once the property is set to a non null value, the behaviour will take care of the event handling while still delegating the task of providing data to implementation of IDragSource. I hope the following fragment from the DragSourceBehaviour class explains it all:

   1:  private static void PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
   2:  {
   3:      _startPoint = e.GetPosition(null);
   4:  }
   5:   
   6:  private static void MouseLeave(object sender, MouseEventArgs e)
   7:  {
   8:      // Need to reset since the mouse left in order to prevent mouse movement 
   9:      // in another element to pick drag an drop
  10:      _startPoint = null;
  11:  }
  12:   
  13:  private static void PreviewMouseMove(object sender, MouseEventArgs e)
  14:  {
  15:      if (e.LeftButton != MouseButtonState.Pressed || _startPoint == null)
  16:          return;
  17:   
  18:      if(!HasMouseMovedFarEnough(e))
  19:          return;
  20:   
  21:      var dependencyObject = (FrameworkElement) sender;
  22:      var dataContext = dependencyObject.GetValue(FrameworkElement.DataContextProperty);
  23:      var dragSource = GetDragSource(dependencyObject);
  24:   
  25:      if (dragSource.GetDragEffects(dataContext) == DragDropEffects.None)
  26:          return;
  27:   
  28:      DragDrop.DoDragDrop(dependencyObject,
  29:                          dragSource.GetData(dataContext),
  30:                          dragSource.GetDragEffects(dataContext));
  31:              
  32:  }
  33:   

 

The object implementing IDragSource interface does not have to be in any sense “related” to the visual initiating the operation, but whoever implements the IDragSource would be often interested in what is the data context of the object and for this reason IDragSource takes object “dataContext” parameter to both of it’s methods

   1:      /// <summary>
   2:      /// Business end of the drag source
   3:      /// </summary>
   4:      public interface IDragSource
   5:      {
   6:          /// <summary>
   7:          /// Gets the supported drop effects.
   8:          /// </summary>
   9:          /// <param name="dataContext">The data context.</param>
  10:          /// <returns></returns>
  11:          DragDropEffects GetDragEffects(object dataContext);
  12:   
  13:          /// <summary>
  14:          /// Gets the data.
  15:          /// </summary>
  16:          /// <param name="dataContext">The data context.</param>
  17:          /// <returns></returns>
  18:          object GetData(object dataContext);
  19:      }

Implementing IDragSource interface over and over again may become tedious very quickly so I came up wit ha shortcut for implementing the interface which simply takes two delegates to be executes as and when required:

   1:  /// <summary>
   2:  /// Gets the (drag) source of cookies.
   3:  /// </summary>
   4:  /// <value>The source of cookies.</value>
   5:  public IDragSource SourceOfCookies
   6:  {
   7:      get
   8:      {
   9:          if (_source == null)
  10:              _source = new DragSource<CookieJar>(GetDragEffects, GetData);
  11:   
  12:          return _source;
  13:      }
 

Handling the “Drop”

The drop operation is being handled in a similar fashion. This time however the attached property is of type IDropTarget and here’s how it is defined.

   1:  public interface IDropTarget
   2:  {
   3:      /// <summary>
   4:      /// Gets the effects.
   5:      /// </summary>
   6:      /// <param name="dataObject">The data object.</param>
   7:      /// <returns></returns>
   8:      DragDropEffects GetDropEffects(IDataObject dataObject);
   9:   
  10:      /// <summary>
  11:      /// Drops the specified data object
  12:      /// </summary>
  13:      /// <param name="dataObject">The data object.</param>
  14:      void Drop(IDataObject dataObject);
  15:  }

Similarly the code in the DropTargetBehaviour class delegates the task of handling the data to the object implementing IDropTarget:

   1:  private static void Drop(object sender, DragEventArgs e)
   2:  {
   3:      var dropTarget = GetDropTarget((DependencyObject)sender);
   4:   
   5:      dropTarget.Drop(e.Data);
   6:      e.Handled = true;
   7:  }
   8:   
   9:  private static void DragOver(object sender, DragEventArgs e)
  10:  {
  11:      var dropTarget = GetDropTarget((DependencyObject)sender);
  12:   
  13:      e.Effects = dropTarget.GetDropEffects(e.Data);
  14:      e.Handled = true;            
  15:  }

Putting it all together

To test the entire machinery I have developed a sample application which allows you to drag cookies between cookie jars. But there are constraints to it: you cannot drag anything out of an empty jar, and each jar will accept no mo re than ten cookies. These are the business rules which are enforced by corresponding unit tests.

image

The following fragment of XAML illustrates how the control is glued together with the DragSource and DropTarget behaviours:

<DragDrop:CookieJarControl Behaviours:DragSourceBehaviour.DragSource="{Binding SourceOfCookies}" 
                            Behaviours:DropTargetBehaviour.DropTarget="{Binding CookieSink}"
                            AllowDrop="True"/>

The source code for the entire project is available as part of the SharpFellows.Toolkit. As we develop more and more reusable goodies I am sure they will make it’s way into the library. Feel free to use it in any which way you want (this includes copy-pasting of suitable fragments) but please let us know if  you find it useful!

August 20 2010
blog comments powered by Disqus