WPF C#:通过拖放重新排列列表框中的项目

问题描述 投票:0回答:8

我试图弄清楚如何通过鼠标拖动来上下移动预先填充的列表框中的项目。

我已经查看了微软api中的Control.DoDragDrop方法,但我仍然无法让它执行任何操作。

由于我是视觉工作室环境的新手,因此我将不胜感激。

c# wpf drag-and-drop
8个回答
81
投票

我尝试使用 ObservableCollection 创建一个。看看吧。

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

拖放过程:

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

注:

  • 此实现中的一个问题是,由于它使用
    PreviewMouseLeftButtonDown
    事件,因此拖动的项目看起来不像选定的项目。
  • 而且为了更容易实现,放置目标是列表框项目而不是列表框本身 - 可能需要更好的解决方案。

34
投票

使用 dnr3 的答案,我创建了具有固定选择问题的版本。

窗口1.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);
                }
            }
        }
    }
}

支持泛型和数据绑定的版本。

窗口1.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;
        }
    }
}

17
投票

我建议使用名为 GongSolutions.WPF.DragDrop 的拖放行为。它允许使用附加属性设置器来启用 MVVM 样式用例,无需在视图中添加代码。您应该查看链接以获取一个简单的示例。


8
投票

我采用了 dnr3 的答案并对其进行了更改以在 XAML 中实现。相同的结果,只是更喜欢在 XAML 中而不是在代码隐藏中做我能做的事情。

代替隐藏代码:

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;

将其放入 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>

这是放置在 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;
    }
}

3
投票

维修代码:

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

2
投票

这对我帮助很大,谢谢。尤其是仿制药版本。

我做了以下修改:

因为我没有设置ListBox的DataContext(只是ItemsSource),所以我使用

var items = this.ItemsSource as IList<T>;

在 Move 方法中。

在“移动”的末尾我添加了:

this.SelectedItem = source;

因为我希望用户将移动的项目作为当前选择。


1
投票

改进 Wiesław Šoltésdnr3 答案的修改,我将其抽象为一个易于重用的类,以便您只需几行代码即可设置多个此类列表。我还添加了一个功能,每个项目在 2 种背景颜色之间交替,以便于查看(尽管如果您不想要,可以轻松删除此功能)。

F.Y.I:如果您是“var”的爱好者,我很抱歉,但我绝对不是,这些内容已被我的 IDE 删除,而且我不打算将它们放回去。当然,这不会改变实际的程序行为,并且无论如何都会改进编译时间,所以......获胜! :p

这是课程:

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

这是一个 IT(项目类型)类示例(与原始答案相同):

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

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

最后,用法:

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

0
投票

我这边多一点努力。

这是执行以下操作的解决方案:

  • 维护所选项目的状态(如果 ItemsSource 为
    ObservableCollection
    )。
  • 拖动时滚动列表。
  • 允许指定滚动强度。
  • 允许指定滚动区域的高度。
  • 作为行为实现,因此可以附加到任何
    ItemsControl

一个缺点 - 它仅在使用

ItemsSource
属性时才有效。但是,我相信,它涵盖了大多数场景。

代码:

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
}

另外 - 解决方案中使用的一些扩展方法:

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
}
© www.soinside.com 2019 - 2024. All rights reserved.