WPF DataGrid Drag & Drop Behavior

Recently I was tasked with getting Drag & Drop behavior implemented within a WPF application that I was working on.  I found some excellent posts and code samples walking through the process, and I felt that it should be fairly straightforward to wrap the resulting code into a Behavior in a way that allows it to be used on any DataGrid with minimal code.  The application in question utilizes the MVVM Light Toolkit. and we have been striving to adhere pretty closely to the ‘no code in the view’ mantra throughout the application.

The resulting code is fairly straightforward, the consuming ViewModel requires only a handler method to do the actual manipulation of the underlying model data.  This leaves the code compartmentalized and testable.  The code attached at the end of the post includes the full source and a working demo application.  Let’s pull apart the pieces to see what’s in there.  We’ll start at the ‘top’ and drill in from there.

The View

The entire concept of Behaviors tries to abstract away potentially complicated functionality into basic XAML blocks that can be fed by binding information, removing the necessity of wrapping that logic into the code-behind of the view and making it easier to reuse.  In this case, the view code can’t get much simpler:

<Window x:Class="VirtualOlympus.WPF.Behaviors.UI.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:Behaviors="clr-namespace:VirtualOlympus.Behaviors;assembly=VirtualOlympus.WPF.Behaviors"
        xmlns:local="clr-namespace:VirtualOlympus.WPF.Behaviors.UI"
        Title="MainWindow" Height="350" Width="525">

    <Window.Resources>
        <local:MainWindowViewModel x:Key="vm" />
    </Window.Resources>

    <Grid x:Name="LayoutRoot" DataContext="{StaticResource vm}">
        <DataGrid ItemsSource="{Binding Path=Items}" RowHeaderWidth="10" AllowDrop="True">
            <i:Interaction.Behaviors>
                <Behaviors:DataGridDragDropBehavior AllowedEffects="Copy, Move" Command="{Binding Path=ItemsDragDropCommand}" />
            </i:Interaction.Behaviors>
        </DataGrid>
    </Grid>
</Window>

I’ll just call out three quick items:

  1. The Behavior includes two arguments, AllowedEffects, which controls what types of operations the drag operation can initiate, and the Command to be executed when the item is dropped.
  2. The use of the ‘System.Windows.Interactivity’ namespace (xmlns:i) from Expression Blend, which is helpfully provided by the MVVM Light Toolkit in case you don’t have access to Blend directly.
  3. The setting of ‘AllowDrop’ on the DataGrid [default: False] to let us actually drop items onto this DataGrid.

 

The ViewModel

Drilling down a level, we look at the underlying ViewModel that supports the drop operation.  In order to accept the drop, the ViewModel must implement a RelayCommand command property.  The RelayCommand<> class is defined as part of the MVVM Light Toolkit, and really makes exposing commands on your ViewModel a breeze.  The DataGridDragDropEventArgs class is defined as the following:

    public class DataGridDragDropEventArgs : EventArgs
    {
        public object Source { get; internal set; }
        public object Destination { get; internal set; }

        public object DroppedObject { get; internal set; }
        public object TargetObject { get; internal set; }

        public DataGridDragDropDirection Direction { get; internal set; }
        public DragDropEffects Effects { get; internal set; }
    }

The class exposed properties for the source and destination collections, the item being dropped and the item it is dropped onto.  Also included are the desired operation [using DragDropEffects as a convenience] and the general direction the mouse was moving when the drop occurred.  The consuming ViewModel then makes decisions based on this information and adjusts the underlying collections accordingly.  In the case of the example handler here, it is assumed that the source and destination collections are the same, but there’s really nothing in the behavior that requires that to be so. The command invokes a matching method to do the actual drop handling, and the CanExecute property makes sure that the command is only enabled if there’s something to drop and some place to drop it. 

    public class MainWindowViewModel : ViewModelBase
    {
        public class DemoClass
        {
            public DemoClass(string field1, string field2, string field3)
            {
                Field1 = field1;
                Field2 = field2;
                Field3 = field3;
            }

            public string Field1 { get; set; }
            public string Field2 { get; set; }
            public string Field3 { get; set; }

            public DemoClass Clone()
            {
                return new DemoClass(this.Field1, this.Field2, this.Field3);
            }
        }

        public MainWindowViewModel()
        {
            Items = new ObservableCollection<DemoClass>(new DemoClass[] {
                new DemoClass("Item 1.1", "Item 1.2", "Item 1.3"),
                new DemoClass("Item 2.1", "Item 2.2", "Item 2.3"),
                new DemoClass("Item 3.1", "Item 3.2", "Item 3.3"),
                new DemoClass("Item 4.1", "Item 4.2", "Item 4.3"),
                new DemoClass("Item 5.1", "Item 5.2", "Item 5.3"),
                new DemoClass("Item 6.1", "Item 6.2", "Item 6.3"),
            });

            ItemsDragDropCommand = new RelayCommand<DataGridDragDropEventArgs>(
                                       (args) => DragDropItem(args),
                                       (args) => args != null &&
                                                 args.TargetObject != null &&
                                                 args.DroppedObject != null &&
                                                 args.Effects != System.Windows.DragDropEffects.None);
        }

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

        public RelayCommand<DataGridDragDropEventArgs> ItemsDragDropCommand { get; private set; }

        public void DragDropItem(DataGridDragDropEventArgs args)
        {
            int targetIndex = Items.IndexOf((DemoClass)args.TargetObject);
            if (args.Direction == DataGridDragDropDirection.Down) targetIndex++;

            if (args.Effects == System.Windows.DragDropEffects.Move)
            {
                int sourceIndex = Items.IndexOf((DemoClass)args.DroppedObject);
                if (sourceIndex < targetIndex) targetIndex--;
                Items.Remove((DemoClass)args.DroppedObject);

                Items.Insert(targetIndex, (DemoClass)args.DroppedObject);
            }
            else if (args.Effects == System.Windows.DragDropEffects.Copy)
            {
                Items.Insert(targetIndex, ((DemoClass)args.DroppedObject).Clone());
            }
        }
    }

