Reorder a winforms listbox using drag and drop?

C#WinformsListbox

C# Problem Overview


Is this a simple process?

I'm only writing a quick hacky UI for an internal tool.

I don't want to spend an age on it.

C# Solutions


Solution 1 - C#

Here's a quick down and dirty app. Basically I created a Form with a button and a ListBox. On button click, the ListBox gets populated with the date of the next 20 days (had to use something just for testing). Then, it allows drag and drop within the ListBox for reordering:

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            this.listBox1.AllowDrop = true;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            for (int i = 0; i <= 20; i++)
            {
                this.listBox1.Items.Add(DateTime.Now.AddDays(i));
            }
        }

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

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

        private void listBox1_DragDrop(object sender, DragEventArgs e)
        {
            Point point = listBox1.PointToClient(new Point(e.X, e.Y));
            int index = this.listBox1.IndexFromPoint(point);
            if (index < 0) index = this.listBox1.Items.Count-1;
            object data = e.Data.GetData(typeof(DateTime));
            this.listBox1.Items.Remove(data);
            this.listBox1.Items.Insert(index, data);
        }

Solution 2 - C#

7 Years Late. But for anybody new, here is the code.

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

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

    private void listBox1_DragDrop(object sender, DragEventArgs e)
    {
        Point point = listBox1.PointToClient(new Point(e.X, e.Y));
        int index = this.listBox1.IndexFromPoint(point);
        if (index < 0) index = this.listBox1.Items.Count - 1;
        object data = listBox1.SelectedItem;
        this.listBox1.Items.Remove(data);
        this.listBox1.Items.Insert(index, data);
    }

    private void itemcreator_Load(object sender, EventArgs e)
    {
        this.listBox1.AllowDrop = true;
    }

Solution 3 - C#

The first time it takes a few hours if you never implemented drag and drop, want to get it done right and have to read through the docs. Especially the immediate feedback and restoring the list if the user cancels the operation require some thoughts. Encapsulating the behavior into a reusable user control will take some time, too.

If you have never done drag and drop at all, have a look at this drag and drop example from the MSDN. This would be a good starting point and it should take you maybe half a day to get the thing working.

Solution 4 - C#

This relies on @BFree's answer above - thanks it helped a lot.
I ran into an error when trying to use the solution because I was using a DataSource for my listbox. Just for completeness, you get this error if you try to remove or add an item to the listbox directly:

// Causes error
this.listBox1.Items.Remove(data);

Error: System.ArgumentException: 'Items collection cannot be modified when the DataSource property is set.'

Solution: Update the datasource itself, and then rebind to your listbox. Program.SelectedReports is a BindingList.

Code:

    private void listboxSelectedReports_DragDrop(object sender, DragEventArgs e)
    {
        // Get the point where item was dropped.
        Point point = listboxSelectedReports.PointToClient(new Point(e.X, e.Y));
        // Get the index of the item where the point was dropped
        int index = this.listboxSelectedReports.IndexFromPoint(point);
        // if index is invalid, put item at the end of the list.
        if (index < 0) index = this.listboxSelectedReports.Items.Count - 1;
        // Get the item's data.
        ReportModel data = (ReportModel)e.Data.GetData(typeof(ReportModel));
     

        // Update the property we use to control sorting within the original datasource
        int newSortOrder = 0;
        foreach (ReportModel report in Program.SelectedReports) {
            // match sorted item on unique property
            if (data.Id == report.Id)
            {
                report.SortOrder = index;
                if (index == 0) {
                    // only increment our new sort order if index is 0
                    newSortOrder += 1;
                }
            } else {
                // skip our dropped item's index
                if (newSortOrder == index) {
                    newSortOrder += 1;
                }
                report.SortOrder = newSortOrder;
                newSortOrder += 1;
            }
        }

        

        // Sort original list and reset the list box datasource.
        // Note:  Tried other things, Reset(), Invalidate().  Updating DataSource was only way I found that worked??
        Program.SelectedReports = new BindingList<ReportModel>(Program.SelectedReports.OrderBy(x => x.SortOrder).ToList());
        listboxSelectedReports.DataSource = Program.SelectedReports;
        listboxSelectedReports.DisplayMember = "Name";
        listboxSelectedReports.ValueMember = "ID";
    }

Other notes: BindingList is under this namespace:

using System.ComponentModel;

When dynamically adding items to the list, make sure you populate your sorting property. I used an integer field 'SortOrder'.

When you remove an item, I don't have to worry about updating the Sorting property, as it will just create a number gap which is ok in my situation, YMMV.

To be honest, there could be a better sorting algorithm other than a foreach loop, but in my situation, I am dealing with a very limited number of items.

Solution 5 - C#

An alternative is using the list-view control, which is the control Explorer uses to display the contents of folders. It is more complicated, but implements item dragging for you.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionGareth SimpsonView Question on Stackoverflow
Solution 1 - C#BFreeView Answer on Stackoverflow
Solution 2 - C#Wally ModzView Answer on Stackoverflow
Solution 3 - C#Daniel BrücknerView Answer on Stackoverflow
Solution 4 - C#cmartinView Answer on Stackoverflow
Solution 5 - C#Simon BuchanView Answer on Stackoverflow