Reputation: 1743
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.
Upvotes: 76
Views: 84959
Reputation: 61
I think that Arthur Edgarov solution is best since it allows using MVVM and xaml correctly.
I have changed the behaviour class to accommodate to items that have different size (by moving the mouse cursor to the dragged item) and fixed the calculation of the scroll feature to work using the ScrollViewer.ScrollableHeight as reference instead of the behaviour ScrolAreaHeight property.
using Microsoft.Xaml.Behaviors;
using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
namespace WPFUtils
{
/// <summary>
/// https://stackoverflow.com/questions/3350187/wpf-c-rearrange-items-in-listbox-via-drag-and-drop
/// (Arthur Edgarov answer)
///
/// The class allows you to use drag & drop of item in an ItemsControl.
///
/// How to use:
/// -----------
/// You must use the nuget "Microsoft.Xaml.Behaviors.Wpf" in the project that
/// define the xaml.
///
/// xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
/// ListBox x:Name="myListBox"
/// ItemsSource="{Binding MyItems}">
/// i:Interaction.Behaviors>
/// <local:DragAndDropRearrangeBehavior />
/// / i:Interaction.Behaviors>
/// /ListBox>
/// </summary>
public sealed class DragAndDropRearrangeBehavior : Behavior<ItemsControl>
{
#region Dependency properties
public static readonly DependencyProperty ScrollOffsetProperty
= DependencyProperty.Register(nameof(ScrollOffset),
typeof(double),
typeof(DragAndDropRearrangeBehavior),
new PropertyMetadata(5.0));
#endregion
#region Fields
private DependencyPropertyDescriptor _itemsSourcePorpertyDescriptor;
private Style _oldItemContainerStyle;
private MethodInfo _moveMethod;
private Point _startDragVisualMousePos;
#endregion
#region Properties
public double ScrollOffset
{
get => (double)GetValue(ScrollOffsetProperty);
set => SetValue(ScrollOffsetProperty, value);
}
//[MemberNotNullWhen(true, nameof(_moveMethod))]
private bool IsMoveMethodAvailable { get; set; }
#endregion
#region Protected methods
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += OnItemControlLoaded;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.PreviewDragOver -= OnItemsControlDragOver;
_itemsSourcePorpertyDescriptor?.RemoveValueChanged(AssociatedObject, OnItemsControlItemsSourceChanged);
_itemsSourcePorpertyDescriptor = null;
AssociatedObject.ItemContainerStyle = _oldItemContainerStyle;
}
#endregion
#region Private methods
private void OnItemControlLoaded(object sender, RoutedEventArgs e)
{
if (AssociatedObject.ItemsSource is null)
return;
_oldItemContainerStyle = AssociatedObject.ItemContainerStyle;
var newItemContainerStyle = new Style(typeof(FrameworkElement), AssociatedObject.ItemContainerStyle);
newItemContainerStyle.Setters.Add(new Setter(UIElement.AllowDropProperty, true));
newItemContainerStyle.Setters.Add(new EventSetter(UIElement.PreviewMouseMoveEvent, new MouseEventHandler(OnItemMouseMove)));
newItemContainerStyle.Setters.Add(new EventSetter(UIElement.DragOverEvent, new DragEventHandler(OnItemOver)));
newItemContainerStyle.Setters.Add(new EventSetter(UIElement.DragEnterEvent, new DragEventHandler(OnItemStartDrag)));
newItemContainerStyle.Setters.Add(new EventSetter(UIElement.DropEvent, new DragEventHandler(OnItemDrop)));
AssociatedObject.ItemContainerStyle = newItemContainerStyle;
AssociatedObject.PreviewDragOver += OnItemsControlDragOver;
_itemsSourcePorpertyDescriptor = DependencyPropertyDescriptor.FromProperty(ItemsControl.ItemsSourceProperty, AssociatedObject.GetType());
_itemsSourcePorpertyDescriptor.AddValueChanged(AssociatedObject, OnItemsControlItemsSourceChanged);
OnItemsControlItemsSourceChanged(null, EventArgs.Empty);
AssociatedObject.Loaded -= OnItemControlLoaded;
}
// When `ItemsSource` property is changed - we need to check whether we
// can use the `Move` method or not.
private void OnItemsControlItemsSourceChanged(object sender, EventArgs e)
{
_moveMethod = null;
if (AssociatedObject.ItemsSource == null)
return;
_moveMethod = GetMoveMethod(AssociatedObject.ItemsSource);
IsMoveMethodAvailable = _moveMethod != null;
}
static bool IsUpScroll(double verticalPosition, double scrollAreaHeight)
=> verticalPosition < scrollAreaHeight;
static void ScrollUp(ScrollViewer scrollViewer, double offset)
=> scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - offset);
static bool IsDownScroll(double scrollViewerParentHeight, double verticalPosition, double scrollAreaHeight)
=> verticalPosition > scrollViewerParentHeight - scrollAreaHeight;
static void ScrollDown(ScrollViewer scrollViewer, double offset)
=> scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset + offset);
// Performs scrolling while dragging.
private void OnItemsControlDragOver(object sender, DragEventArgs e)
{
ItemsControl itemsControl = sender as ItemsControl;
if (itemsControl == null)
return;
var verticalPosition = e.GetPosition(itemsControl).Y;
var scrollViewer = itemsControl.GetFirstChildOfType<ScrollViewer>();
UpdateScroll(scrollViewer, verticalPosition, itemsControl);
scrollViewer = itemsControl.GetFirstParentOfType<ScrollViewer>();
UpdateScroll(scrollViewer, verticalPosition, itemsControl);
}
private void UpdateScroll(ScrollViewer scrollViewer, double verticalPosition, ItemsControl itemsControl)
{
if (scrollViewer is null)
return;
var scrollHeight = scrollViewer.ScrollableHeight;
if (IsUpScroll(verticalPosition, scrollHeight))
ScrollUp(scrollViewer, ScrollOffset);
else if (IsDownScroll(itemsControl.ActualHeight, verticalPosition, scrollHeight))
ScrollDown(scrollViewer, ScrollOffset);
}
[DllImport("User32.dll")]
private static extern bool SetCursorPos(int X, int Y);
public struct POINT
{
public int X;
public int Y;
public POINT(int x, int y)
{
this.X = x;
this.Y = y;
}
}
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetCursorPos(out POINT pPoint);
bool inDragMode = false;
private void OnItemStartDrag(object sender, DragEventArgs e)
{
// since this method is called whenever the mouse is starting
// over an item, no matter if in drag mode, we only want
// the mouse position of the object that the user started dragging.
if (inDragMode)
return;
var vis = sender as Visual;
if (vis == null)
return;
inDragMode = true;
POINT p;
GetCursorPos(out p);
var startDragMousePos = new Point(p.X, p.Y);
_startDragVisualMousePos = vis.PointFromScreen(startDragMousePos);
}
private void OnItemDrop(object sender, DragEventArgs e)
{
inDragMode = false;
_insideTimer = false;
}
bool _insideTimer = false;
private void OnItemOver(object sender, DragEventArgs e)
{
// do not handle rearranging items
// if the previous hasn't done yet.
if (_insideTimer)
return;
IList items = AssociatedObject.ItemsSource as IList;
if (items == null)
return;
var draggedData = e.Data.GetData(DataFormats.Serializable);
var hoveredData = ((FrameworkElement)sender).DataContext;
if (!RearrangeItems(items, draggedData, hoveredData))
return;
int draggedIdx = items.IndexOf(draggedData);
int hoveredIdx = items.IndexOf(hoveredData);
_insideTimer = true;
// create a timer to let the visual objects of the new positioned
// items to be created.
DispatcherTimer timer = new DispatcherTimer();
timer.Tick += ((s, ee) =>
{
// incase of a bug (that I can't think of right now)
// the timer will stop when OnItemDrop() is called
if (!_insideTimer)
{
timer.Stop();
return;
}
// make sure both items visuals were created
var draggedVis = AssociatedObject.ItemContainerGenerator.ContainerFromIndex(draggedIdx);
if (draggedVis == null)
return;
var hoveredVis = AssociatedObject.ItemContainerGenerator.ContainerFromIndex(hoveredIdx);
if (hoveredVis == null)
return;
// reposition mouse on the same place as was when starting dragging
// so that the mouse will always be on the dragged item.
// This is needed when the visual items have different sizes.
var srcVisual = draggedVis as Visual;
var mousePos = srcVisual.PointToScreen(_startDragVisualMousePos);
_insideTimer = false;
timer.Stop();
SetCursorPos((int)mousePos.X, (int)mousePos.Y);
});
timer.Interval = TimeSpan.FromMilliseconds(1);
timer.Start();
}
private void OnItemMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed)
return;
FrameworkElement draggedItem = sender as FrameworkElement;
if (draggedItem == null)
return;
try
{
DragDrop.DoDragDrop(draggedItem, new DataObject(DataFormats.Serializable, draggedItem.DataContext), DragDropEffects.Move);
}
catch (Exception ee)
{
}
}
// Performs the rearrangement of the items, whether my `Move` method or
// `Insert` + `Remove`.
private bool RearrangeItems(IList list, object draggedData, object hoveredData)
{
if (draggedData == hoveredData)
return false;
if (IsMoveMethodAvailable)
{
var draggedDataIndex = list.IndexOf(draggedData);
var hoveredDataIndex = list.IndexOf(hoveredData);
if (draggedDataIndex != -1 && hoveredDataIndex != -1)
_moveMethod.Invoke(AssociatedObject.ItemsSource, new object[] { draggedDataIndex, hoveredDataIndex });
}
else
{
var draggedDataIndex = list.IndexOf(draggedData);
var hoveredDataIndex = list.IndexOf(hoveredData);
if (draggedDataIndex < hoveredDataIndex)
{
list.Insert(hoveredDataIndex + 1, draggedData);
list.RemoveAt(draggedDataIndex);
}
else
{
draggedDataIndex++;
if (list.Count + 1 > draggedDataIndex)
{
list.Insert(hoveredDataIndex, draggedData);
list.RemoveAt(draggedDataIndex);
}
}
}
return true;
}
// This method is required to maintain the item's selection state while dragging.
private static MethodInfo GetMoveMethod(IEnumerable list)
{
var listType = list.GetType();
var integerType = typeof(int);
var methodName = nameof(ObservableCollection<object>.Move);
return listType.GetMethod(methodName, new Type[] {
integerType, integerType });
}
#endregion
}
}
Upvotes: 1
Reputation: 731
A bit more effort from my side.
Here is the solution which does the following:
ObservableCollection
).ItemsControl
.One disadvantage - it works only if ItemsSource
property is in use. But, I believe, it covers most of the scenarios.
Code:
using Microsoft.Xaml.Behaviors;
using System.Collections;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace MyNamespace.Behaviors;
public sealed class DragAndDropRearrangeBehavior : Behavior<ItemsControl>
{
#region Dependency properties
public static readonly DependencyProperty ScrollOffsetProperty
= DependencyProperty.Register(nameof(ScrollOffset),
typeof(double),
typeof(DragAndDropRearrangeBehavior),
new(5d));
public static readonly DependencyProperty ScrolAreaHeightProperty
= DependencyProperty.Register(nameof(ScrolAreaHeight),
typeof(double),
typeof(DragAndDropRearrangeBehavior),
new(60d));
#endregion
#region Fields
private DependencyPropertyDescriptor? _itemsSourcePorpertyDescriptor;
private Style? _oldItemContainerStyle;
private MethodInfo? _moveMethod;
#endregion
#region Properties
public double ScrollOffset
{
get => (double)GetValue(ScrollOffsetProperty);
set => SetValue(ScrollOffsetProperty, value);
}
public double ScrolAreaHeight
{
get => (double)GetValue(ScrolAreaHeightProperty);
set => SetValue(ScrolAreaHeightProperty, value);
}
[MemberNotNullWhen(true, nameof(_moveMethod))]
private bool IsMoveMethodAvailable { get; set; }
#endregion
#region Protected methods
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += OnItemControlLoaded;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.PreviewDragOver -= OnItemsControlDragOver;
_itemsSourcePorpertyDescriptor?.RemoveValueChanged(AssociatedObject, OnItemsControlItemsSourceChanged);
_itemsSourcePorpertyDescriptor = null;
AssociatedObject.ItemContainerStyle = _oldItemContainerStyle;
}
#endregion
#region Private methods
private void OnItemControlLoaded(object sender, RoutedEventArgs e)
{
if (AssociatedObject.ItemsSource is null)
return;
_oldItemContainerStyle = AssociatedObject.ItemContainerStyle;
var newItemContainerStyle = new Style(typeof(FrameworkElement), AssociatedObject.ItemContainerStyle);
newItemContainerStyle.Setters.Add(new Setter(UIElement.AllowDropProperty, true));
newItemContainerStyle.Setters.Add(new EventSetter(UIElement.PreviewMouseMoveEvent, new MouseEventHandler(OnItemMouseMove)));
newItemContainerStyle.Setters.Add(new EventSetter(UIElement.DragOverEvent, new DragEventHandler(OnItemDrag)));
AssociatedObject.ItemContainerStyle = newItemContainerStyle;
AssociatedObject.PreviewDragOver += OnItemsControlDragOver;
_itemsSourcePorpertyDescriptor = DependencyPropertyDescriptor.FromProperty(ItemsControl.ItemsSourceProperty, AssociatedObject.GetType());
_itemsSourcePorpertyDescriptor.AddValueChanged(AssociatedObject, OnItemsControlItemsSourceChanged);
OnItemsControlItemsSourceChanged(null, EventArgs.Empty);
AssociatedObject.Loaded -= OnItemControlLoaded;
}
// When `ItemsSource` property is changed - we need to check whether we
// can use the `Move` method or not.
private void OnItemsControlItemsSourceChanged(object? sender, EventArgs e)
{
_moveMethod = GetMoveMethod(AssociatedObject.ItemsSource);
IsMoveMethodAvailable = _moveMethod is not null;
}
// Performs scrolling while dragging.
private void OnItemsControlDragOver(object sender, DragEventArgs e)
{
static bool IsUpScroll(double verticalPosition, double scrollAreaHeight)
=> verticalPosition < scrollAreaHeight;
static void ScrollUp(ScrollViewer scrollViewer, double offset)
=> scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - offset);
static bool IsDownScroll(double scrollViewerParentHeight, double verticalPosition, double scrollAreaHeight)
=> verticalPosition > scrollViewerParentHeight - scrollAreaHeight;
static void ScrollDown(ScrollViewer scrollViewer, double offset)
=> scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset + offset);
if (sender is not ItemsControl itemsControl)
return;
var scrollViewer = itemsControl.GetFirstChildOfType<ScrollViewer>();
if (scrollViewer is null)
return;
var verticalPosition = e.GetPosition(itemsControl).Y;
if (IsUpScroll(verticalPosition, ScrolAreaHeight))
ScrollUp(scrollViewer, ScrollOffset);
else if (IsDownScroll(itemsControl.ActualHeight, verticalPosition, ScrolAreaHeight))
ScrollDown(scrollViewer, ScrollOffset);
}
private void OnItemDrag(object sender, DragEventArgs e)
{
if (AssociatedObject.ItemsSource is not IList items)
return;
var draggedData = e.Data.GetData(DataFormats.Serializable);
var hoveredData = ((FrameworkElement)sender).DataContext;
RearrangeItems(items, draggedData, hoveredData);
}
private void OnItemMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton is not MouseButtonState.Pressed)
return;
if (sender is not FrameworkElement draggedItem)
return;
DragDrop.DoDragDrop(draggedItem, new DataObject(DataFormats.Serializable, draggedItem.DataContext), DragDropEffects.Move);
}
// Performs the rearrangement of the items, whether my `Move` method or
// `Insert` + `Remove`.
private void RearrangeItems(IList list, object draggedData, object hoveredData)
{
if (IsMoveMethodAvailable)
{
if (draggedData == hoveredData)
return;
var draggedDataIndex = list.IndexOf(draggedData);
var hoveredDataIndex = list.IndexOf(hoveredData);
if (draggedDataIndex is not -1 && hoveredDataIndex is not -1)
_moveMethod.Invoke(AssociatedObject.ItemsSource, [draggedDataIndex, hoveredDataIndex]);
}
else
{
var draggedDataIndex = list.IndexOf(draggedData);
var hoveredDataIndex = list.IndexOf(hoveredData);
if (draggedDataIndex < hoveredDataIndex)
{
list.Insert(hoveredDataIndex + 1, draggedData);
list.RemoveAt(draggedDataIndex);
}
else
{
draggedDataIndex++;
if (list.Count + 1 > draggedDataIndex)
{
list.Insert(hoveredDataIndex, draggedData);
list.RemoveAt(draggedDataIndex);
}
}
}
}
// This method is required to maintain the item's selection state while dragging.
private static MethodInfo? GetMoveMethod(IEnumerable list)
{
var listType = list.GetType();
var integerType = typeof(int);
var methodName = nameof(ObservableCollection<object>.Move);
return listType.GetMethod(methodName, [integerType, integerType]);
}
#endregion
}
Also - some extension methods which were used in the solution:
using System.Windows;
using System.Windows.Media;
namespace MyNamespace.Extensions;
internal static class DependencyObjectExtensions
{
#region Public methods
public static T? GetFirstParentOfType<T>(this DependencyObject child) where T : DependencyObject
{
var parent = VisualTreeHelper.GetParent(child);
return parent is null
? null
: parent is T castedParent
? castedParent
: GetFirstParentOfType<T>(parent);
}
public static DependencyObject? GetFirstParentOfType(this DependencyObject child, Type parentType)
{
var parent = VisualTreeHelper.GetParent(child);
return parent is null
? null
: parent.GetType() == parentType
? parent
: GetFirstParentOfType(parent, parentType);
}
public static T? GetFirstChildOfType<T>(this DependencyObject parent) where T : DependencyObject
{
var child = VisualTreeHelper.GetChild(parent, 0);
return child is null
? null
: child is T castedChild
? castedChild
: GetFirstChildOfType<T>(child);
}
public static DependencyObject? GetFirstChildOfType(this DependencyObject parent, Type childType)
{
var child = VisualTreeHelper.GetChild(parent, 0);
return child is null
? null
: child.GetType() == childType
? child
: GetFirstChildOfType(child, childType);
}
#endregion
}
Upvotes: 2
Reputation: 1531
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:
PreviewMouseLeftButtonDown
event, the dragged item does not look like a selected item.Upvotes: 84
Reputation: 590
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"));
}
}
Upvotes: 1
Reputation: 1170
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;
}
}
Upvotes: 8
Reputation: 103
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.
Upvotes: 2
Reputation: 2262
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.
Upvotes: 18
Reputation: 3314
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;
}
}
}
Upvotes: 36
Reputation: 31
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);
}
}
}
}
Upvotes: 3