Sunday, March 29, 2009

WinForms Drag & Drop. A simple example to get you started.

When using computers these days, we've all gotten accustomed to dragging and dropping things from one place to another. We do it all the time, and it gives your application that extra usability factor that the end user will appreciate. At work, I was working on an app that gave the user the ability to "Screen" on various columns of data. The user needed to have the ability to choose specifically which fields they want to screen on. So, we came up with the idea of having two treeviews. On the left hand side were all the available columns in the database and on the right was an empty treeview. The user would then be able to drag and drop fields from one to the other. So, here's the basic way of implementing Drag / Drop in your windows app.

For simplicity in this example, I decided to go with a Listbox on the left and a Treeview on the right. The idea here will be simple. The user will be able to drag an item from the left, and move it to the treeview on the right. We will have to track where the user "drops" the item, so that we can place it under the proper parent node. Here's what the sample form looks like:




When the form loads, we'll click on the Populate controls button. That will add the next 10 dates to the listbox on the left. It will also add two parents nodes to the treeview as well as two subnodes to each of the parent nodes.

Here's the full code, I'll go through and explain it all as best I can:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
 
namespace SetFocusDemo.DragDropSample
{
    public partial class DragDropSample : Form
    {
        public DragDropSample()
        {
            InitializeComponent();
 
            this.treeView1.AllowDrop = true;
            this.listBox1.AllowDrop = true;
 
            this.listBox1.MouseDown += new MouseEventHandler(listBox1_MouseDown);
            this.listBox1.DragOver += new DragEventHandler(listBox1_DragOver);
 
            this.treeView1.DragEnter += new DragEventHandler(treeView1_DragEnter);
            this.treeView1.DragDrop += new DragEventHandler(treeView1_DragDrop);
 
        }
 
        private void buttonPopulate_Click(object sender, EventArgs e)
        {
            this.PopulateListBox();
            this.PopulateTreeView();
        }
 
        private void PopulateListBox()
        {
            for (int i = 0; i <= 10; i++)
            {
                this.listBox1.Items.Add(DateTime.Now.AddDays(i));
            }
        }
 
        private void PopulateTreeView()
        {
            for (int i = 1; i <= 2; i++)
            {
                TreeNode node = new TreeNode("Node" + i);
                for (int j = 1; j <= 2; j++)
                {
                    node.Nodes.Add("SubNode" + j);
                }
                this.treeView1.Nodes.Add(node);
            }
        }
 
        private void listBox1_MouseDown(object sender, MouseEventArgs e)
        {
            this.listBox1.DoDragDrop(this.listBox1.SelectedItem, DragDropEffects.Move);
        }
 
        private void listBox1_DragOver(object sender, DragEventArgs e)
        {
            e.Effect = DragDropEffects.Move;
        }
 
        private void treeView1_DragEnter(object sender, DragEventArgs e)
        {
            e.Effect = DragDropEffects.Move;
        }
 
        private void treeView1_DragDrop(object sender, DragEventArgs e)
        {
 
            TreeNode nodeToDropIn = this.treeView1.GetNodeAt(this.treeView1.PointToClient(new Point(e.X, e.Y)));
            if (nodeToDropIn == null) { return; }
            if (nodeToDropIn.Level > 0)
            {
                nodeToDropIn = nodeToDropIn.Parent;
            }
 
            object data = e.Data.GetData(typeof(DateTime));
            if (data == null) { return; }
            nodeToDropIn.Nodes.Add(data.ToString());
            this.listBox1.Items.Remove(data);
        }
    }
}


Ok, let's start with the constructor. First thing you need to make sure, is that if you want a control to be a "Drop Target" you need to set the "AllowDrop" property to true. You'll also notice that we're setting the "AllowDrop" property of the listbox. Even though we won't actually be dropping anything in the listbox, the only way to get the correct icon while dragging out of the listbox, is to set it's AllowDrop to true as well.

            this.listBox1.MouseDown += new MouseEventHandler(listBox1_MouseDown);
            this.listBox1.DragOver += new DragEventHandler(listBox1_DragOver);
 
            this.treeView1.DragEnter += new DragEventHandler(treeView1_DragEnter);
            this.treeView1.DragDrop += new DragEventHandler(treeView1_DragDrop);


