WPF C#: Rearrange items in listbox via drag and drop

69,573

Solution 1

I've tried creating one using ObservableCollection. Have a look.

    ObservableCollection<Emp> _empList = new ObservableCollection<Emp>();

    public Window1()
    {
        InitializeComponent();

        _empList .Add(new Emp("1", 22));
        _empList .Add(new Emp("2", 18));
        _empList .Add(new Emp("3", 29));
        _empList .Add(new Emp("4", 9));
        _empList .Add(new Emp("5", 29));
        _empList .Add(new Emp("6", 9));
        listbox1.DisplayMemberPath = "Name";
        listbox1.ItemsSource = _empList;
        
        Style itemContainerStyle = new Style(typeof(ListBoxItem));
        itemContainerStyle.Setters.Add(new Setter(ListBoxItem.AllowDropProperty, true));
        itemContainerStyle.Setters.Add(new EventSetter(ListBoxItem.PreviewMouseLeftButtonDownEvent, new MouseButtonEventHandler(s_PreviewMouseLeftButtonDown)));
        itemContainerStyle.Setters.Add(new EventSetter(ListBoxItem.DropEvent, new DragEventHandler(listbox1_Drop)));
        listbox1.ItemContainerStyle = itemContainerStyle;
    }

Drag and drop process:

    void s_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {

        if (sender is ListBoxItem)
        {
            ListBoxItem draggedItem = sender as ListBoxItem;
            DragDrop.DoDragDrop(draggedItem, draggedItem.DataContext, DragDropEffects.Move);
            draggedItem.IsSelected = true;
        }
    }

    void listbox1_Drop(object sender, DragEventArgs e)
    {
        Emp droppedData = e.Data.GetData(typeof(Emp)) as Emp;
        Emp target = ((ListBoxItem)(sender)).DataContext as Emp;

        int removedIdx = listbox1.Items.IndexOf(droppedData);
        int targetIdx = listbox1.Items.IndexOf(target);

        if (removedIdx < targetIdx)
        {
            _empList.Insert(targetIdx + 1, droppedData);
            _empList.RemoveAt(removedIdx);
        }
        else
        {
            int remIdx = removedIdx+1;
            if (_empList.Count + 1 > remIdx)
            {
                _empList.Insert(targetIdx, droppedData);
                _empList.RemoveAt(remIdx);
            }
        }
    }

Note:

  • One thing that sucks in this implementation is that since it uses the PreviewMouseLeftButtonDown event, the dragged item does not look like a selected item.
  • And also for an easier implementation, the drop target is the list box items and not the listbox itself - might need a better solution for this.

Solution 2

Using dnr3's answers I have created version with fixed selection issues.

Window1.xaml

<Window x:Class="ListBoxReorderDemo.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ListBoxReorderDemo" Height="300" Width="300"
        WindowStartupLocation="CenterScreen">
    <Grid>
        <ListBox x:Name="listBox"/>
    </Grid>
</Window>

Window1.xaml.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;

namespace ListBoxReorderDemo
{
    public class Item
    {
        public string Name { get; set; }
        public Item(string name)
        {
            this.Name = name;
        }
    }

    public partial class Window1 : Window
    {
        private Point _dragStartPoint;

        private T FindVisualParent<T>(DependencyObject child)
            where T : DependencyObject
        {
            var parentObject = VisualTreeHelper.GetParent(child);
            if (parentObject == null)
                return null;
            T parent = parentObject as T;
            if (parent != null)
                return parent;
            return FindVisualParent<T>(parentObject);
        }

        private IList<Item> _items = new ObservableCollection<Item>();

        public Window1()
        {
            InitializeComponent();

            _items.Add(new Item("1"));
            _items.Add(new Item("2"));
            _items.Add(new Item("3"));
            _items.Add(new Item("4"));
            _items.Add(new Item("5"));
            _items.Add(new Item("6"));

            listBox.DisplayMemberPath = "Name";
            listBox.ItemsSource = _items;

            listBox.PreviewMouseMove += ListBox_PreviewMouseMove;

            var style = new Style(typeof(ListBoxItem));
            style.Setters.Add(new Setter(ListBoxItem.AllowDropProperty, true));
            style.Setters.Add(
                new EventSetter(
                    ListBoxItem.PreviewMouseLeftButtonDownEvent,
                    new MouseButtonEventHandler(ListBoxItem_PreviewMouseLeftButtonDown)));
            style.Setters.Add(
                    new EventSetter(
                        ListBoxItem.DropEvent, 
                        new DragEventHandler(ListBoxItem_Drop)));
            listBox.ItemContainerStyle = style;
        }

