Reader Level:
ARTICLE

WPF and user interactivity Part V: Drag and Drop for WPF

Posted by Bechir Bejaoui Articles | WPF April 28, 2010
In this article, I will show a very simple and basic drag and drop use case.
  • 0
  • 0
  • 15799
Download Files:
 

In previous articles, I demonstrated some basic techniques of how to deal with shapes and geometries; I mean coloring them, dragging, dropping and moving them within a scene as a part of the user mouse experience. In this article, I will show a very simple and basic drag and drop use case. Of Corse, the code is very basic and it requires several enhancements in order to be useful on a real world application. Nevertheless, it could be sort of guide for people how want to discover the drag and drop activities and how to implement them as a part of a WPF application.
First let's imagine the situation. You have two items controls, let's say two list boxes. Those ones might hold some objects like images or so. This figure clarifies the case.

final1.gif

Now, the aim is to enable exchanging those items between the two lists. This kind of task is done through dragging and dropping activities. From one list to other one. Once again, the attached property is very useful in this case as it could be attached not only at the list box control but to any kind of items control.

To begin, we create a new project and we add some images to it within an images folder. The XAML of the above interface will be like this bellow one:

<Window x:Class="MainProject.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       Loaded="Window_Loaded"
    Title="Window1" Height="632" Width="731" ResizeMode="NoResize">
    <Grid Name="LayoutRoot" >
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Border CornerRadius="3" Grid.Column="0" BorderThickness="3" 
                                                BorderBrush
="Maroon">
            <StackPanel>
                <TextBlock Margin="5" FontSize="16">
                                    
First list</TextBlock>
                <ListBox  Name="FirstListBox"
                          TabIndex="1" AllowDrop="True"
                          Height="537" Width="300">
                    <ListBox.ItemsPanel>
                        <ItemsPanelTemplate>
                            <UniformGrid Columns="2" Rows="4">

                            </UniformGrid>
                        </ItemsPanelTemplate>
                    </ListBox.ItemsPanel>
                </ListBox>
            </StackPanel>
        </Border>
        <Border CornerRadius="3"
            Grid.Column
="1" BorderThickness="3" BorderBrush="Maroon">
            <StackPanel>
                <TextBlock Margin="5"
                   FontSize
="16">Second list</TextBlock>
                <ListBox  Name="SecondListBox"  TabIndex="2"
                                               AllowDrop="True"
                                                    Height="537"  
                                                    Width="301">
                    <ListBox.ItemsPanel>
                        <ItemsPanelTemplate>
                            <UniformGrid Columns="2" Rows="4">

                            </UniformGrid>
                        </ItemsPanelTemplate>
                    </ListBox.ItemsPanel>
                </ListBox>
            </StackPanel>
        </Border>
    </Grid>
</
Window>

The code behind will have as mission to populate the first list box with images from the added images folder.

public ObservableCollection<Image> collection;
private void Window_Loaded(object sender, RoutedEventArgs e)
 {
   collection = new ObservableCollection<Image> {
       new Image{ Height=128,
                  Width=128,
                  Source=new BitmapImage(new Uri
                 ("/MainProject;component/images/Chrysanthemum.jpg",
                  UriKind.Relative))} ,

       new Image{ Height=128,
                  Width=128,
                  Source=new BitmapImage(new Uri
                 ("/MainProject;component/images/Desert.jpg",
                  UriKind.Relative))} ,
       new Image{ Height=128,
                  Width=128,
                  Source=new BitmapImage(new Uri
                 ("/MainProject;component/images/Hydrangas.jpg",
                  UriKind.Relative))} ,
               new Image{ Height=128,
                  Width=128,
                  Source=new BitmapImage(new Uri
                 ("/MainProject;component/images/Jellyfish.jpg",
                  UriKind.Relative))} ,

                new Image{ Height=128,
                  Width=128,
                  Source=new BitmapImage(new Uri
                 ("/MainProject;component/images/Koala.jpg",
                  UriKind.Relative))} ,

                new Image{ Height=128,
                  Width=128,
                  Source=new BitmapImage(new Uri
                 ("/MainProject;component/images/Lighthouse.jpg",
                  UriKind.Relative))} ,
                new Image{ Height=128,
                  Width=128,
                  Source=new BitmapImage(new Uri
                 ("/MainProject;component/images/Penguins.jpg",
                  UriKind.Relative))} ,
                new Image{ Height=128,
                  Width=128,
                  Source=new BitmapImage(new Uri
                 ("/MainProject;component/images/Tulips.jpg",
                  UriKind.Relative))}
        };
             FirstListBox.Items.Add(collection[0]);
             FirstListBox.Items.Add(collection[1]);
             FirstListBox.Items.Add(collection[2]);
             FirstListBox.Items.Add(collection[3]);
             FirstListBox.Items.Add(collection[4]);
             FirstListBox.Items.Add(collection[5]);
             FirstListBox.Items.Add(collection[6]);
             FirstListBox.Items.Add(collection[7]);

        }

As you can see, the code is very simple; the purpose is to populate the first list box with images from the images folder.

Now, let's create a new class.
 
namespace MainProject.AttachedProperties
{
    public class MoveElements
    {
    }
}

This class will hold our attached property. This last one should reflect all the situations that the user can experience. The drag and drop could be enabled in a unique direction from one container to another, in both direction from and to a given container or it could be just about disabled.

Thus, we develop an enumeration that reflects those four situations.

