Tuesday, April 8, 2008

Extending the TreeView control with drag & drop

The TreeView control in C# is quite a useful control used to visualize items in a tree. Unfortunately (or fortunately, if you like figuring things out) you still have to write your code to allow drag and drop of the TreeNode items when you have populated the control.

I've written a simple BuilderTreeView component based on the TreeView control, which allows the TreeNode items to be drag and dropped.

So far the control has the following additional characteristics over the normal TreeView control:

  1. Drag & Drop nodes
  2. Move up/down of nodes
  3. Move child nodes to the root
  4. Auto scroll when dragging nodes up/down
  5. Basic context menu

Below is the code. It's still in progress, and not final, but at least it's usable and I've tested it in some scenarios.

I do believe there are other ways of doing this, so leave a comment if you have experienced different ways of implementing drag & drop. :)

  1 namespace Builder
2 {
3 public class BuilderTreeView : TreeView
4 {
5 // private members
6 private TreeNode _draggedNode = null;
7 private ContextMenu _defaultContextMenu = null;
8
9 /// <summary>
10 /// MoveDirection enum
11 /// </summary>
12 private enum MoveDirection
13 {
14 MoveUp,
15 MoveDown
16 }
17
18 // default constructor
19 public BuilderTreeView()
20 {
21 // allow drag & drop
22 this.AllowDrop = true;
23
24 #region Create default context menu
25
26 MenuItem[] menuItems = new MenuItem[] {
27 new MenuItem("Move to &Root", new EventHandler(DefaultContextMenu_MoveToRoot_Eventhandler)) { Name = "mniMoveToRoot" },
28 new MenuItem("-"),
29 new MenuItem("Move &Up", new EventHandler(DefaultContextMenu_MoveUp_Eventhandler)) { Name = "mniMoveUp" },
30 new MenuItem("Move &Down", new EventHandler(DefaultContextMenu_MoveDown_Eventhandler)) { Name = "mniMoveDown" },
31 new MenuItem("-"),
32 new MenuItem("&Expand", new EventHandler(DefaultContextMenu_Expand_Eventhandler)) { Name = "mniExpand" },
33 new MenuItem("&Collapse", new EventHandler(DefaultContextMenu_Collapse_Eventhandler)) { Name = "mniCollapse" },
34 new MenuItem("-"),
35 new MenuItem("E&xpand All", new EventHandler(DefaultContextMenu_ExpandAll_Eventhandler)) { Name = "mniExpandAll" },
36 new MenuItem("C&ollapse All", new EventHandler(DefaultContextMenu_CollapseAll_Eventhandler)) { Name = "mniCollapseAll" },
37 };
38
39 // create contect menu
40 this._defaultContextMenu = new ContextMenu(menuItems);
41 this._defaultContextMenu.Popup += new EventHandler(_defaultContextMenu_Popup);
42
43 // assign context menu
44 this.ContextMenu = this._defaultContextMenu;
45
46 #endregion
47 }
48
49 #region DefaultContextMenu/MenuItem Eventhandlers
50
51 // DefaultContextMenu, Popup Eventhandler
52 void _defaultContextMenu_Popup(object sender, EventArgs e)
53 {
54 // enable MenuItems when a node is selected
55 for (int i = 0; i < _defaultContextMenu.MenuItems.Count - 1; i++)
56 _defaultContextMenu.MenuItems[i].Enabled = (this.SelectedNode != null);
57
58 // enable nodes by criteria
59 if (this.SelectedNode != null)
60 {
61 // check if node is a root node
62 _defaultContextMenu.MenuItems["mniMoveToRoot"].Enabled = (this.SelectedNode.Parent != null);
63
64 // check if node is at the top
65 _defaultContextMenu.MenuItems["mniMoveUp"].Enabled = (this.SelectedNode.PrevNode != null);
66
67 // check if node is at the bottom
68 _defaultContextMenu.MenuItems["mniMoveDown"].Enabled = (this.SelectedNode.NextNode != null);
69 }
70 }
71
72 // DefaultContextMenu, MoveToRoot MenuItem Eventhandler
73 protected void DefaultContextMenu_MoveToRoot_Eventhandler(object sender, EventArgs e)
74 {
75 this.Nodes.Add((TreeNode)this.SelectedNode.Clone());
76 this.SelectedNode.Remove();
77 }
78
79 // DefaultContextMenu, MoveUp MenuItem Eventhandler
80 protected void DefaultContextMenu_MoveUp_Eventhandler(object sender, EventArgs e)
81 {
82 MoveNode(this.SelectedNode, MoveDirection.MoveUp);
83 }
84
85 // DefaultContextMenu, MoveDown MenuItem Eventhandler
86 protected void DefaultContextMenu_MoveDown_Eventhandler(object sender, EventArgs e)
87 {
88 MoveNode(this.SelectedNode, MoveDirection.MoveDown);
89 }
90
91 // DefaultContextMenu, Expand MenuItem Eventhandler
92 protected void DefaultContextMenu_Expand_Eventhandler(object sender, EventArgs e)
93 {
94 this.SelectedNode.Expand();
95 }
96
97 // DefaultContextMenu, Collapse MenuItem Eventhandler
98 protected void DefaultContextMenu_Collapse_Eventhandler(object sender, EventArgs e)
99 {
100 this.SelectedNode.Collapse();
101 }
102
103 // DefaultContextMenu, ExpandAll MenuItem Eventhandler
104 protected void DefaultContextMenu_ExpandAll_Eventhandler(object sender, EventArgs e)
105 {
106 this.ExpandAll();
107 }
108
109 // DefaultContextMenu, CollapseAll MenuItem Eventhandler
110 protected void DefaultContextMenu_CollapseAll_Eventhandler(object sender, EventArgs e)
111 {
112 this.CollapseAll();
113 }
114
115 #endregion
116
117 #region Drag Events
118
119 // start node drag
120 protected override void OnItemDrag(ItemDragEventArgs e)
121 {
122 this._draggedNode = (TreeNode)e.Item;
123 this.SelectedNode = this._draggedNode;
124
125 this.DoDragDrop(e.Item, DragDropEffects.Move);
126 }
127
128 // create new node location
129 protected override void OnDragDrop(DragEventArgs drgevent)
130 {
131 if (drgevent.AllowedEffect.Equals(DragDropEffects.Move))
132 {
133 // get node at drop location
134 TreeNode dropNode = DroppedOnNode(drgevent);
135
136 // add to root if new node parent has no parent
137 if (dropNode != null)
138 dropNode.Nodes.Add((TreeNode)this._draggedNode.Clone());
139 else
140 this.Nodes.Add((TreeNode)this._draggedNode.Clone());
141
142 // remove original
143 this._draggedNode.Remove();
144 }
145 }
146
147 // set DragDropEffects
148 protected override void OnDragEnter(DragEventArgs drgevent)
149 {
150 // no external drag & drop allowed
151 drgevent.Effect = DragDropEffects.None;
152 }
153
154 // set Effect when dragging over control items
155 protected override void OnDragOver(DragEventArgs drgevent)
156 {
157 // get node at drop location
158 TreeNode dropNode = DroppedOnNode(drgevent);
159
160 // scroll if next/prev node is not visible
161 if (dropNode != null)
162 if (dropNode.NextVisibleNode != null)
163 if (!dropNode.NextVisibleNode.IsVisible)
164 dropNode.NextVisibleNode.EnsureVisible();
165 else
166 if (dropNode.PrevVisibleNode != null)
167 if (!dropNode.PrevVisibleNode.IsVisible)
168 dropNode.PrevVisibleNode.EnsureVisible();
169
170 // valid if the node is not moving to itself, or to one of its child nodes
171 if (SameBranch(this._draggedNode, dropNode))
172 drgevent.Effect = DragDropEffects.None;
173 else
174 {
175 // expand collapsed node
176 if (dropNode != null)
177 dropNode.Expand();
178
179 drgevent.Effect = DragDropEffects.Move;
180 }
181 }
182
183 // set UseDefaultCursors
184 protected override void OnGiveFeedback(GiveFeedbackEventArgs gfbevent)
185 {
186 gfbevent.UseDefaultCursors = true;
187 }
188
189 #endregion
190
191 #region Helper functions
192
193 /// <summary>
194 /// Move a node
195 /// </summary>
196 /// <param name="node">TreeNode to move</param>
197 /// <param name="move">MoveDirection enum</param>
198 /// <returns>int</returns>
199 private void MoveNode(TreeNode node, MoveDirection move)
200 {
201 // default to "no move"
202 int Index = -1;
203 switch (move)
204 {
205 // move up if a previous node is found
206 case MoveDirection.MoveUp:
207 if (node.PrevNode != null)
208 Index = node.PrevNode.Index;
209 break;
210
211 // move down if a next node is found
212 case MoveDirection.MoveDown:
213 if (node.NextNode != null)
214 Index = node.NextNode.Index + 1;
215 break;
216 }
217
218 // if moveable
219 if (Index != -1)
220 {
221 // check for root node
222 if (node.Parent != null)
223 {
224 node.Parent.Nodes.Insert(Index, (TreeNode)node.Clone());
225 this.SelectedNode = node.Parent.Nodes[Index];
226 }
227 else
228 {
229 this.Nodes.Insert(Index, (TreeNode)node.Clone());
230 this.SelectedNode = this.Nodes[Index];
231 }
232
233 node.Remove();
234 }
235 }
236
237 /// <summary>
238 /// Get Node at mouse pointer location
239 /// </summary>
240 /// <param name="drgevent">DragEventArgs</param>
241 /// <returns>TreeNode</returns>
242 private TreeNode DroppedOnNode(DragEventArgs drgevent)
243 {
244 // get the drop point on the control
245 Point dropLocation = this.PointToClient(new Point(drgevent.X, drgevent.Y));
246
247 // get the node at the drop point
248 return (this.GetNodeAt(dropLocation));
249 }
250
251 /// <summary>
252 /// Check if a dragged node is the same, or on the same node branch
253 /// </summary>
254 /// <param name="dragnode">Node being dragged</param>
255 /// <param name="targetnode">Node being dragged to</param>
256 /// <returns>bool</returns>
257 private bool SameBranch(TreeNode draggedNode, TreeNode dropNode)
258 {
259 // at the root level
260 if (dropNode == null)
261 return false;
262 else
263 {
264 // check if it is the same node, else call recursively
265 if (draggedNode.Equals(dropNode))
266 return true;
267 else
268 return SameBranch(draggedNode, dropNode.Parent); // ;-)
269 }
270 }
271
272 #endregion
273 }
274 }


Environment:
Microsoft ® Visual Studio 2008
Microsoft ® .NET framework 3.5
Microsoft ® Windows XP (SP2)

8 comments:

Juan R said...

Thanks! Great Job
A little complicated put the code into visual studio ;-) With word and replacing with regexp... working!!

Christo said...

Cool. For new posts I don't include line numbering anymore. :)

Anonymous said...

Thanks worked great Until....

I used you code and it worked fine until my application was run on a Vista machine. The nodes still change order but the index remains the same.

Has anyone had the same experience?

Christo said...

Please could you elaborate more on your problem? I tested this on Windows 7, and I haven't encountered any problems. :)

Anonymous said...

Unfortunately I don't have a Vista or Windows 7 machine to debug. First, I am not implying that your code is at fault. I need to go a little more testing and to make sure I haven't changed your orginal code. I have an application as I said that runs fine on XP but when run on Vista it doesn't work the same. If I have two nodes (children of root) of which I want to change the order when I right click and select move up or move down the nodes change order in the tree but their index (which I display as order is very important in my case) stays as it was before the move. I will dig deeper today and see if I can figure it out. I was just hoping this was a common problem but it seems it just may be my bug.

Christo said...

Thank you for the feedback. When you figured out the cause, let me know. ;)

Sumit said...

Hi Christo, the custom tree view control written by you is cool ? Have u changed it to fix the issue it has with running it in windows vista or do u hv any new version of it. Kindly reply by email. thanks,sumit

Christo said...

I've tested it on Windows 7 and haven't encountered the issue described above. Windows Vista should be fine also.