        private void ListBox_PreviewMouseMove(object sender, MouseEventArgs e)
        {
            Point point = e.GetPosition(null);
            Vector diff = _dragStartPoint - point;
            if (e.LeftButton == MouseButtonState.Pressed &&
                (Math.Abs(diff.X) > SystemParameters.MinimumHorizontalDragDistance ||
                    Math.Abs(diff.Y) > SystemParameters.MinimumVerticalDragDistance))
            {
                var lb = sender as ListBox;
                var lbi = FindVisualParent<ListBoxItem>(((DependencyObject)e.OriginalSource));
                if (lbi != null)
                {
                    DragDrop.DoDragDrop(lbi, lbi.DataContext, DragDropEffects.Move);
                }
            }
        }
        private void ListBoxItem_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            _dragStartPoint = e.GetPosition(null);
        }

        private void ListBoxItem_Drop(object sender, DragEventArgs e)
        {
            if (sender is ListBoxItem)
            {
                var source = e.Data.GetData(typeof(Item)) as Item;
                var target = ((ListBoxItem)(sender)).DataContext as Item;

                int sourceIndex = listBox.Items.IndexOf(source);
                int targetIndex = listBox.Items.IndexOf(target);

                Move(source, sourceIndex, targetIndex);
            }
        }

        private void Move(Item source, int sourceIndex, int targetIndex)
        {
            if (sourceIndex < targetIndex)
            {
                _items.Insert(targetIndex + 1, source);
                _items.RemoveAt(sourceIndex);
            }
            else
            {
                int removeIndex = sourceIndex + 1;
                if (_items.Count + 1 > removeIndex)
                {
                    _items.Insert(targetIndex, source);
                    _items.RemoveAt(removeIndex);
                }
            }
        }
    }
}

Version with support for generics and data binding.

Window1.xaml

<Window x:Class="ListBoxReorderDemo.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:ListBoxReorderDemo"
        Title="ListBoxReorderDemo" Height="300" Width="300"
        WindowStartupLocation="CenterScreen">
    <Grid>
        <local:ItemDragAndDropListBox x:Name="listBox" ItemsSource="{Binding}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Name}"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </local:ItemDragAndDropListBox>
    </Grid>
</Window>

Window1.xaml.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;

namespace ListBoxReorderDemo
{
    public class DragAndDropListBox<T> : ListBox 
        where T : class
    {
        private Point _dragStartPoint;

        private P FindVisualParent<P>(DependencyObject child) 
            where P : DependencyObject
        {
            var parentObject = VisualTreeHelper.GetParent(child);
            if (parentObject == null)
                return null;

            P parent = parentObject as P;
            if (parent != null)
                return parent;

            return FindVisualParent<P>(parentObject);
        }

        public DragAndDropListBox()
        {
            this.PreviewMouseMove += ListBox_PreviewMouseMove;

            var style = new Style(typeof(ListBoxItem));

            style.Setters.Add(new Setter(ListBoxItem.AllowDropProperty, true));

            style.Setters.Add(
                new EventSetter(
                    ListBoxItem.PreviewMouseLeftButtonDownEvent,
                    new MouseButtonEventHandler(ListBoxItem_PreviewMouseLeftButtonDown)));

            style.Setters.Add(
                    new EventSetter(
                        ListBoxItem.DropEvent, 
                        new DragEventHandler(ListBoxItem_Drop)));

            this.ItemContainerStyle = style;
        }

        private void ListBox_PreviewMouseMove(object sender, MouseEventArgs e)
        {
            Point point = e.GetPosition(null);
            Vector diff = _dragStartPoint - point;
            if (e.LeftButton == MouseButtonState.Pressed &&
                (Math.Abs(diff.X) > SystemParameters.MinimumHorizontalDragDistance ||
                    Math.Abs(diff.Y) > SystemParameters.MinimumVerticalDragDistance))
            {
                var lb = sender as ListBox;
                var lbi = FindVisualParent<ListBoxItem>(((DependencyObject)e.OriginalSource));
                if (lbi != null)
                {
                    DragDrop.DoDragDrop(lbi, lbi.DataContext, DragDropEffects.Move);
                }
            }
        }

        private void ListBoxItem_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            _dragStartPoint = e.GetPosition(null);
        }

        private void ListBoxItem_Drop(object sender, DragEventArgs e)
        {
            if (sender is ListBoxItem)
            {
                var source = e.Data.GetData(typeof(T)) as T;
                var target = ((ListBoxItem)(sender)).DataContext as T;

                int sourceIndex = this.Items.IndexOf(source);
                int targetIndex = this.Items.IndexOf(target);

                Move(source, sourceIndex, targetIndex);
            }
        }

