ListViewでドラッグアンドドロップ

M-V-VMパターンで添付ビヘイビアを使って、項目の入れ替えができるListViewを作ってみました。ちょっと長いですが。
なお、ViewModelBaseクラスは、WPF Model-View-ViewModel Toolkit 0.1で生成されたものを利用しています。

まずはMainView.xamlです。DataContextにはMainViewModelを関連付けています。

<Window x:Class="DraggableListView.Views.MainView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Behaviors="clr-namespace:DraggableListView.Behaviors"
    Title="Main Window" Height="400" Width="800">
    
    <ListView x:Name="DraggableListView" ItemsSource="{Binding Children}"
              Behaviors:DragAndDropBehavior.IsEnabled="true">
        <ListView.View>
            <GridView>
                <GridView.Columns>
                    <GridViewColumn Header="No" DisplayMemberBinding="{Binding No}" />
                    <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" />
                </GridView.Columns>
            </GridView>
        </ListView.View>
    </ListView>
</Window>

つづいてMainViewModel.csです。ItemViewModelのリストを持っているだけですね。

using System.Collections.ObjectModel;

namespace DraggableListView.ViewModels
{
    public class MainViewModel : ViewModelBase
    {
        public ObservableCollection<ItemViewModel> Children { get; set; }

        public MainViewModel()
        {
            Children = new ObservableCollection<ItemViewModel>()
                           {
                               new ItemViewModel(1, "item1"),
                               new ItemViewModel(2, "item2"),
                               new ItemViewModel(3, "item3"),
                               new ItemViewModel(4, "item4"),
                               new ItemViewModel(5, "item5")
                           };
        }
    }
}

ItemViewModel.csは、NoとNameをプロパティとして持っています。

namespace DraggableListView.ViewModels
{
    public class ItemViewModel : ViewModelBase
    {
        public ItemViewModel(int no, string name)
        {
            No = no;
            Name = name;
        }

        private string _name;
        public string Name
        {
            get{ return _name;}
            set
            {
                if(value != _name)
                {
                    _name = value;
                    OnPropertyChanged("Name");
                }
            }
        }

        private int _no;
        public int No
        {
            get { return _no; }
            set
            {
                if (value != _no)
                {
                    _no = value;
                    OnPropertyChanged("No");
                }
            }
        }
    }
}

最後にドラッグアンドドロップの添付ビヘイビア。

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using DraggableListView.ViewModels;

namespace DraggableListView.Behaviors
{
    public class DragAndDropBehavior
    {
        public static bool GetIsEnabled(ListView view)
        {
            return (bool)view.GetValue(IsEnabledProperty);
        }

        public static void SetIsEnabled(ListView view, bool value)
        {
            view.SetValue(IsEnabledProperty, value);
        }

        public static readonly DependencyProperty IsEnabledProperty =
            DependencyProperty.RegisterAttached(
                "IsEnabled", typeof(bool), typeof(DragAndDropBehavior),
                new UIPropertyMetadata(false, OnIsEnabledChanged));

        static void OnIsEnabledChanged(DependencyObject depObj,
                                       DependencyPropertyChangedEventArgs e)
        {
            var view = depObj as ListView;
            if (view == null)
            {
                return;
            }

            if (e.NewValue is bool == false)
            {
                return;
            }

            bool isEnabled = (bool)e.NewValue;

            if (isEnabled)
            {
                view.PreviewMouseLeftButtonDown += OnMouseLeftButtonDown;
                view.Drop += OnDrop;
            }
            else
            {
                view.PreviewMouseLeftButtonDown -= OnMouseLeftButtonDown;
                view.Drop -= OnDrop;
            }
        }

        private static void OnDrop(object sender, DragEventArgs e)
        {
            var view = sender as ListView;
            var newIndex = GetItemIndex(view, e.GetPosition(view));
            var source = (ItemViewModel)e.Data.GetData(typeof(ItemViewModel));

            if (newIndex != -1 && source != null)
            {
                MainViewModel vm = (MainViewModel)view.DataContext;
                vm.Children.Remove(source);
                vm.Children.Insert(newIndex, source);
            }
            view.AllowDrop = false; 
        }

        private static void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            var view = sender as ListView;
            var index = GetItemIndex(view, e.GetPosition(view));
            if (index != -1)
            {
                view.SelectedIndex = index;
            }

            if (view.SelectedItem is ItemViewModel)
            {
                view.AllowDrop = true;
                var vm = (ItemViewModel)view.SelectedItem;
                DragDrop.DoDragDrop(view, vm, DragDropEffects.Copy);
            }
        }

        private static int GetItemIndex(ListView view, Point pos)
        {
            var result = VisualTreeHelper.HitTest(view, pos);
            var item = result.VisualHit;
            while (item != null)
            {
                if (item is ListViewItem)
                {
                    break;
                }
                item = VisualTreeHelper.GetParent(item);
            }
            if (item != null)
            {
                return view.Items.IndexOf(((ListViewItem)item).Content);
            }
            return -1;
        }
    }
}