Extending the old WPF drag/drop behavior

A while back I wrote a post of creating A WPF drag/drop target behavior (well really it’s a drop behavior). Let’s extend this and add keyboard paste capabilities and tie it into a view model.

Adding keyboard capabilities

I’ll list the full source at the end of this post, for now I’ll just show changes from my original post.

In the behavior’s OnAttached method add

AssociatedObject.PreviewKeyDown += AssociatedObjectOnKeyDown;

in the OnDetaching method add

AssociatedObject.PreviewKeyDown -= AssociatedObjectOnKeyDown;

the AssociatedObjectOnKeyDown method looks like this

private void AssociatedObjectOnKeyDown(object sender, KeyEventArgs e)
{
   if ((e.Key == Key.V && 
      (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control) ||
         (e.Key == Key.V) && (Keyboard.IsKeyDown(Key.LeftCtrl) || 
            Keyboard.IsKeyDown(Key.RightCtrl)))
   {
      var data = Clipboard.GetDataObject();
      if (CanAccept(sender, data))
      {
         Drop(sender, data);
      }
   }
}

Don’t worry about CanAccept and Drop at the moment. As you can see, we capture the preview key down events and if Ctrl+V is being pressed whilst the AssociatedObject has focus, we get the data object from the clipboard, then we want to check if our view model accepts the data, i.e. if we only accept CSV we can fail the paste if somebody tries to drag and image into the view, otherwise we call Drop, which our old has been refactored to also use.

Both the CanAccept and Drop methods need to call into the view model for it to decide whether to accept the data and upon accepting, how to use it, so first we need to define an interface our view model can implement which allows the behavior to call into it, here’s the IDropTarget

public interface IDropTarget
{
   bool CanAccept(object source, IDataObject data);
   void Drop(object source, IDataObject data);
}

Fairly obvious how this is going to work, the behavior will decode the clipbaord/drop event to an IDataObject. The source argument is for situations where we might be dragging from a listbox (for example) to another listbox and want access to the view model behind the drag source.

If we take a look at both the CanAccept method and Drop method on the behavior

private bool CanAccept(object sender, IDataObject data)
{
   var element = sender as FrameworkElement;
   if (element != null && element.DataContext != null)
   {
      var dropTarget = element.DataContext as IDropTarget;
      if (dropTarget != null)
      {
         if (dropTarget.CanAccept(data.GetData("DragSource"), data))
         {
            return true;
         }
      }
   }
   return false;
}

private void Drop(object sender, IDataObject data)
{
   var element = sender as FrameworkElement;
   if (element != null && element.DataContext != null)
   {
      var target = element.DataContext as IDropTarget;
      if (target != null)
      {
         target.Drop(data.GetData("DragSource"), data);
      }
   }
}

As you can see, in both cases we try to get the DataContext of the framework element that sent the event and if it is an IDropTarget we hand off CanAccept and Drop to it.

What’s the view model look like

So a simple view model (which just supplies a property Items of type ObservableCollection) is implemented below

public class SampleViewModel : IDropTarget
{
   public SampleViewModel()
   {
      Items = new ObservableCollection<string>();
   }

   bool IDropTarget.CanAccept(object source, IDataObject data)
   {
      return data?.GetData(DataFormats.CommaSeparatedValue) != null;
   }

   void IDropTarget.Drop(object source, IDataObject data)
   {
       var s = data?.GetData(DataFormats.CommaSeparatedValue) as string;
       if (s != null)
       {
           var split = s.Split(
              new [] { ',', '\r', '\n' }, 
                 StringSplitOptions.RemoveEmptyEntries);
           foreach (var item in split)
           {
              if (!String.IsNullOrEmpty(item))
              {
                 Items.Add(item);
              }
           }
       }
    }

    public ObservableCollection<string> Items { get; private set; }
}

In the above we only accept CSV data, the drop method is very simple and just splits the string into separate parts, each of which is then added to the Items collection.

our XAML (using a Listbox for demo) looks like this

<ListBox ItemsSource="{Binding Items}" x:Name="List">
   <i:Interaction.Behaviors>
      <local:UIElementDropBehavior />
   </i:Interaction.Behaviors>
</ListBox>

Note: the x:Name is here because in MainWindow.xaml.cs (hosting this control) we needed to force focus onto the listbox at startup. Otherwise the control, when empty doesn’t seem to get focus for the keyboard events. Ocourse we might look to use a Focus Behavior

The UIElementDropBehavior in full

public class UIElementDropBehavior : Behavior<UIElement>
{
    private AdornerManager _adornerManager;

    protected override void OnAttached()
    {
        base.OnAttached();

        AssociatedObject.AllowDrop = true;
        AssociatedObject.DragEnter += AssociatedObject_DragEnter;
        AssociatedObject.DragOver += AssociatedObject_DragOver;
        AssociatedObject.DragLeave += AssociatedObject_DragLeave;
        AssociatedObject.Drop += AssociatedObject_Drop;
        AssociatedObject.PreviewKeyDown += AssociatedObjectOnKeyDown;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();

        AssociatedObject.AllowDrop = false;
        AssociatedObject.DragEnter -= AssociatedObject_DragEnter;
        AssociatedObject.DragOver -= AssociatedObject_DragOver;
        AssociatedObject.DragLeave -= AssociatedObject_DragLeave;
        AssociatedObject.Drop -= AssociatedObject_Drop;
        AssociatedObject.PreviewKeyDown -= AssociatedObjectOnKeyDown;
    }

    private void AssociatedObjectOnKeyDown(object sender, KeyEventArgs e)
    {
        if ((e.Key == Key.V && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control) ||
            (e.Key == Key.V) && (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)))
        {
            var data = Clipboard.GetDataObject();
            if (CanAccept(sender, data))
            {
                Drop(sender, data);
            }
        }
    }

    private void AssociatedObject_Drop(object sender, DragEventArgs e)
    {
        if (CanAccept(sender, e.Data))
        {
            Drop(sender, e.Data);
        }

        if (_adornerManager != null)
        {
            _adornerManager.Remove();
        }
        e.Handled = true;
    }

    private void AssociatedObject_DragLeave(object sender, DragEventArgs e)
    {
        if (_adornerManager != null)
        {
            var inputElement = sender as IInputElement;
            if (inputElement != null)
            {
                var pt = e.GetPosition(inputElement);

                var element = sender as UIElement;
                if (element != null)
                {
                    if (!pt.Within(element.RenderSize) || e.KeyStates == DragDropKeyStates.None)
                    {
                        _adornerManager.Remove();
                    }
                }
            }
        }
        e.Handled = true;
    }

    private void AssociatedObject_DragOver(object sender, DragEventArgs e)
    {
        if (CanAccept(sender, e.Data))
        {
            e.Effects = DragDropEffects.Copy;

            if (_adornerManager != null)
            {
                var element = sender as UIElement;
                if (element != null)
                {
                    _adornerManager.Update(element);
                }
            }
        }
        else
        {
            e.Effects = DragDropEffects.None;
        }
        e.Handled = true;
    }

    private void AssociatedObject_DragEnter(object sender, DragEventArgs e)
    {
        if (_adornerManager == null)
        {
            var element = sender as UIElement;
            if (element != null)
            {
                _adornerManager = new AdornerManager(AdornerLayer.GetAdornerLayer(element), adornedElement => new UIElementDropAdorner(adornedElement));
            }
        }
        e.Handled = true;
    }

    private bool CanAccept(object sender, IDataObject data)
    {
        var element = sender as FrameworkElement;
        if (element != null && element.DataContext != null)
        {
            var dropTarget = element.DataContext as IDropTarget;
            if (dropTarget != null)
            {
                if (dropTarget.CanAccept(data.GetData("DragSource"), data))
                {
                    return true;
                }
            }
        }
        return false;
    }

    private void Drop(object sender, IDataObject data)
    {
        var element = sender as FrameworkElement;
        if (element != null && element.DataContext != null)
        {
            var target = element.DataContext as IDropTarget;
            if (target != null)
            {
                target.Drop(data.GetData("DragSource"), data);
            }
        }
    }
}

Sample Code

DragAndDropBehaviorWithPaste