blog community
Implementing Simple drag-and-drop operations on a WPF treeview

Last week I was trying to get a drag and drop treeview to work in WPF. Creating the basic structure was rather pleasing work to do because setting up databinding and getting the items to render correctly is a breeze with the new layout and databinding capabilities. However, getting items dragged from one point in the tree to another point in the tree is a whole different story. As it turns out there's a problem with the mouse eventhandling.

When you issue a DragDrop.DoDragDrop operation within a mousemove eventhandler, the mousemove event no longer gets fired, until you drop the item. I tried different variations on this, but to no avail. Appearantly I wasn't the only person who noticed this weird behaviour, because Josh Smith tried to do something similar[1] and had to write a wrapper for the Win32 mouse api to get the correct result.

After using this utility I had the whole thing working pretty quickly. It's as simple as implementing three eventhandlers: MouseMove, DragOver and Drop. In the MouseMove eventhandler you have to add code similar to the following snippet:

void treeView_MouseMove(object sender,MouseEventArgs e) 
{
    if(!_isDragging && e.LeftButton == MouseButtonState.Pressed)
    {
        _isDragging = true;
        DragDrop.DoDragDrop(treeView,treeView.SelectedValue,
            DragDropEffects.Move);
    }
}

This issues the drag-drop movement and ensures that the user can't start the same operation a second time. Next you need to check if the user is doing the correct thing in the DragOver eventhandler, you can manipulate the behaviour of the drag-drop movement by setting the Effects property of the passed DragEventArgs object. The code here will look something like this:

void treeView_DragOver(object sender,DragEventArgs e)
{
    if(e.Data.GetDataPresent(typeof(Task))) 
    {
        e.Effects = DragDropEffects.Move;
    }
    else
    {
        e.Effects = DragDropEffects.None;
    }
}

Last you need to finish the drag-drop movement in the Drop eventhandler. This is done by moving the dragged item to another position in the tree and setting the isDragging flag to false again, so that the user can issue another drag-drop movement. In this eventhandler I had another problem, how to get the target item to drop the source item on. Because WPF has a separated visual tree, this can't be done using normal methods. The drop method is shown below.

void treeView_Drop(object sender,DragEventArgs e)
{
    if(e.Data.GetDataPresent(typeof(Task)))
    {
        Task sourceTask = (Task)e.Data.GetData(typeof(Task));
        Task targetTask = GetItemAtLocation(MouseUtilities.GetMousePosition());

        // Code to move the item in the model is placed here...
    }
}

I discovered that if you do a VisualTreeHelper.HitTest(reference,location) call, a visual gets returned that represents the control you clicked on. This can be anything, from a TextBlock element to a Border element or even controls you didn't creat but were added during runtime by WPF itself. This proved to be really helpful, because now you can cast it to something a bit more meaninful and extract the DataContext from there. Depending on how you configured the DataContext, you will get the item that is bound to the element, in my case a task. The code to get a visual from the current mouse location is shown below.

T GetItemAtLocation(Point location)
{
    T foundItem = default(T);
    HitTestResult hitTestResults = VisualTreeHelper.HitTest(treeView,location);
    
    if(hitTestResults.VisualHit is FrameworkElement)
    {
        object dataObject = (hitTestResults.VisualHit as 
            FrameworkElement).DataContext;

        if(dataObject is T)
        {
            foundItem = (T)dataObject;
        }
    }

    return foundItem;
}

It's pretty logical that Microsoft didn't include the method GetItemAtLocation in the treeview, because that would pretty much break the whole idea of having a separate visual tree. However what I don't understand at this point is why WPF doesn't fire the MouseMove event anymore when you are performing a drag-drop operation. Ooh well, the MouseUtilties class Josh created is a good solution, but I sure hope Microsoft is going to fix this problem in the next version of WPF.

[1] Drag and Drop items in a WPF listview - http://www.codeproject.com/WPF/ListViewDragDropManager.asp


Posted 11-06-2007 11:12 by willemm
Filed under: ,

