Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Moving ListViewItems Up & Down

Tags:

c#

.net

winforms

I have a ListView (WinForms) in which i want to move items up and down with the click of a button. The items to be moved are the ones who are checked. So if item 2, 6 and 9 are selected, they will become 1, 5 and 8 when I press the button for movement upwards and the items that were on those places are moved down a step.

I feel that I have made this unnecessarily complicated, as you can see below. The second SubItem of each ListViewItem is a number which represents its place in the list (starts on 1).

I blame lack of sleep and coffee for the following code, but if you could figure out a simpler way to complete this task I would be thankful.

private void sourceMoveUpButton_Click(object sender, EventArgs e)
    {
        List<Int32> affectedNumbers = new List<Int32>();
        bool foundNonChecked = false;

        List<KeyValuePair<int, ListViewItem>> newList = new List<KeyValuePair<int, ListViewItem>>();

        foreach (ListViewItem item in this.sourceListView.CheckedItems)
        {
            int newNum = int.Parse(item.SubItems[1].Text) - 1;

            if (newNum >= 1)
            {
                foreach (ListViewItem testItem in this.sourceListView.Items)
                {
                    if (int.Parse(testItem.SubItems[1].Text) == newNum && !testItem.Checked)
                    {
                        foundNonChecked = true;
                    }
                }

                if (foundNonChecked)
                {
                    item.SubItems[1].Text = newNum.ToString();
                    affectedNumbers.Add(newNum);
                }
            }
        }

        foreach (ListViewItem item in this.sourceListView.Items)
        {
            int num = int.Parse(item.SubItems[1].Text);

            if (affectedNumbers.Contains(num) && !item.Checked)
            {
                item.SubItems[1].Text = (num + affectedNumbers.Count).ToString();
            }

            newList.Add(new KeyValuePair<int, ListViewItem>(int.Parse(item.SubItems[1].Text), item));
            item.Remove();
        }

        newList.Sort((firstPair, secondPair) =>
            {
                return firstPair.Key.CompareTo(secondPair.Key);
            }
        );

        foreach (KeyValuePair<int, ListViewItem> pair in newList)
        {
            this.sourceListView.Items.Add(pair.Value);
        }
    }

EDIT I have shorted it down to the following:

foreach (ListViewItem item in this.sourceListView.CheckedItems)
        {
            if (item.Index > 0)
            {
                int newIndex = item.Index - 1;
                this.sourceListView.Items.RemoveAt(item.Index);
                this.sourceListView.Items.Insert(newIndex, item);
            }
        }

        int index = 1;
        foreach (ListViewItem item in this.sourceListView.Items)
        {
            item.SubItems[1].Text = index.ToString();

            index++;
        }

But now, if I select the two topmost items (or similar) they will switch place when I click the button for upwards movement.

SECOND EDIT
Everything works fine for upwards movement with the following:

if (this.sourceListView.CheckedItems[0].Index != 0)
        {
            this.sourceListView.BeginUpdate();

            foreach (ListViewItem item in this.sourceListView.CheckedItems)
            {
                if (item.Index > 0)
                {
                    int newIndex = item.Index - 1;
                    this.sourceListView.Items.RemoveAt(item.Index);
                    this.sourceListView.Items.Insert(newIndex, item);
                }
            }

            this.updateListIndexText();

            this.sourceListView.EndUpdate();
        }

But for downward movement I can't seem to get it right:

if (this.sourceListView.CheckedItems[this.sourceListView.CheckedItems.Count - 1].Index < this.sourceListView.Items.Count - 1)
        {
            this.sourceListView.BeginUpdate();

            foreach (ListViewItem item in this.sourceListView.CheckedItems)
            {
                if (item.Index < this.sourceListView.Items.Count - 1)
                {
                    int newIndex = item.Index + 1;
                    this.sourceListView.Items.RemoveAt(item.Index);
                    this.sourceListView.Items.Insert(newIndex, item);
                }
            }

            this.updateListIndexText();

            this.sourceListView.EndUpdate();
        }

It works for moving single items down, but when I select more than one, it doesn't.

like image 666
EClaesson Avatar asked Jul 24 '12 03:07

EClaesson


4 Answers

Try something like this:

foreach (ListViewItem lvi in sourceListView.SelectedItems)
{
    if (lvi.Index > 0)
    {
        int index = lvi.Index - 1;
        sourceListView.Items.RemoveAt(lvi.Index);
        sourceListView.Items.Insert(index, lvi);
    }
}

Basically just removes the item then inserts it above of where it used to be. The ListView automatically handles reshuffling the items down the order after an insert so no worries.

