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

Viewed 73023

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.

7 Answers

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;
    }
}

Improving upon Wiesław Šoltés's modification of dnr3's answer, I abstracted this out into an easily-reusable class, so that you can set up multiple of these lists with only a few lines of code. I also added a feature whereby each item alternates between 2 background colours, for easier viewing (though this feature can easily be removed, if you don't want it).

F.Y.I: Sorry if you're a lover of 'var', but I am absolutely not and those got removed by my IDE, and I don't intend to put them back. Of course this changes nothing about the actual program behaviour and should improve compile-time anyway, so.. winning! :p

Here is the class:

/// <typeparam name="IT">The item type to be stored in this list</typeparam>
internal class ReorderableList<IT> where IT : class
{
    private readonly SolidColorBrush m_alternator1, m_alternator2; // Background colours for the list items to alternate between
    private readonly ListBox m_ListBox; // The target ListBox we're modifying
    private readonly string m_displayMemberPath; // The name of the member in to display 
    private readonly IList<IT> m_items = new ObservableCollection<IT>();
    private Point m_cursorStartPos;

    /// <summary>
    /// Initializes the list (this must be done after components are initialized and loaded!).
    /// </summary>
    /// <param name="resourceProvider">Pass 'this' for this parameter</param>
    /// <param name="listBox">The target ListBox control to modify</param>
    /// <param name="displayMemberPath">The name of the member in the generic type contained in this list, to be displayed</param>
    public ReorderableList(ListBox listBox, string displayMemberPath, SolidColorBrush alternator1, SolidColorBrush alternator2)
    {
        m_ListBox = listBox;
        m_displayMemberPath = displayMemberPath;
        m_alternator1 = alternator1;
        m_alternator2 = alternator2;

        Initialize();
    }

    private void Initialize()
    {
        // Set the list box's items source and tell it what member in the IT class to use for the display name
        // Add an event handler for preview mouse move

        m_ListBox.DisplayMemberPath = m_displayMemberPath;
        m_ListBox.ItemsSource = m_items;
        m_ListBox.PreviewMouseMove += OnListPreviewMouseMove;

        // Create the item container style to be used by the listbox
        // Add mouse event handlers to the style

        Style style = new Style(typeof(ListBoxItem));
        style.Setters.Add(new Setter(UIElement.AllowDropProperty, true));
        style.Setters.Add(new EventSetter(UIElement.PreviewMouseLeftButtonDownEvent, new MouseButtonEventHandler(OnListPreviewMouseLeftButtonDown)));
        style.Setters.Add(new EventSetter(UIElement.DropEvent, new DragEventHandler(OnListDrop)));

        // Add triggers to alternate the background colour of each element based on its alternation index
        // (Remove this, as well as the two SolidColorBrush resources if you don't want this feature)

        Trigger trigger1 = new Trigger()
        {
            Property = ItemsControl.AlternationIndexProperty,
            Value = 0
        };

        Setter setter1 = new Setter()
        {
            Property = Control.BackgroundProperty,
            Value = m_alternator1
        };

        trigger1.Setters.Add(setter1);
        style.Triggers.Add(trigger1);

        Trigger trigger2 = new Trigger()
        {
            Property = ItemsControl.AlternationIndexProperty,
            Value = 1
        };

        Setter setter2 = new Setter()
        {
            Property = Control.BackgroundProperty,
            Value = m_alternator2
        };

        trigger2.Setters.Add(setter2);
        style.Triggers.Add(trigger2);

        // Set the item container style

        m_ListBox.ItemContainerStyle = style;
    }

    /// <summary>
    /// Adds an item to the list. If [ignoreDuplicates] is false and the item is already in the list,
    /// the item won't be added.
    /// </summary>
    /// <param name="item">The item to add</param>
    /// <param name="ignoreDuplicates">Whether or not to add the item regardless of whether it's already in the list</param>
    /// <returns>Whether or not the item was added</returns>
    public bool Add(IT item, bool ignoreDuplicates = true)
    {
        if (!ignoreDuplicates && Contains(item)) return false;

        m_items.Add(item);
        return true;
    }

    /// <summary>
    /// Removes an item from the list.
    /// </summary>
    /// <param name="item">The item to remove</param>
    /// <returns>Whether or not the item was removed from the list. This will be false if the item was not in the list to begin with.</returns>
    public bool Remove(IT item)
    {
        if (Contains(item)) return false;

        m_items.Remove(item);
        return true;
    }

    /// <summary>
    /// Returns whether or not the list contains the given item.
    /// </summary>
    /// <param name="item">The item to check for</param>
    /// <returns>Whether or not the list contains the given item.</returns>
    public bool Contains(IT item)
    {
        return m_items.Contains(item);
    }

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

    private void OnListPreviewMouseMove(object sender, MouseEventArgs e)
    {
        Point currentCursorPos = e.GetPosition(null);
        Vector cursorVector = m_cursorStartPos - currentCursorPos;

        if (e.LeftButton == MouseButtonState.Pressed
            &&(Math.Abs(cursorVector.X) > SystemParameters.MinimumHorizontalDragDistance
            || Math.Abs(cursorVector.Y) > SystemParameters.MinimumVerticalDragDistance))
        {
            ListBoxItem targetItem = FindVisualParent<ListBoxItem>(((DependencyObject)e.OriginalSource));
            if (targetItem != null)
            {
                DragDrop.DoDragDrop(targetItem, targetItem.DataContext, DragDropEffects.Move);
            }
        }
    }

    private void OnListDrop(object sender, DragEventArgs e)
    {
        if (sender is ListBoxItem item)
        {
            IT source = e.Data.GetData(typeof(IT)) as IT;
            IT target = item.DataContext as IT;

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

            Move(source, sourceIndex, targetIndex);
        }
    }

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

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

        return FindVisualParent<T>(parentObject);
    }
}

And here is an example IT (item type) class (same as in the original answer):

public class ExampleItem
{
    public string Name { get; set; }

    public ExampleItem(string name)
    {
        Name = name;
    }
}

And finally, the usage:

public partial class MainWindow : Window
{
    private readonly ReorderableList<ExampleItem> ExampleList;

    public MainWindow()
    {
        InitializeComponent();

        ExampleList = new ReorderableList<ExampleItem>(myXamlListBoxControl, "Name",
            FindResource("AlternatingBG1") as SolidColorBrush,
            FindResource("AlternatingBG2") as SolidColorBrush);

        ExampleList.Add(new ExampleItem("Test Item 1"));
        ExampleList.Add(new ExampleItem("Test Item 2"));
        ExampleList.Add(new ExampleItem("Test Item 3"));
        ExampleList.Add(new ExampleItem("Test Item 4"));
        ExampleList.Add(new ExampleItem("Test Item 5"));
        ExampleList.Add(new ExampleItem("Test Item 6"));
    }
}
Related