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.

Sunday, April 3, 2011

Defeated Exam 70-516

Last week I did the MCTS Exam 70-516: Accessing Data with ADO.NET 4. Luckily I passed it at the first attempt. Because there are still no official Microsoft preparation materials out there (the according book will be released in June 2011), it was not that easy to prepare for the exam. I used the following preparation materials:
The practice tests by MeasureUp are really nice. They give you a very good feeling about the focus areas of the real exam and can be used to effectively learn the content. What’s sometimes missing with the practice tests is the context. That means that sometimes in the real exam you need to know a little bit more around a certain feature than you get to know within the practice tests. So that’s why I would suggest reading the MSDN or even trying out some features by yourself within a little sample application.
If you’re going to do that exam too: Good Luck! I will head on to the next exam and post my experiences here again.