Comments

Nelis Bijl wrote re: Implementing Simple drag-and-drop operations on a WPF treeview
on 22-06-2007 12:50

what's wrong with:

Task targetTask = (e.OriginalSource as FrameworkElement).DataContext as Task;

I guess you can forget about the difficult GetItemAtLocation, or am I missing something?

Nelis Bijl wrote re: Implementing Simple drag-and-drop operations on a WPF treeview
on 22-06-2007 15:38
DragDrop.DoDragDrop returns only after the complete drag-drop process is finished, thus avoiding the need of the _isDragging flag This could also be the reason for hiding MouseMove events during a drag-drop operation. In fact DragOver is just a special form of MouseMove
willemm wrote re: Implementing Simple drag-and-drop operations on a WPF treeview
on 26-06-2007 14:57

Hi Nelis,

Thanks for your comments. When I tried this trick out, I couldn't find anything about the OriginalSource property. But if I read the manual right, it should indeed contain the item I am hovering over in the DragOver eventhandler.

About the DragDrop.DoDragDrop, you're absolutely right ;)

Thanks for the corrections.

Pavel wrote re: Implementing Simple drag-and-drop operations on a WPF treeview
on 12-07-2007 14:56

Hi, your articel is very good. Can I please you send me o source code (packed project in VS2005) with a treeview in WPF, that supports drag and drop. I am beginner in programming and I am not sure, that I can complete the code without help. When I have a whole project with example I can better understand it. Thank you very much, my email: [removed]

Nelis Bijl wrote re: Implementing Simple drag-and-drop operations on a WPF treeview
on 19-07-2007 10:35
Not sure whether you tested the code yourself. I have had troubles setting the e.Effects in DragOver. They only seem to stick to values you specify if you also specify e.Handled = true. This took me quite a while to find out. A Canvas for instance does not have this requirement, so it seems the TreeView control is internally fidgetting with this setting as well !?
Laurent wrote re: Implementing Simple drag-and-drop operations on a WPF treeview
on 26-07-2007 10:51
Task targetTask = GetItemAtLocation(e.GetPosition((IInputElement) sender)); seems to work fine either.
Justin Scott wrote re: Implementing Simple drag-and-drop operations on a WPF treeview
on 03-11-2007 21:00
You all are thinking about this in the wrong way! Don't find the drop item by drilling through the treeview... Find the item from the item itself!
Tobey wrote re: Implementing Simple drag-and-drop operations on a WPF treeview
on 05-11-2007 16:09
Justin, How do you get the item itself??? It would seem "logical" that the item would be passed in the DragEventArgs, but I've been unable to find the target item in the args. If you know how to derive the target item from the given args, I'm sure _many_ people would be very grateful!
Tobey wrote re: Implementing Simple drag-and-drop operations on a WPF treeview
on 05-11-2007 16:29
I apologize for my last comment; Nelis Bijl answered it in the first post: Task targetTask = (e.OriginalSource as FrameworkElement).DataContext as Task;
Andrew wrote re: Implementing Simple drag-and-drop operations on a WPF treeview
on 08-01-2008 16:30
Thank you and one thing Use "SystemParameters.MinimumHorizontalDragDistance" and "SystemParameters.MinimumVerticalDragDistance" to define start of the Drag operation ( ............................ private clickPoint = new Point(0, 0); treeView_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { clickPoint = e.GetPosition(null); } void treeView_MouseMove(object sender,MouseEventArgs e) { if(!_isDragging && e.LeftButton == MouseButtonState.Pressed) { Point position = e.GetPosition(null); if (Math.Abs(position.X - clickPoint .X) > SystemParameters.MinimumHorizontalDragDistance || Math.Abs(position.Y - clickPoint .Y) > SystemParameters.MinimumVerticalDragDistance) { _isDragging = true; DragDrop.DoDragDrop(treeView,treeView.SelectedValue, DragDropEffects.Move); } } } .......................... )
Powered by Community Server (Commercial Edition), by Telligent Systems