        private void Move(T source, int sourceIndex, int targetIndex)
        {
            if (sourceIndex < targetIndex)
            {
                var items = this.DataContext as IList<T>;
                if (items != null)
                {
                    items.Insert(targetIndex + 1, source);
                    items.RemoveAt(sourceIndex);
                }
            }
            else
            {
                var items = this.DataContext as IList<T>;
                if (items != null)
                {
                    int removeIndex = sourceIndex + 1;
                    if (items.Count + 1 > removeIndex)
                    {
                        items.Insert(targetIndex, source);
                        items.RemoveAt(removeIndex);
                    }
                }
            }
        }
    }

    public class Item
    {
        public string Name { get; set; }
        public Item(string name)
        {
            this.Name = name;
        }
    }

    public class ItemDragAndDropListBox : DragAndDropListBox<Item> { }

    public partial class Window1 : Window
    {
        private IList<Item> _items = new ObservableCollection<Item>();

        public Window1()
        {
            InitializeComponent();

            _items.Add(new Item("1"));
            _items.Add(new Item("2"));
            _items.Add(new Item("3"));
            _items.Add(new Item("4"));
            _items.Add(new Item("5"));
            _items.Add(new Item("6"));

            listBox.DataContext = _items;
        }
    }
}

Solution 3

I would suggest using the drag and drop behavior called GongSolutions.WPF.DragDrop. It allows MVVM style use cases using attached property setters to enable it, no need for code behind in your views. You should check out the link for a simple example.

Solution 4

I took dnr3's answer and altered it for implementation in XAML. Same result, just prefer doing what I can in XAML rather than in the code-behind.

In place of the code-behind:

Style itemContainerStyle = new Style(typeof(ListBoxItem));
itemContainerStyle.Setters.Add(new Setter(AllowDropProperty, true));
itemContainerStyle.Setters.Add(new EventSetter(PreviewMouseLeftButtonDownEvent, new MouseButtonEventHandler(s_PreviewMouseLeftButtonDown)));
itemContainerStyle.Setters.Add(new EventSetter(DropEvent, new DragEventHandler(listbox1_Drop)));
listbox1.ItemContainerStyle = itemContainerStyle;

Put this in the XAML:

<Window.Resources>
    <Style x:Key="ListBoxDragDrop" TargetType="{x:Type ListBoxItem}">
        <Setter Property="AllowDrop" Value="true"/>
        <EventSetter Event="PreviewMouseMove" Handler="s_PreviewMouseMoveEvent"/>
        <EventSetter Event="Drop" Handler="listbox1_Drop"/>
    </Style>
</Window.Resources>
<Grid>
    <ListBox x:Name="listbox1" ItemContainerStyle="{StaticResource ListBoxDragDrop}" HorizontalAlignment="Left" Height="299" Margin="10,10,0,0" VerticalAlignment="Top" Width="224"/>
</Grid>

This is mouse-handler placed in the code-behind of the XAML.

void s_PreviewMouseMoveEvent(object sender, MouseEventArgs e)
{
    if (sender is ListBoxItem && e.LeftButton == MouseButtonState.Pressed)
    {
        ListBoxItem draggedItem = sender as ListBoxItem;
        DragDrop.DoDragDrop(draggedItem, draggedItem.DataContext, DragDropEffects.Move);
        draggedItem.IsSelected = true;
    }
}

Solution 5

Repair code :

private void listbox1_Drop(object sender, DragEventArgs e)
{
    if (sender is ListBoxItem)
    {
        Emp droppedData = e.Data.GetData(typeof(Emp)) as Emp;
        Emp target = ((ListBoxItem)(sender)).DataContext as Emp;

        int removedIdx = listbox1.Items.IndexOf(droppedData);
        int targetIdx = listbox1.Items.IndexOf(target);

        if (removedIdx < targetIdx)
        {
            _empList.Insert(targetIdx + 1, droppedData);
            _empList.RemoveAt(removedIdx);
        }
        else
        {
            int remIdx = removedIdx + 1;
            if (_empList.Count + 1 > remIdx)
            {
                _empList.Insert(targetIdx, droppedData);
                _empList.RemoveAt(remIdx);
            }
        }
    }
}
Share:
69,573
Aaron
Author by

Aaron

Updated on July 08, 2022

