Skip to content

Instantly share code, notes, and snippets.

@mamidenn
Created July 6, 2017 11:59
Show Gist options
  • Save mamidenn/9eaa09695ba4d0a5762a2b74eee34d7f to your computer and use it in GitHub Desktop.
Save mamidenn/9eaa09695ba4d0a5762a2b74eee34d7f to your computer and use it in GitHub Desktop.
A ListView with rearrangeable items. Uses a DropIndicator object to provide visual clues as to where a dragged item will be dropped.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;
namespace de.dennhardt
{
/// <summary>
/// Eine Himmelsrichtung. Richtungen lassen sich per logischer
/// Oder-Operation kombinieren.
/// </summary>
public enum Direction
{
None = 0,
North = 1,
Northeast = 3,
East = 2,
Southeast = 6,
South = 4,
Southwest = 12,
West = 8,
Northwest = 9
}
/// <summary>
/// Ein Adorner, der visuelles Feedback bei Drag and Drop-Operationen bietet
/// </summary>
public class DropIndicator : Adorner
{
/// <summary>
/// Richtung, in der der Indikator angezeigt wird
/// </summary>
public Direction Direction;
/// <summary>
/// Initialisiert einen neuen Indikator
/// </summary>
/// <param name="adornedElement">Das Element, an das der Indikator angehängt wird</param>
/// <param name="direction">Die Richtung, in der der Indikator angehängt wird</param>
public DropIndicator(UIElement adornedElement, Direction direction)
: base(adornedElement)
{
this.Direction = direction;
this.IsHitTestVisible = false;
}
/// <summary>
/// Wird beim Rendern ausgeführt. Rendert den Indikator
/// </summary>
/// <param name="context">Kontext, in dem der Indikator gerendert wird</param>
protected override void OnRender(DrawingContext context)
{
Rect rect = new Rect(this.AdornedElement.RenderSize);
Pen pen = new Pen(new SolidColorBrush(Colors.Black), 2);
if ((Direction & Direction.West) > 0)
{
drawFeatheredLine(context, pen, rect.TopLeft, rect.BottomLeft);
}
if ((Direction & Direction.East) > 0)
{
drawFeatheredLine(context, pen, rect.TopRight, rect.BottomRight);
}
}
private void drawFeatheredLine(DrawingContext context, Pen pen, Point p0, Point p1)
{
context.DrawLine(pen, p0.Add(-2, 0), p0.Add(0,2));
context.DrawLine(pen, p0.Add(2, 0), p0.Add(0,2));
context.DrawLine(pen, p0.Add(0, 2), p1.Add(0,-2));
context.DrawLine(pen, p1.Add(-2, 0), p1.Add(0, -2));
context.DrawLine(pen, p1.Add(2, 0), p1.Add(0, -2));
}
}
/// <summary>
/// Bietet zusätzliche Methoden
/// </summary>
public static partial class ExtensionMethods
{
/// <summary>
/// Bildet die Summe aus einem Punkt und einem durch zwei Skalare definierten
/// Punkt. Erleichtert die lesbarkeit der Point.Add()-Methode.
/// </summary>
/// <param name="p">Punkt</param>
/// <param name="x">X-Koordinate</param>
/// <param name="y">Y-Koordinate</param>
/// <returns></returns>
public static Point Add(this Point p, double x, double y)
{
return new Point(p.X + x, p.Y + y);
}
}
}
using System;
using System.Collections;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
namespace de.dennhardt
{
/// <summary>
/// A ListView with rearrangeable items. Uses a DropIndicator object to
/// provide visual clues as to where a dragged item will be dropped. As generic
/// as possible to enable usage in many circumstances.
///
/// Proudly presented by Martin Dennhardt
/// </summary>
class OrderableListView : ListView
{
public DropIndicator DropIndicator;
private bool prepareDrag = false;
private Type itemType {
// get the type of the generic list items
get
{
var genericArguments = ItemsSource.GetType().GetGenericArguments();
if (genericArguments.Length == 1)
{
return genericArguments[0];
}
else
{
return typeof(object);
}
}
}
public OrderableListView()
{
AllowDrop = true;
}
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
{
var item = getItemAtPosition(e.GetPosition(this));
// clear selection if clicked in empty space
if (item == null)
{
if (getItemAtPosition<ScrollBar>(e.GetPosition(this)) == null)
{
// deselect all Items, but only if we're not using a scrollbar
Focus();
SelectedItem = null;
}
return;
}
prepareDrag = true;
// don't drag and drop if clicked in textbox
if (getItemAtPosition<TextBoxBase>(e.GetPosition(this)) != null)
{
prepareDrag = false;
}
// suppress item selection on mousedown
// instead this is handled @ mouse up event
if (!item.IsSelected)
{
// but only if we haven't clicked a button!
if (getItemAtPosition<ButtonBase>(e.GetPosition(this)) == null)
{
e.Handled = true;
}
}
}
protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)
{
prepareDrag = false;
// let other logic handle it, if we clicked a button
if (getItemAtPosition<ButtonBase>(e.GetPosition(this)) != null)
{
return;
}
// focus item if selection has changed
var item = getItemAtPosition(e.GetPosition(this));
if (item != null && SelectedItem != ItemContainerGenerator.ItemFromContainer(item))
{
item.Focus();
SelectedItem = ItemContainerGenerator.ItemFromContainer(item);
}
}
protected override void OnMouseLeave(MouseEventArgs e)
{
prepareDrag = false;
}
protected override void OnMouseMove(MouseEventArgs e)
{
removeDropIndicator();
// drag if we're already waiting for it
if (prepareDrag)
{
prepareDrag = false;
onDrag(e);
}
}
protected sealed override void OnDragOver(DragEventArgs e)
{
// check if dragged element is of correct type
// prevent dropping if it is not
if (!e.Data.GetDataPresent(itemType))
{
e.Effects = DragDropEffects.None;
e.Handled = true;
return;
}
// get ListViewItem below pointer
var item = getItemAtPosition(e.GetPosition(this));
if (item == null)
{
return;
}
// remove old drop indicator
removeDropIndicator();
// get octant of pointer position
var direction = getDirectionFromPosition(item, e.GetPosition(item));
// add new drop indicator
DropIndicator = new DropIndicator(item, direction);
var newAdornerLayer = AdornerLayer.GetAdornerLayer(item);
newAdornerLayer.Add(DropIndicator);
}
protected override void OnDrop(DragEventArgs e)
{
var dropIndex = ((IList)ItemsSource).Count;
if (DropIndicator != null)
{
// get the item next to the drop indicator
var item = DropIndicator.AdornedElement as ListViewItem;
var direction = DropIndicator.Direction;
// remove drop indicator
removeDropIndicator();
// figure out where to insert the item
// TODO generify for vertical Lists!
var addBelow = (direction & Direction.East) > 0 ? 1 : 0;
dropIndex = ItemContainerGenerator.IndexFromContainer(item) + addBelow;
}
// get dragged item and its index
var draggedItem = e.Data.GetData(itemType);
var draggedIndex = ((IList)ItemsSource).IndexOf(draggedItem);
// decrement if we're moving up to compensate for index recalculation
if (draggedIndex >= 0)
{
dropIndex -= (dropIndex > draggedIndex) ? 1 : 0;
}
// alway select new items
var wasSelected = true;
if (((IList)ItemsSource).Contains(draggedItem))
{
// preserve selection if item was already in list
wasSelected = ((ListViewItem)ItemContainerGenerator.ContainerFromItem(draggedItem)).IsSelected;
// remove item from list
((IList)ItemsSource).Remove(draggedItem);
}
else
{
// clear selection if we add a new item
SelectedItem = null;
}
// insert item at new location
((IList)ItemsSource).Insert(dropIndex, draggedItem);
// restore item selection state
var itemContainer = ItemContainerGenerator.ContainerFromItem(draggedItem) as ListViewItem;
if (itemContainer != null)
{
itemContainer.IsSelected = wasSelected;
}
}
private void onDrag(MouseEventArgs e)
{
// get ListViewItem below pointer
var item = getItemAtPosition(e.GetPosition(this));
if (item == null)
{
return;
}
// get corresponding data object
var data = ItemContainerGenerator.ItemFromContainer(item);
// start drag and drop
DragDrop.DoDragDrop(item, data, DragDropEffects.Move);
}
private void removeDropIndicator()
{
if (DropIndicator != null)
{
var layer = AdornerLayer.GetAdornerLayer(DropIndicator.AdornedElement);
if (layer != null)
{
layer.Remove(DropIndicator);
}
DropIndicator = null;
}
}
private ListViewItem getItemAtPosition(Point p)
{
return getItemAtPosition<ListViewItem>(p);
}
/// <summary>
/// Get item of type T at supplied point that is farthest down in the
/// tree or null if no such item exists.
/// </summary>
/// <typeparam name="T">Type of required item</typeparam>
/// <param name="p">Point where to look for the item</param>
/// <returns>Required item or null</returns>
private T getItemAtPosition<T>(Point p) where T : FrameworkElement
{
var item = InputHitTest(p) as FrameworkElement;
while (item as T == null && item != this)
{
item = VisualTreeHelper.GetParent(item) as FrameworkElement;
}
return item as T;
}
private Direction getDirectionFromPosition(ListViewItem item, Point p)
{
Direction direction = Direction.None;
direction |= p.Y < item.ActualHeight / 2 ? Direction.North : Direction.South;
direction |= p.X < item.ActualWidth / 2 ? Direction.West : Direction.East;
return direction;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment