WPF ListBox subclass with added drag/drop support including an Adorner.
Consumers should use the ItemDropOn and ItemDragOver events, which provide the item in the collection the data was dropped on, as well as its index in the collection. There’s also built-in support for reordering items in the ListBox itself by drag and drop. All you have to do is set AllowDragReorder=”True” in the XAML.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; namespace HollowEarthIndustries { public class DragDropListBox : ListBox { public DragDropListBox() { AllowDrop = true; DragEnter += DragDropListBox_DragEnter; DragOver += DragDropListBox_DragOver; DragLeave += DragDropListBox_DragLeave; Drop += DragDropListBox_Drop; } /// <summary> /// If ItemsSource is in use, you can't insert into Items. /// /// If ItemsSource is in use but it's IEnumerable, as far as I know you can't insert into anything, /// but maybe some consumer is doing something clever, so we still allow drop in that case. /// </summary> /// <returns></returns> public System.Collections.IList GetInsertableCollection() { if (ItemsSource == null) return Items; return ItemsSource as System.Collections.IList; } protected override DependencyObject GetContainerForItemOverride() { // We configure the ListBoxItems here rather than with a Style because this is the // simplest way to allow consumers to use ItemContainerStyle without stepping on // our internals. var itemContainer = new ListBoxItem { // TODO: We need to update the ListBoxItems when the ListBox's AllowDrop changes // TODO: What if consumer provides literal ListBoxItem elements in the XAML? Derp. AllowDrop = true, }; // Events for items as object of drag/drop itemContainer.DragEnter += ItemContainer_DragEnter; itemContainer.DragOver += ItemContainer_DragOver; itemContainer.DragLeave += ItemContainer_DragLeave; itemContainer.Drop += ItemContainer_Drop; // Events for items as subject of drag/drop itemContainer.PreviewMouseLeftButtonDown += ItemContainer_PreviewMouseLeftButtonDown; itemContainer.PreviewMouseMove += ItemContainer_PreviewMouseMove; return itemContainer; } #region Events #region ItemDropOn event public event EventHandler<ItemDragDropEventArgs> ItemDropOn; private string ReorderFormat { get; } = typeof(ItemDragData).FullName; protected void OnItemDropOn(DragEventArgs e, object targetItem, int targetIndex) { if (IsDragReorderingSelf(e)) { var data = (ItemDragData)e.Data.GetData(ReorderFormat); System.Collections.IList list = GetInsertableCollection(); if (list != null) { if (!e.KeyStates.HasFlag(DragDropKeyStates.ControlKey) || !AllowDragReorderCopy) { if (data.SourceItemIndex < targetIndex) { // If we're removing an item that precedes the target item, // adjust targetIndex down to account for the target item's // changed position. --targetIndex; } list.RemoveAt(data.SourceItemIndex); } list.Insert(targetIndex, data.DragItem); return; } } ItemDropOn?.Invoke(this, new ItemDragDropEventArgs(e, targetItem, targetIndex)); } #endregion ItemDropOn event #region ItemDragOver event public event EventHandler<ItemDragDropEventArgs> ItemDragOver; protected bool IsDragReorderingSelf(DragEventArgs e) { if (AllowDragReorder && e.Data.GetDataPresent(ReorderFormat)) { var data = (ItemDragData)e.Data.GetData(ReorderFormat); if (data.Source == this) { return true; } } return false; } protected void OnItemDragOver(DragEventArgs e, object targetItem, int targetIndex) { if (IsDragReorderingSelf(e)) { if (e.KeyStates.HasFlag(DragDropKeyStates.ControlKey) && AllowDragReorderCopy) { e.Effects = DragDropEffects.Copy; } else { e.Effects = DragDropEffects.Move; } } ItemDragOver?.Invoke(this, new ItemDragDropEventArgs(e, targetItem, targetIndex)); } #endregion ItemDropOn event #endregion Events #region Drag Over Helpers protected void SetDragAfterLastItem(bool isDragging) { if (HasItems) { UpdateAdorner(GetLastListBoxItem(), isDragging, false); } } protected void UpdateAdorner(ListBoxItem lbi, bool isDragging, bool insertingBefore) { var adornerLayer = AdornerLayer.GetAdornerLayer(lbi); adornerLayer.IsHitTestVisible = false; if (isDragging) { // It's a nullable bool; adornerLayer.GetAdorners() is old fashioned and may return null. if (adornerLayer.GetAdorners(lbi)?.OfType<DropTargetAdorner>().Any() != true) { adornerLayer.Add(/*_adorner = */new DropTargetAdorner(lbi, insertingBefore, TargetMarkerBrush)); } } else { adornerLayer.GetAdorners(lbi)?.OfType<DropTargetAdorner>().ToList().ForEach(a => adornerLayer.Remove(a)); } } protected ListBoxItem GetLastListBoxItem() { if (HasItems) { return (ListBoxItem)ItemContainerGenerator.ContainerFromIndex(Items.Count - 1); } return null; } #endregion Drag Over Helpers #region ListBox Event Handlers private void DragDropListBox_DragEnter(object sender, DragEventArgs e) { SetDragAfterLastItem(true); OnItemDragOver(e, null, Items.Count); e.Handled = true; } private void DragDropListBox_DragOver(object sender, DragEventArgs e) { OnItemDragOver(e, null, Items.Count); e.Handled = true; } private void DragDropListBox_DragLeave(object sender, DragEventArgs e) { SetDragAfterLastItem(false); e.Handled = true; } private void DragDropListBox_Drop(object sender, DragEventArgs e) { SetDragAfterLastItem(false); if (e.Effects != DragDropEffects.None) { OnItemDropOn(e, null, Items.Count); } e.Handled = true; } #endregion ListBox Event Handlers #region ListBoxItem event handlers #region Drag source event handlers Point? _mouseDownPoint = null; private void ItemContainer_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (AllowItemDrag) { _mouseDownPoint = Mouse.GetPosition((Control)sender); } } private void ItemContainer_PreviewMouseMove(object sender, MouseEventArgs e) { //System.Diagnostics.Trace.WriteLine($"ItemContainer_PreviewMouseMove e.LeftButton {e.LeftButton}"); if (AllowItemDrag && e.LeftButton == MouseButtonState.Pressed && _mouseDownPoint.HasValue) { var pos = Mouse.GetPosition((Control)sender); //System.Diagnostics.Trace.WriteLine($"_mouseDownPoint {_mouseDownPoint} pos {pos}"); if (Distance(pos, _mouseDownPoint.Value) > 4) { var lbi = (ListBoxItem)sender; var dragData = new ItemDragData { DragItem = lbi.DataContext ?? lbi, SourceItemIndex = ItemContainerGenerator.IndexFromContainer(lbi), Source = this }; var effect = DragDrop.DoDragDrop(this, dragData, DragDropEffects.Move | DragDropEffects.Copy); _mouseDownPoint = null; e.Handled = true; } } else { _mouseDownPoint = null; } } protected static double Distance(Point a, Point b) { var xdist = Math.Abs(a.X - b.X); var ydist = Math.Abs(a.Y - b.Y); return Math.Sqrt((xdist * xdist) + (ydist * ydist)); } #endregion Drag source event handlers #region Drag target event handlers private void ItemContainer_DragEnter(object sender, DragEventArgs e) { var lbi = (ListBoxItem)sender; UpdateAdorner(lbi, true, true); int index = ItemContainerGenerator.IndexFromContainer(lbi); OnItemDragOver(e, lbi.DataContext ?? lbi, index); e.Handled = true; } private void ItemContainer_DragOver(object sender, DragEventArgs e) { var lbi = (ListBoxItem)sender; int index = ItemContainerGenerator.IndexFromContainer(lbi); OnItemDragOver(e, lbi.DataContext ?? lbi, index); e.Handled = true; } private void ItemContainer_DragLeave(object sender, DragEventArgs e) { UpdateAdorner((ListBoxItem)sender, false, true); e.Handled = true; } private void ItemContainer_Drop(object sender, DragEventArgs e) { System.Diagnostics.Trace.WriteLine($"ItemContainer_Drop e.Effects {e.Effects}"); var lbi = (ListBoxItem)sender; UpdateAdorner(lbi, false, true); if (e.Effects != DragDropEffects.None) { int index = ItemContainerGenerator.IndexFromContainer(lbi); OnItemDropOn(e, lbi.DataContext ?? lbi, index); } e.Handled = true; } #endregion Drag target event handlers #endregion ListBoxItem event handlers #region Dependency Properties #region TargetMarkerBrush Property public Brush TargetMarkerBrush { get { return (Brush)GetValue(TargetMarkerBrushProperty); } set { SetValue(TargetMarkerBrushProperty, value); } } public static readonly DependencyProperty TargetMarkerBrushProperty = DependencyProperty.Register(nameof(TargetMarkerBrush), typeof(Brush), typeof(DragDropListBox), new PropertyMetadata(Brushes.Black)); #endregion TargetMarkerBrush Property #region AllowItemDrag Property public bool AllowItemDrag { get { return (bool)GetValue(AllowItemDragProperty); } set { SetValue(AllowItemDragProperty, value); } } public static readonly DependencyProperty AllowItemDragProperty = DependencyProperty.Register(nameof(AllowItemDrag), typeof(bool), typeof(DragDropListBox), new PropertyMetadata(false)); #endregion AllowItemDrag Property #region AllowDragReorderCopy Property /// <summary> /// If true, drag-reorder operation will copy when control key is pressed /// </summary> public bool AllowDragReorderCopy { get { return (bool)GetValue(AllowDragReorderCopyProperty); } set { SetValue(AllowDragReorderCopyProperty, value); } } public static readonly DependencyProperty AllowDragReorderCopyProperty = DependencyProperty.Register(nameof(AllowDragReorderCopy), typeof(bool), typeof(DragDropListBox), new PropertyMetadata(false)); #endregion AllowDragReorderCopy Property #region AllowDragReorder Property public bool AllowDragReorder { get { return (bool)GetValue(AllowDragReorderProperty); } set { SetValue(AllowDragReorderProperty, value); } } public static readonly DependencyProperty AllowDragReorderProperty = DependencyProperty.Register(nameof(AllowDragReorder), typeof(bool), typeof(DragDropListBox), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, AllowDragReorder_PropertyChanged) { DefaultUpdateSourceTrigger = System.Windows.Data.UpdateSourceTrigger.PropertyChanged }); protected static void AllowDragReorder_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { (d as DragDropListBox).OnAllowDragReorderChanged(e.OldValue); } private void OnAllowDragReorderChanged(object oldValue) { if (AllowDragReorder) { AllowItemDrag = true; } } #endregion AllowDragReorder Property #endregion Dependency Properties #region ItemDragData class public class ItemDragData { public object DragItem { get; set; } public int SourceItemIndex { get; set; } public DragDropListBox Source { get; set; } } #endregion ItemDragData class } #region ItemDragDropEventArgs class public class ItemDragDropEventArgs : EventArgs { public ItemDragDropEventArgs(DragEventArgs e, object targetItem, int targetIndex) { _e = e; TargetItem = targetItem; TargetIndex = targetIndex; } private DragEventArgs _e; public int TargetIndex { get; private set; } public IDataObject Data => _e.Data; public DragDropEffects Effects { get => _e.Effects; set => _e.Effects = value; } public DragDropKeyStates KeyStates => _e.KeyStates; public DragDropEffects AllowedEffects => _e.AllowedEffects; /// <summary> /// Either ListBoxItem or DataContext of ListBoxItem /// If null, user is dragging over the empty space after the last item in the listbox /// </summary> public Object TargetItem { get; private set; } } #endregion ItemDragDropEventArgs class #region DropTargetAdorner class public class DropTargetAdorner : Adorner { /* * AdornerContentPresenter with a DataTemplate would be preferable: * * https://stackoverflow.com/a/10034274/424129 * * */ public DropTargetAdorner(UIElement adornedElement, bool insertingBefore, Brush targetHighlightBrush = null) : base(adornedElement) { IsHitTestVisible = false; _insertingBefore = insertingBefore; _targetHighlightBrush = targetHighlightBrush ?? Brushes.Black; Border border = new Border(); } protected double _targetBarHeight = 2; protected bool _insertingBefore; protected Brush _targetHighlightBrush; protected override void OnRender(DrawingContext drawingContext) { var lbi = AdornedElement as ListBoxItem; Rect adornedElementRect = new Rect(0, 0, lbi.ActualWidth, lbi.ActualHeight); Size targetBarSize = new Size(adornedElementRect.Width, _targetBarHeight); Rect targetBarRect = new Rect(adornedElementRect.Location, targetBarSize); if (!_insertingBefore) { targetBarRect.Location = new Point(targetBarRect.Left, adornedElementRect.BottomLeft.Y - _targetBarHeight); } drawingContext.DrawRectangle(_targetHighlightBrush, null, targetBarRect); } } #endregion DropTargetAdorner class }