Comments

  • Aaron
    Aaron almost 2 years

    I am trying to figure out how to move the items in a pre-populated listbox up and down via mouse drags.

    I have looked at the Control.DoDragDrop method from microsoft's api, but I still can't get it to do anything.

    I would appreciate any help since I am new to the visual studios environment.

  • Charles
    Charles about 11 years
    If you change PreviewMouseLeftButtonDown to PreviewMouseMoveEvent, and then add e.LeftButton == MouseButtonState.Pressed to your if statement, you fix the selection issue.
  • Andrew
    Andrew over 8 years
    I needed to add listbox1.Items.Refresh(); to the end of the Drop handler, but other than, it works great!
  • dnr3
    dnr3 over 8 years
    @Andrew, do you use observablecollection? if you use it, it should be auto update
  • Andrew
    Andrew over 8 years
    You are correct, in the process of changing the class, I changed the collection to a List without thinking about it. However, I have a button that changes the evaluation of the .ToString() method for the selected item. I did still need listbox1.Items.Refresh(); to refresh the text.
  • premes
    premes over 8 years
    This is very nice indeed, thanks to you and @dnr3. The only thing I could see that could improve it is if the cursor, when dragged, could hover over the part of the listbox that has no files. As it is, the cursor changes to a null set, no-drag symbol if you drag too low. And I suppose if you could drag multiple items. Not that I'm complaining, this is more than enough to get started.
  • AdvApp
    AdvApp over 6 years
    This works -- thanks. But why doesn't it work replacing the code (that sets the ItemContainerStyle) with the respective XAML tags -- AllowDrop="True" PreviewMouseLeftButtonDown="s_PreviewMouseLeftButtonDown" Drop="listbox1_Drop" -- ?
  • AdvApp
    AdvApp over 6 years
    @IronRod - Figured this out -- see additional answer
  • tCoe
    tCoe about 6 years
    I know this is a little late in the game, but how did you initialize listbox1 in the code behind? this is my current hangup in my code.
  • dnr3
    dnr3 about 6 years
    @tCoe - what do you mean by initialize? you can do a ListBox listbox1 = new ListBox(), but you can do it in xaml
  • tCoe
    tCoe about 6 years
    @dnr3 I am trying to implement the code above in WPF using MVVM Lite. So i had to make adjustments here and there, but mainly having trouble binding the listbox_1Drop to the listbox view
  • dnr3
    dnr3 about 6 years
    @tCoe - do you mean you want to subscribe the drop event in the xaml instead of code behind? check the answer from IronRod on how to do that below. But if you're thinking of no code behind at all then I think you'll have to make it as a behavior or something.
  • tCoe
    tCoe about 6 years
    @dnr3 I have a dynamically created collection in the VM that populates on load. This would be the _empList in your example. If i were to put the above code in the code-behind how do i bind to that list, or connect to that list once its populated in the view?
  • tCoe
    tCoe about 6 years
    I keep getting the error "No Overload for s_PreviewMouseLeftButtonDown matches delegate MouseEventHandler
  • tCoe
    tCoe about 6 years
    Could you share what your Handler looks like for the Mouse Button Event handler?
  • dnr3
    dnr3 about 6 years
    @tCoe assigning collection to a listbox is by simply put assigned it to the itemssource property (listbox1.ItemsSource = _empList;). about the error, I think you're using a worng event, on my code I use the PreviewMouseLeftButtonDownEvent which is a MouseButtonEventHandler and not MouseEventHandler
  • jeuxjeux20
    jeuxjeux20 over 5 years
    You could use ObservableCollection.Move instead of InsertAt and RemoveAt
  • rawpower
    rawpower over 5 years
    While I generally don't like adding libraries for such a narrow use-case (drag and drop) the one you've linked to appears to be very well thought out and complete. At the same time, the amount of code for proper drag & drop is surprisingly large if one wants a decent, complete implementation. I actually might make an exception and use the library you've linked to. Thanks!
  • Nik
    Nik over 5 years
    This library is really nice! I'd recommend this to anyone needing to use drag-and-drop for anything more complicated than the basics, and even then.
  • christopher_h
    christopher_h almost 4 years
    great library very simple
  • R1PFake
    R1PFake almost 4 years
    I had a small issue with this solution, because you store the drag position when the mouse button is clicked, but you search the item in the mouse move. This means that in some cases the item you find in the mouse move event is different than the actual clicked item if you click on the edge of an item. I changed to logic to search the clicked item in the mouse down event and store it in a variable and in the mouse move event I used the previously stored clicked item to prevent this edge case
  • Haukinger
    Haukinger almost 4 years
    @rawpower I agree that this library is great, and I'd like to add I very much prefer adding a library with a narrow use-case over libraries that try to fix or enhance everything. That only works if you use only one library, but try DevExpress together with Telerik and Infragistics and Prism for example...