namespace MainProject.AttachedProperties
{
   public enum Mode {Drag,Drop,None,DragAndDrop};
}

The attached property type will be of this above enumeration type

 public static Mode GetProcessMode(DependencyObject obj)
 {
    return (Mode)obj.GetValue(ProcessModeProperty);
  }

  public static void SetProcessMode(DependencyObject obj, Mode value)
  {
      obj.SetValue(ProcessModeProperty, value);

  }
 
        // Using a DependencyProperty as the backing store for ProcessMode.  This enables animation, styling, binding,
etc...
 public static readonly DependencyProperty ProcessModeProperty =
 DependencyProperty.RegisterAttached("ProcessMode", typeof(Mode),
                                  typeof(MoveElements),_metadata);

The meta data _metadata holds the hole logic and precise the default value which should be none in our case.

static UIPropertyMetadata _metadata = new UIPropertyMetadata(Mode.None, new PropertyChangedCallback(OnPropertyChangedValue));

The metadata calls back the OnPropertyChangedValue method as you can recall above. This last one holds the given logic that handles the drag and drop experience.

static ItemsControl _control;
 static void OnPropertyChangedValue(DependencyObject d, DependencyPropertyChangedEventArgs args)
{
  _control = d as ItemsControl;
  switch ((Mode)args.NewValue)
  {
   //Only allow drag 
   case Mode.Drag:
   {
    _control.PreviewMouseLeftButtonDown +=
       new MouseButtonEventHandler(_control_PreviewMouseLeftButtonDown);
                         break;
    }
    //Only allow drop
    case Mode.Drop:
    {
      _control.Drop += new DragEventHandler(_control_Drop);
                         break;
     }
     //Allow drag and drop for the same element
     case Mode.DragAndDrop:
      {
         control.PreviewMouseLeftButtonDown += new
           MouseButtonEventHandler(_control_PreviewMouseLeftButtonDown);
          _control.Drop += new DragEventHandler(_control_Drop);
                        break;
      }
      //Totally disable draging and droping for a given element
      case Mode.None:
      {
            _control.PreviewMouseLeftButtonDown -= new  
           MouseButtonEventHandler(_control_PreviewMouseLeftButtonDown);
           _control.Drop -= new DragEventHandler(_control_Drop);           
            break;
      }
       default:
       {
          _control.PreviewMouseLeftButtonDown -= new  
           MouseButtonEventHandler(_control_PreviewMouseLeftButtonDown);
           _control.Drop -= new DragEventHandler(_control_Drop);
                        break;
       }
     }
   }


According to the situation, the corresponding event(s) will be subscribed or un subscribed and their relative event handler(s) will be processed in case of raising the given event(s).

All this is very nice, but you tell me if I apply this attached property to more than one container how the compiler does distinguishes between the source container and the destination one. This fact should be determined in order to know from where the items are removed and where they will be inserted.

Well, first you can see the _control variable; I mean the first line of the above code. This one will represent the current container, in addition to which we add another variable of the same type.

static ItemsControl _targetSource;

This last one will hold a reference to the current container instance when the user press the mouse left button as the user will do this only when he wants to drag something.

The second question is how to keep the transferred data between the moment of beginning dragging and the end of dropping. It is also a good question, and the answer is that data could be kept within a separate variable which could be of type dependency object, _data variable could hold a reference to the object that the mouse hits, the Visual tree helper class provides us a very useful method which is HitTest one and that helps us to get a reference to that object through the VisualHit variable.

static DependencyObject _data;

We can write down the mouse left button down event handler as follow:

static void _control_PreviewMouseLeftButtonDown(object sender, MouseEventArgs args)
{
   /* Get a reference to the current container*/
   ItemsControl itemsControl = sender as ItemsControl;
   /* Hold a reference to the source*/   
  _targetSource = itemsControl;
    /* Get the list view item which is an image in this case using the
       visual tree helper*/
    HitTestResult result = VisualTreeHelper.HitTest(itemsControl, 
                                 args.GetPosition(itemsControl));
    /* Get a reference to the data going to be transfered from a 
       container to another one*/
     _data = result.VisualHit;
    try
    {
       DragDrop.DoDragDrop(itemsControl, _data, DragDropEffects.Copy);
    }
    catch (InvalidOperationException caught)
   {
     /* TO DO: Add a code here to handle the situation where the element
      has already a logical parent*/
   }
}
 

Then we do write a second event handler that corresponds to the destination container where data will be droped.

static void _control_Drop(object sender, DragEventArgs args)
{
     /* Get a reference to the current container*/
      ItemsControl control = sender as ItemsControl;
    /* Remove the data from within the source container*/   
     _targetSource.Items.Remove(_data);
      /* Add data to the current container*/   
     control.Items.Add(_data);
 }

As you can see the _targetSource does well the job by holding the reference to the exact container source.

The final step consists of mapping the namespace where the attached property resides into the XAML scope.

 <Window x:Class="MainProject.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  
 xmlns:att="clr-namespace:MainProject.AttachedProperties"
    Loaded="Window_Loaded"
    Title="Window1" Height="632" Width="731" ResizeMode="NoResize">


And then applying that property to the target containers.

<ListBox  Name="FirstListBox"  TabIndex="1" AllowDrop="True"
                          att:MoveElements.ProcessMode="DragAndDrop"
<ListBox  Name="SecondListBox"
                         
 att:MoveElements.ProcessMode="DragAndDrop"

So try to apply the different modes and see what's happening, that's all.

Good Dotneting!!!

COMMENT USING

Trending up