Edit: The reason the two topmost items swap is that the top item will never move (i.e I haven't implemented a wrap-around move. The 2nd item, however, is free to move and thus goes to the top of the list.

To resolve this, you can do 1 of 2 things:

  1. Implement a wrap-around reshuffle (i.e top item goes to the bottom)
  2. Prevent any movement if the top item is selected (check listview.Items[0].Selected)

As for the re-doing of the text, just do it in the original loop.

Implementation with wraparound:

foreach (ListViewItem lvi in sourceListView.SelectedItems)
{
    int index = lvi.Index > 0 ? lvi.Index - 1 : sourceListView.Items.Count - 1;
    sourceListView.Items.RemoveAt(lvi.Index);
    sourceListView.Items.Insert(index, lvi);

    if (index != sourceListView.Items.Count - 1) //not a wraparound:
    {
        //just swap the indices over.
        sourceListView.Items[index + 1].SubItems[1].Text = (index + 1).ToString();
        lvi.SubItems[1].Text = index.ToString();
    }
    else //item wrapped around, have to manually update all items.
    {
        foreach (ListViewItem lvi2 in sourceListView.Items)
            lvi2.SubItems[1].Text = lvi2.Index.ToString();
    }
}

Edit 2:

Static helper implementation, no wrap-around:

private enum MoveDirection { Up = -1, Down = 1 };

private static void MoveListViewItems(ListView sender, MoveDirection direction)
{
    int dir = (int)direction;
    int opp = dir * -1;

    bool valid = sender.SelectedItems.Count > 0 &&
                    ((direction == MoveDirection.Down && (sender.SelectedItems[sender.SelectedItems.Count - 1].Index < sender.Items.Count - 1))
                || (direction == MoveDirection.Up && (sender.SelectedItems[0].Index > 0)));

    if (valid)
    {
        foreach (ListViewItem item in sender.SelectedItems)
        {
            int index = item.Index + dir;
            sender.Items.RemoveAt(item.Index);
            sender.Items.Insert(index, item);

            sender.Items[index + opp].SubItems[1].Text = (index + opp).ToString();
            item.SubItems[1].Text = (index).ToString();
        }
    }
}

Example:

MoveListViewItems(sourceListView, MoveDirection.Up);
MoveListviewItems(sourceListview, MoveDirection.Down);
like image 122
Jason Larke Avatar answered Oct 16 '22 07:10

Jason Larke


Just to complete the @Jason Larkes answer to make it support "move down" properly, add this right before the foreach in the MoveListViewItems function he provided:

ListViewItem[] itemsToBeMoved = sender.SelectedItems.Cast<ListViewItem>().ToArray<ListViewItem>(); 
IEnumerable<ListViewItem> itemsToBeMovedEnum;
if (direction == MoveDirection.Down)
     itemsToBeMovedEnum = itemsToBeMoved.Reverse();
else
     itemsToBeMovedEnum = itemsToBeMoved;

and then iterate using:

foreach (ListViewItem item in itemsTobemovedEnum)

instead of the original foreach.

Works like a charm. @EClaesson - I hope this overcomes the issue you wrote about in the comments.

like image 25
SatA Avatar answered Oct 16 '22 08:10

SatA


This one is with wrapping, so if you move item at index 0 down it will come to last position, and if you move last item up it will be first on list:

    public static class ListExtensions
    {
        public static void MoveUp<T>(this IList<T> list, int index)
        {
            int newPosition = ((index > 0) ? index - 1 : list.Count - 1);
            var old = list[newPosition];
            list[newPosition] = list[index];
            list[index] = old;
        }

        public static void MoveDown<T>(this IList<T> list, int index)
        {
            int newPosition = ((index + 1 < list.Count) ? index + 1 : 0);
            var old = list[newPosition];
            list[newPosition] = list[index];
            list[index] = old;
        }
    }
like image 1
Dalibor Korpar Avatar answered Oct 16 '22 08:10

Dalibor Korpar


Code with wrap around:

    private enum MoveDirection { Up = -1, Down = 1 };

    private void MoveListViewItems(ListView sourceListView, MoveDirection direction)
    {
        int dir = (int)direction;

        foreach (ListViewItem lvi in sourceListView.SelectedItems)
        {
            int index = lvi.Index + dir;
            if(index >= sourceListView.Items.Count)
                index = 0;
            else if(index < 0)
                index = sourceListView.Items.Count + dir;

            sourceListView.Items.RemoveAt(lvi.Index);
            sourceListView.Items.Insert(index, lvi);
        }
    }
like image 1
Meow Avatar answered Oct 16 '22 07:10

Meow