Tuesday, April 12, 2011

WPF TreeView – Drag ‘n‘ Drop

Using Drag&Drop inside an application can greatly improve the user experience. This post shows how to implement Drag&Drop inside a TreeView within a WPF application. I will only point out key features directly within the post. For the complete code example use the link at the end of the post.

Theory

Let’s take a second to see what it takes to realize Drag&Drop functionality. First of all the user needs to indicate to the application/system that he or she wants to drag a user interface item. Traditionally this is done by moving the item with the mouse while holding a mouse button down. You may also have other input devices (e.g. your finger tip). The system needs to determine when to start the dragging process. While dragging it must at least know the type of the dragged item in order to give any possible drop target the ability to decide whether it can accept the item or not. So that’s the other part: the drop target. Any user interface element must have the possibility to indicate to the system that it accepts dragged items and which type of items or which data it accepts. The system uses this information in turn to visually indicate the user that he or she may drop the item on a certain target element. If the user lets the item go (e.g. by releasing the mouse button) the drop target is responsible for handling the received item or data.

Drag ‘n’ Drop in WPF

So, how is this process done in WPF. For the sample application I used a TreeView. The goal is to reorder the items in the tree by using Drag&Drop. In order to get started I used a really good article by Josh Smith on Codeproject to build up a tree representing a folder structure. The TreeView uses a ViewModel to generate the items and handle selection and expansion inside the view:
public bool IsSelected
{
    get { return isSelected; }
    set
    {
        if (value != isSelected)
        {
            isSelected = value;
            this.OnPropertyChanged("IsSelected");
        }
    }
}

public bool IsExpanded
{
    get { return isExpanded; }
    set
    {
        if (value != isExpanded)
        {
            isExpanded = value;
            this.OnPropertyChanged("IsExpanded");
        }
        if (isExpanded && parent != null && !parent.IsExpanded)
            parent.IsExpanded = true;       
        }
    }
}

If you define these properties in your ViewModel you can easily bind them to the TreeView items by setting the ItemContainerStyle:
<TreeView.ItemContainerStyle>
  <Style TargetType="{x:Type TreeViewItem}">
    <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
    <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
    <Setter Property="FontWeight" Value="Normal" />
    <Style.Triggers>
      <Trigger Property="IsSelected" Value="True">
        <Setter Property="FontWeight" Value="Bold" />
      </Trigger>
    </Style.Triggers>
  </Style>
</TreeView.ItemContainerStyle>

Ok, now to the Drag&Drop part. A UIElement that wants to start a drag operation has to call the static DoDragDrop method of the DragDrop class. This should be done when the user presses a mouse button on a draggable item and starts moving the mouse. Therefore you should listen to the following events:
-          PreviewMouseLeftButtonDown
-          PreviewMouseMove

XAML:
<TreeView Width="200"          
PreviewMouseLeftButtonDown="Tree_PreviewMouseLeftButtonDown"
       PreviewMouseMove="Tree_MouseMove"/>

Code-behind:
private void Tree_PreviewMouseLeftButtonDown(
object sender, MouseButtonEventArgs e)
{
    startPoint = e.GetPosition(null);
}

private void Tree_MouseMove(object sender, MouseEventArgs e)
{
    if (e.LeftButton == MouseButtonState.Pressed)
    {
        var mousePos = e.GetPosition(null);
        var diff = startPoint - mousePos;

        if (Math.Abs(diff.X) > SystemParameters.MinimumHorizontalDragDistance
            || Math.Abs(diff.Y) > SystemParameters.MinimumVerticalDragDistance)
        {
            var treeView = sender as TreeView;
            var treeViewItem =
                FindAnchestor<TreeViewItem>((DependencyObject) e.OriginalSource);

            if (treeView == null || treeViewItem == null)
                return;

            var folderViewModel = treeView.SelectedItem as FolderViewModel;
            if (folderViewModel == null)
                return;

            var dragData = new DataObject(folderViewModel);
            DragDrop.DoDragDrop(treeViewItem, dragData, DragDropEffects.Move);
        }
    }
}