Here, we hook up to all the events that we'll need to make this happen. I'll explain it when we get to the handlers.

        private void buttonPopulate_Click(object sender, EventArgs e)
        {
            this.PopulateListBox();
            this.PopulateTreeView();
        }
 
        private void PopulateListBox()
        {
            for (int i = 0; i <= 10; i++)
            {
                this.listBox1.Items.Add(DateTime.Now.AddDays(i));
            }
        }
 
        private void PopulateTreeView()
        {
            for (int i = 1; i <= 2; i++)
            {
                TreeNode node = new TreeNode("Node" + i);
                for (int j = 1; j <= 2; j++)
                {
                    node.Nodes.Add("SubNode" + j);
                }
                this.treeView1.Nodes.Add(node);
            }
        }


This is just some demo code to set up are controls. In the PopulateListBox() method we just do a for loop from 0 to 10 and add the date for the next 10 days to the listbox. In the PopulateTreeView() method, we just put two parent nodes along with two child nodes in the treeview. Again, this is just for the purpose of this demo.

        private void listBox1_MouseDown(object sender, MouseEventArgs e)
        {
            this.listBox1.DoDragDrop(this.listBox1.SelectedItem, DragDropEffects.Move);
        }


Here's where it starts getting interesting. The first event we deal with, is the "MouseDown" on the listbox. This kicks off the Drag / Drop; when the user actually clicks on an item. Here, we call "DoDragDrop" on the listbox. The arguments that the DoDragDrop method expect are "object data", and "DragDropEffects allowedEffects". The data in the first argument, is the actual object that you want to move from one control to the other. In our case, we just grab the listbox selected item, which will be a DateTime. We then use the Move drag effect, which gives the nice visual mouse cursor we expect for a move.

        private void listBox1_DragOver(object sender, DragEventArgs e)
        {
            e.Effect = DragDropEffects.Move;
        }


The next event we handle is the listbox DragOver event. Since we called the DoDragDrop in the mousedown, when the user starts to "drag" the item out, the DragOver event is now raised in the listbox. We are only dealing with this event so that we can continue to show the "move" mouse cursor.

Once the user leaves the Listbox, the DragLeave event is raised. In this case I'm not handling it because I have no need for it, but if you do need it, it does get raised.

        private void treeView1_DragEnter(object sender, DragEventArgs e)
        {
            e.Effect = DragDropEffects.Move;
        }


The next event we're listening for is the "DragEnter" on the Treeview. At this point, the user has clicked his mouse on one of the dates, dragged it out of the listbox, and now moved his mouse into the treeview. All we're doing here, is just setting the DragEffects to move so that we can continue to show the correct mouse cursor.

        private void treeView1_DragDrop(object sender, DragEventArgs e)
        {
 
            TreeNode nodeToDropIn = this.treeView1.GetNodeAt(this.treeView1.PointToClient(new Point(e.X, e.Y)));
            if (nodeToDropIn == null) { return; }
            if (nodeToDropIn.Level > 0)
            {
                nodeToDropIn = nodeToDropIn.Parent;
            }
 
            object data = e.Data.GetData(typeof(DateTime));
            if (data == null) { return; }
            nodeToDropIn.Nodes.Add(data.ToString());
            this.listBox1.Items.Remove(data);
        }


Finally, we actually deal with the "main event" (pun intended). This is where the user finally lets go of the mouse and "Drops" the item into the TreeView. First we need to determine where the user let go of his mouse. There's a very helpful method on the TreeView called "GetNodeAt(Point pt)" which takes a Point as a parameter, and gives you back the TreeNode at that location. The Point that's passed to us through the DragEventArgs is in screen coordinates. Therefore, we first need to call "PointToClient" to get us the coordinates relative to the TreeView. Once we have that, we can get the exact node where the user let go of the mouse.

Now, we just need to determine if the node where the user dropped the item is a parent node or a child node. This is the business rule I decided to set up for this example; that the user can only drag a date into the treeview as a subnode. We will not allow them to add parent nodes. Therefore, we first need to determine if the node is a parent or a sub. Once we have the parent node, we will add the dropped item as a subnode.

            object data = e.Data.GetData(typeof(DateTime));
            if (data == null) { return; }


Here we retrieve the item that was brought over from the listbox through the eventargs. We call the GetData method to actually retrieve the item which in our case is a DateTime object.

            nodeToDropIn.Nodes.Add(data.ToString());
            this.listBox1.Items.Remove(data);


Once that's done, we just add it as a node, and remove the item from the listbox. Run the sample and try it yourself. Click the Populate Button and then drag items from the listbox to the treeview. Cool stuff no?

I admit, it's a bit of a pain to get this all to work, but the end result is worth it. Users are accustomed to drag and drop and will appreciate the extra touch in your app.

No comments: