Thursday, July 10, 2008

Item dragging in Windows Forms ListBox control

Drag-n-drop support in general case is the ability to drag some object from one control and then drop it into the same or some other control.
But very often when we work with ListBox controls we just need an ability to move items up and down within one particular control. It is not necessary to implement all those drag-n-drop functionality in such case.
The following small class will help you to handle such situation without overkill. When it is attached to some ListBox you can grab any item and move it on any place within the control.
Here is the class source:
using System;
using System.Windows.Forms;

namespace Korzh.WinControls {

    /// 
    /// Turn on item dragging for some ListBox control
    /// 
    class ListBoxItemDragger {
        private ListBox listBox;

        private int dragItemIndex = -1;

        /// 
        /// Gets the index of the dragged item.
        /// 
        /// The index of the dragged item.
        public int DragItemIndex {
            get { return dragItemIndex; } 
        }

        private bool dragging = false;

        /// 
        /// Initializes a new instance of the  class.
        /// 
        /// The list box.
        public ListBoxItemDragger(ListBox listBox) {
            Attach(listBox);
        }

        /// 
        /// Attaches current instance to some ListBox control.
        /// 
        public void Attach(ListBox listBox) {
            this.listBox = listBox;
            this.listBox.MouseDown += new System.Windows.Forms.MouseEventHandler(this.MouseDownHandler);
            this.listBox.MouseUp += new System.Windows.Forms.MouseEventHandler(this.MouseUpHandler);
            this.listBox.MouseMove += new System.Windows.Forms.MouseEventHandler(this.MouseMoveHandler);
        }

        /// 
        /// Detaches current instance from ListBox control.
        /// 
        public void Detach() {
            this.listBox.MouseDown -= new System.Windows.Forms.MouseEventHandler(this.MouseDownHandler);
            this.listBox.MouseUp -= new System.Windows.Forms.MouseEventHandler(this.MouseUpHandler);
            this.listBox.MouseMove -= new System.Windows.Forms.MouseEventHandler(this.MouseMoveHandler);
        }

        private Cursor dragCursor = Cursors.SizeNS;
        public Cursor DragCursor {
            get { return dragCursor; }
            set { dragCursor = value; }
        }

        /// 
        /// Raises the  event.
        /// 
        /// The  instance containing the event data.
        protected void OnItemMoved(EventArgs e) {
            if (ItemMoved != null) ItemMoved(this, e);
        }

        /// 
        /// Occurs when some item has been moved
        /// 
        public event EventHandler ItemMoved;

        private void MouseDownHandler(object sender, System.Windows.Forms.MouseEventArgs e) {
            dragItemIndex = listBox.SelectedIndex;
        }


        private Cursor prevCursor = Cursors.Default;

        private void MouseUpHandler(object sender, System.Windows.Forms.MouseEventArgs e) {
            dragItemIndex = -1;
            if (dragging) {
                listBox.Cursor = prevCursor;
                dragging = false;
            }
        }

        private void MouseMoveHandler(object sender, System.Windows.Forms.MouseEventArgs e) {
            if (dragItemIndex >= 0 && e.Y > 0) {
                if (!dragging) {
                    dragging = true;
                    prevCursor = listBox.Cursor;
                    listBox.Cursor = DragCursor;
                }
                int dstIndex = listBox.IndexFromPoint(e.X, e.Y);

                if (dragItemIndex != dstIndex) {
                    object item = listBox.Items[dragItemIndex];
                    listBox.BeginUpdate();
                    try {
                        listBox.Items.RemoveAt(dragItemIndex);
                        if (dstIndex != ListBox.NoMatches)
                            listBox.Items.Insert(dstIndex, item);
                        else
                            dstIndex = listBox.Items.Add(item);

                        listBox.SelectedIndex = dstIndex;
                    }
                    finally {
                        listBox.EndUpdate();
                    }
                    dragItemIndex = dstIndex;
                    OnItemMoved(EventArgs.Empty);
                }
            }
        }

    }
}

To use this class you just need to create its instance and attach it to your ListBox control. In the most simple case it will be only one line of code:
.  .  .  .  .  .  .
 new ListBoxItemDragger(someListBoxControl);
.  .  .  .  .  .  .
Nothing else is needed! We have created ListBoxItemDragger object and it was atomatically attached to the ListBox control passed in constructor's parameter. You do not even need to store the object of this class into some variable. Our object will "live" as long as the ListBox control it is attached to. However you may want to get notified when some item is dragged. Additionally you may want to change the cursor shown during dragging (the default is SizeNS cursor as you can see from the source). In such case you should get the instance of this class and set some of its properties:
.  .  .  .  .  .  .
 ListBoxItemDragger itemDragger1 = new ListBoxItemDragger(someListBox);
 itemDragger1.DragCursor = Cursor.Hand;
 itemDragger1.ItemMoved += new EventHandler(SomeListBoxItemMoved);

.  .  .  .  .  .  .

private void SomeListBoxItemMoved(object sender, System.EventArgs e) {
 //handle ListBox item moving here
}
Here we created an instance of ListBoxItemDragger class, then changed its DragCursor property and finally we atached the handler for ItemMoved event. To get the index of dragged item use DragItemIndex property. That's all. Enjoy!

Wednesday, July 2, 2008

IE quirk: offsetParent of DOM element added into document.body

We spent about 1 day of work while find out the following crazy feature of DOM implementation in Internet Explorer. Just look at the HTML page below and try to say what will be shown on execution of the test() JavaScript function:

  
    DOM Element offsetParent test
  
  
  
    

 Test 
    


You think it will show you "BODY" text?
You will be right in case of FireFox or Opera. But in Internet Explorer (we tested it both on versions 6 and 7) test() function will print "HTML" instead of "BODY" because element.offsetParent in this case is equal to document.documentElement instead of document.body as you may suppose.

Why is it implemented in such strage way? I do not know. Moreover, for some reason it works right (shows "BODY" text) if you change the DTD declaration for this page to the following:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"">
So just be careful when implementing such functions as described in this post.
Actually we had to make changes in our existing code when found this strange behaviour, for example in our GetElementAbsolutePos() function.
The following piece of code:
 
 if (offsetParent != document.body) {  
 res.x -= offsetParent.scrollLeft;  
 res.y -= offsetParent.scrollTop;  
} 

 
had been changed to this one:
     if (offsetParent != document.body && offsetParent != document.documentElement) {  
 res.x -= offsetParent.scrollLeft;  
 res.y -= offsetParent.scrollTop;  
}  

     
Hope this information will save somebody's time.
Enjoy!