The button down event handler is simply used to store the current mouse position. When the user moves the mouse the mouse move event handler is called and calculates the difference between the starting point and the current point. If the difference is greater than the given system parameters then it tries to get the underlying view model for the selected tree item and stores it into a DataObject which can be passed to the DoDragDrop method.

Every UIElement in WPF which wants to act as a drop target must use the following events and properties:
-          Drop (will be raised when the user releases the mouse)
-          DragEnter (will be raised when a dragged item enters the bounding box of a potential drop target)
-          AllowDrop (tells the system, that this UIElement can accept dragged items)

XAML:
<TreeView Width="200"
      PreviewMouseLeftButtonDown="Tree_PreviewMouseLeftButtonDown"
      PreviewMouseMove="Tree_MouseMove"
      Drop="DropTree_Drop"
      DragEnter="DropTree_DragEnter"
      AllowDrop="True"/>

Code-behind:
private void DropTree_DragEnter(object sender, DragEventArgs e)
{
    if (!e.Data.GetDataPresent(typeof(FolderViewModel)))
    {
        e.Effects = DragDropEffects.None;
    }
}

private void DropTree_Drop(object sender, DragEventArgs e)
{
    if (e.Data.GetDataPresent(typeof(FolderViewModel)))
    {
        var folderViewModel = e.Data.GetData(typeof (FolderViewModel))
as FolderViewModel;
        var treeViewItem =
            FindAnchestor<TreeViewItem>((DependencyObject)e.OriginalSource);

        var dropTarget = treeViewItem.Header as FolderViewModel;

        if (dropTarget == null || folderViewModel == null)
            return;

        folderViewModel.Parent = dropTarget;
    }
}

The DragEnter event handler simply looks for the type of the data it gets from the DragEventArgs and sets the drag drop effect to none when it is not of the expected type. That means that there is no drop possible. If the user releases the mouse button over an allowed drop target the drop event handler is called. The handler extracts the data out of the EventArgs and in this case changes the parent of the found ViewModel to the new TreeViewItem. The setter of the parent property handles the change of the parent folder.

Example Code

To use the sample project just change the root path in the constructor of the main window class. But be careful when dragging folders around in the application because this is directly promoted to the file system. You can download the sample application here.

11 comments:

  1. FindAnchestor((DependencyObject) e.OriginalSource)? Is this an extension method of yours?

    ReplyDelete
  2. This is just a little helper method to search for ancestors up the visual tree. It's implemented like this:

    static T FindAnchestor(DependencyObject current)
    where T : DependencyObject
    {
    do
    {
    if (current is T)
    {
    return (T)current;
    }
    current = VisualTreeHelper.GetParent(current);
    }
    while (current != null);
    return null;
    }

    ReplyDelete
  3. Thank you for this article, it was very helpful. Do you have any idea how to draw a horizontal line at the place where I'm about to drop the item?

    ReplyDelete
    Replies
    1. Hi George!
      you would need to implement an appropriate Adorner to accomplish this. There are a lot examples and implemenations on the net to get you started. Just try a google search with "WPF DragAdorner".

      Delete

  4. Thank you for the article. It was helpful.

    ReplyDelete
  5. I think this is not MVVM as your writing code in codebehind !

    ReplyDelete
  6. Thanks for this piece of code. Did not know about SysmteParameters.MinimumDistance but this eventually causes some bugs:

    So you start you Drag'n'Drop operation not always on the actual trewViewItem, you start it on the item where the mouse after the drag distance is. If you are close to the edge, you select another treeViewItem. Also if you leave the treeView within this distance, you won't have any treeViewItem and Drag'n'Drop is not starting. With ContainerFromItem method (maybe recursive) you can check for the actual selected item.

    Also I like to add, DoDragDrop is a blocking operation, which returns the final result of the Drag'n'Drop operation, even if it's outside the program.

    ReplyDelete