WPF C#: Rearrange items in listbox via drag and drop
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 g开发者_JAVA百科et it to do anything.
I would appreciate any help since I am new to the visual studios environment.
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.
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;
}
}
}
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.
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;
}
}
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);
}
}
}
}
This has helped me greatly thank you. Especially the generics version.
I made the following amendments:
Because I don't set the DataContext of the ListBox (just the ItemsSource), I use
var items = this.ItemsSource as IList<T>;
in the Move method.
And at the end of "Move" I added:
this.SelectedItem = source;
as I want the user to have the moved item as the current selection.
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"));
}
}
精彩评论