There’s nothing very fancy [or necessarily bug free] about the above code.  It also does not reference any UI specific information, and so remains relatively easy to test.  I’ll leave the exercise of actually writing unit tests for the methods as an exercise for the reader.

 

The Behavior

We’ve seen the View and the ViewModel, but what glues everything together into a cohesive whole?  The glue is provided by the DataGridDragDropBehavior class.  This class inherits from the base Behavior<> class provided by the Blend interactivity libraries and implements the OnAttached() method to hook into the DataGrid object's events.

The example code utilizes the WeakEvent pattern to avoid having the behavior keep objects from hanging around longer than need be, and utilizes the pattern pretty much straight from the blog posts here and here.  The details of the pattern are beyond the scope of this post, but I strongly suggest you read through them.  I’m not using the WPF WeakEventManager class here, because I hope at some point to port this to Silverlight, and there doesn't appear to be any direct analog.

The resulting code looks pretty much like the code samples, so I won’t call it out here, but please download the project file and take a look.  The only real difference from the samples is in the MouseMove handler that fires the drag & drop operation.  Once the drag operation completes, a DataGridDragDropEventArgs instance is created and the bound command invoked:

    private void DataGrid_MouseMove(object sender, MouseEventArgs e)
    {
        if (e.LeftButton == MouseButtonState.Pressed)
        {
            DataGridRow row = UIHelper.FindVisualParent<DataGridRow>(e.OriginalSource as FrameworkElement);
            if (row != null && row.IsSelected)
            {
                _source = UIHelper.FindVisualParent<DataGrid>(row).ItemsSource;
                _itemToMove = row.Item;
                DragDropEffects finalEffects = DragDrop.DoDragDrop(row, _itemToMove, AllowedEffects);
                DataGridDragDropEventArgs args = new DataGridDragDropEventArgs()
                {
                    Destination = _destination,
                    Direction = _direction,
                    DroppedObject = _itemToMove,
                    Effects = finalEffects,
                    Source = _source,
                    TargetObject = _dropTarget
                };

                if (_dropTarget != null && Command != null && Command.CanExecute(args))
                {
                    Command.Execute(args);

                    _itemToMove = null;
                    _dropTarget = null;
                    _source = null;
                    _destination = null;
                    _direction = DataGridDragDropDirection.Indeterminate;
                    _lastIndex = -1;
                }
            }
        }
    }

Here you can see we’re grabbing the original source collection and item, performing the drag & drop [which handles setting destination information] and then passing these items to the bound command if the command is valid to execute.  Once the command execution completes, we reset our state and are ready to handle the next event.

 

Conclusion

This has been a pretty quick-and-dirty run through the behavior creation process, but I’m pretty pleased with the results vs. effort ratio.  I can now very easily add drag & drop to my grids and have my ViewModels handle shuffling the data around without worrying about copying/pasting code into all my UI screens.  So what’s left?  Really I see a few issues that I’d like to tackle at some point:

  1. Port the Behavior to Silverlight – This is difficult, since the Silverlight MouseEventArgs class does not support retrieval of the current mouse button state, we would likely need to do something like capturing the mouse, or restrict the behavior to work with only a single data grid.
  2. Does not work well with composition – The behavior falls down to a certain extent if the ViewModel that is handling the drop event does not have access to the source and destination collections [e.g. dragging a row across two composed user controls].  In that case, the ViewModel may not know what to do with the incoming data other than add it to the destination collection.
  3. Runtime typing – The current model uses runtime type casting to do it’s work against the strongly typed collections on the ViewModel.  A further refinement could allow the ViewModel to expose a strongly typed derivative that could expose the individual properties as the types that the ViewModel expects, and use those properties to better enable/disable drop behaviors.  There are also lots of reflection based solutions that could be put in place to expose strongly typed bits to the ViewModels, but I'm not certain that the additional complexity is worth it.
  4. UX - I haven't really explored the use of popups or other user-level feedback that could be leveraged alongside this solution to provide a richer experience, and ideally, the mouse position relative to the current element would be used to control the 'direction' of the drop, rather than the 'fake inertia' that I'm using now.

Thanks for reading!

 

Attachments:

DataGridDragDropBehavior-20110325.zip (33 kB)