Computing Atman
C# | WPF Prism DataGrid ドラッグ&ドロップで並び替え
🥄

C# | WPF Prism DataGrid ドラッグ&ドロップで並び替え

C# WPF で、 DataGrid のレコードをドラッグ&ドロップで並び替える処理

2023/03/02

C#の WPF で、DataGridのレコードをドラッグ&ドロップで並び替える処理を説明します。

image

対象ファイル(例)

コーディングが必要なファイルは下記の A~B です。

Services/
 |-BindingDragDropRowBehavior.cs ・・・A
 
Views/
 |-SampleView.xaml ・・・B

BindingDragDropRowBehavior.cs ・・・A

①DataGripの行をドラッグ&ドロップするビヘイビアを準備

DataGripの行をドラッグ&ドロップするビヘイビアを準備します。コードは下記となります。
namespaece は、適宜変更して下さい。

▼BindingDragDropRowBehavior.cs

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Collections;
using System.Windows.Data;
using System.Windows.Media;
 
namespace Template2.WPF.Services
{
    /// <summary>
    /// DataGridの行をドラッグ&ドロップで並び変えるビヘイビア
    /// 複雑なDataGrid構造には対応していないため注意
    ///
    ///  利用可能なDataGridの例
    ///  ・自動列生成のみ
    ///  ・DataGridTextColumnのみ
    /// </summary>
    public static class BindingDragDropRowBehavior
    {
        private static DataGrid dataGrid;
 
        private static Popup popup;
 
        private static bool enable;
 
        private static object draggedItem;
 
        public static object DraggedItem
        {
            get { return BindingDragDropRowBehavior.draggedItem; }
            set { BindingDragDropRowBehavior.draggedItem = value; }
        }
 
        public static Popup GetPopupControl(DependencyObject obj)
        {
            return (Popup)obj.GetValue(PopupControlProperty);
        }
 
        public static void SetPopupControl(DependencyObject obj, Popup value)
        {
            obj.SetValue(PopupControlProperty, value);
        }
 
        // Using a DependencyProperty as the backing store for PopupControl.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PopupControlProperty =
            DependencyProperty.RegisterAttached("PopupControl", typeof(Popup), typeof(BindingDragDropRowBehavior), new UIPropertyMetadata(null, OnPopupControlChanged));
 
        private static void OnPopupControlChanged(DependencyObject depObject, DependencyPropertyChangedEventArgs e)
        {
            if (e.NewValue == null || !(e.NewValue is Popup))
            {
                throw new ArgumentException("Popup Control should be set", "PopupControl");
            }
            popup = e.NewValue as Popup;
 
            dataGrid = depObject as DataGrid;
            // Check if DataGrid
            if (dataGrid == null)
                return;
 
 
            if (enable && popup != null)
            {
                dataGrid.BeginningEdit += new EventHandler<DataGridBeginningEditEventArgs>(OnBeginEdit);
                dataGrid.CellEditEnding += new EventHandler<DataGridCellEditEndingEventArgs>(OnEndEdit);
                dataGrid.MouseLeftButtonUp += new System.Windows.Input.MouseButtonEventHandler(OnMouseLeftButtonUp);
                dataGrid.PreviewMouseLeftButtonDown += new MouseButtonEventHandler(OnMouseLeftButtonDown);
                dataGrid.MouseMove += new MouseEventHandler(OnMouseMove);
            }
            else
            {
                dataGrid.BeginningEdit -= new EventHandler<DataGridBeginningEditEventArgs>(OnBeginEdit);
                dataGrid.CellEditEnding -= new EventHandler<DataGridCellEditEndingEventArgs>(OnEndEdit);
                dataGrid.MouseLeftButtonUp -= new System.Windows.Input.MouseButtonEventHandler(OnMouseLeftButtonUp);
                dataGrid.MouseLeftButtonDown -= new MouseButtonEventHandler(OnMouseLeftButtonDown);
                dataGrid.MouseMove -= new MouseEventHandler(OnMouseMove);
 
                dataGrid = null;
                popup = null;
                draggedItem = null;
                IsEditing = false;
                IsDragging = false;
            }
        }
 
        public static bool GetEnabled(DependencyObject obj)
        {
            return (bool)obj.GetValue(EnabledProperty);
        }
 
        public static void SetEnabled(DependencyObject obj, bool value)
        {
            obj.SetValue(EnabledProperty, value);
        }
 
        // Using a DependencyProperty as the backing store for Enabled.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty EnabledProperty =
            DependencyProperty.RegisterAttached("Enabled", typeof(bool), typeof(BindingDragDropRowBehavior), new UIPropertyMetadata(false, OnEnabledChanged));
 
        private static void OnEnabledChanged(DependencyObject depObject, DependencyPropertyChangedEventArgs e)
        {
            //Check if value is a Boolean Type
            if (e.NewValue is bool == false)
                throw new ArgumentException("Value should be of bool type", "Enabled");
 
            enable = (bool)e.NewValue;
 
        }
 
        public static bool IsEditing { get; set; }
 
        public static bool IsDragging { get; set; }
 
        private static void OnBeginEdit(object sender, DataGridBeginningEditEventArgs e)
        {
            IsEditing = true;
            //in case we are in the middle of a drag/drop operation, cancel it...
            if (IsDragging) ResetDragDrop();
        }
 
        private static void OnEndEdit(object sender, DataGridCellEditEndingEventArgs e)
        {
            IsEditing = false;
        }
 
 
        /// <summary>
        /// Initiates a drag action if the grid is not in edit mode.
        /// </summary>
        private static void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (IsEditing) return;
 
            var row = UIHelpers.TryFindFromPoint<DataGridRow>((UIElement)sender, e.GetPosition(dataGrid));
            if (row == null || row.IsEditing) return;
 
            //set flag that indicates we're capturing mouse movements
            IsDragging = true;
            DraggedItem = row.Item;
        }
 
        /// <summary>
        /// Completes a drag/drop operation.
        /// </summary>
        private static void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            if (!IsDragging || IsEditing)
            {
                return;
            }
 
            //get the target item
            var targetItem = dataGrid.SelectedItem;
 
            if (targetItem == null || !ReferenceEquals(DraggedItem, targetItem))
            {
                //get target index
                var targetIndex = ((dataGrid).ItemsSource as IList).IndexOf(targetItem);
 
                //remove the source from the list
                ((dataGrid).ItemsSource as IList).Remove(DraggedItem);
 
                //move source at the target's location
                ((dataGrid).ItemsSource as IList).Insert(targetIndex, DraggedItem);
 
                //select the dropped item
                dataGrid.SelectedItem = DraggedItem;
            }
 
            //reset
            ResetDragDrop();
        }
 
        /// <summary>
        /// Closes the popup and resets the
        /// grid to read-enabled mode.
        /// </summary>
        private static void ResetDragDrop()
        {
            IsDragging = false;
            popup.IsOpen = false;
            dataGrid.IsReadOnly = false;
        }
 
        /// <summary>
        /// Updates the popup's position in case of a drag/drop operation.
        /// </summary>
        private static void OnMouseMove(object sender, MouseEventArgs e)
        {
            if (!IsDragging || e.LeftButton != MouseButtonState.Pressed) return;
 
            popup.DataContext = DraggedItem;
            //display the popup if it hasn't been opened yet
            if (!popup.IsOpen)
            {
                //switch to read-only mode
                dataGrid.IsReadOnly = true;
 
                //make sure the popup is visible
                popup.IsOpen = true;
            }
 
 
            Size popupSize = new Size(popup.ActualWidth, popup.ActualHeight);
            popup.PlacementRectangle = new Rect(e.GetPosition(dataGrid), popupSize);
 
            //make sure the row under the grid is being selected
            Point position = e.GetPosition(dataGrid);
            var row = UIHelpers.TryFindFromPoint<DataGridRow>(dataGrid, position);
            if (row != null) dataGrid.SelectedItem = row.Item;
        }
    }
    public static class UIHelpers
    {
 
        #region find parent
 
        /// <summary>
        /// Finds a parent of a given item on the visual tree.
        /// </summary>
        /// <typeparam name="T">The type of the queried item.</typeparam>
        /// <param name="child">A direct or indirect child of the
        /// queried item.</param>
        /// <returns>The first parent item that matches the submitted
        /// type parameter. If not matching item can be found, a null
        /// reference is being returned.</returns>
        public static T TryFindParent<T>(DependencyObject child)
          where T : DependencyObject
        {
            //get parent item
            DependencyObject parentObject = GetParentObject(child);
 
            //we've reached the end of the tree
            if (parentObject == null) return null;
 
            //check if the parent matches the type we're looking for
            T parent = parentObject as T;
            if (parent != null)
            {
                return parent;
            }
            else
            {
                //use recursion to proceed with next level
                return TryFindParent<T>(parentObject);
            }
        }
 
 
        /// <summary>
        /// This method is an alternative to WPF's
        /// <see cref="VisualTreeHelper.GetParent"/> method, which also
        /// supports content elements. Do note, that for content element,
        /// this method falls back to the logical tree of the element.
        /// </summary>
        /// <param name="child">The item to be processed.</param>
        /// <returns>The submitted item's parent, if available. Otherwise
        /// null.</returns>
        public static DependencyObject GetParentObject(DependencyObject child)
        {
            if (child == null) return null;
            ContentElement contentElement = child as ContentElement;
 
            if (contentElement != null)
            {
                DependencyObject parent = ContentOperations.GetParent(contentElement);
                if (parent != null) return parent;
 
                FrameworkContentElement fce = contentElement as FrameworkContentElement;
                return fce != null ? fce.Parent : null;
            }
 
            //if it's not a ContentElement, rely on VisualTreeHelper
            return VisualTreeHelper.GetParent(child);
        }
 
        #endregion
 
 
        #region update binding sources
 
        /// <summary>
        /// Recursively processes a given dependency object and all its
        /// children, and updates sources of all objects that use a
        /// binding expression on a given property.
        /// </summary>
        /// <param name="obj">The dependency object that marks a starting
        /// point. This could be a dialog window or a panel control that
        /// hosts bound controls.</param>
        /// <param name="properties">The properties to be updated if
        /// <paramref name="obj"/> or one of its childs provide it along
        /// with a binding expression.</param>
        public static void UpdateBindingSources(DependencyObject obj,
                                  params DependencyProperty[] properties)
        {
            foreach (DependencyProperty depProperty in properties)
            {
                //check whether the submitted object provides a bound property
                //that matches the property parameters
                BindingExpression be = BindingOperations.GetBindingExpression(obj, depProperty);
                if (be != null) be.UpdateSource();
            }
 
            int count = VisualTreeHelper.GetChildrenCount(obj);
            for (int i = 0; i < count; i++)
            {
                //process child items recursively
                DependencyObject childObject = VisualTreeHelper.GetChild(obj, i);
                UpdateBindingSources(childObject, properties);
            }
        }
 
        #endregion
 
 
        /// <summary>
        /// Tries to locate a given item within the visual tree,
        /// starting with the dependency object at a given position. 
        /// </summary>
        /// <typeparam name="T">The type of the element to be found
        /// on the visual tree of the element at the given location.</typeparam>
        /// <param name="reference">The main element which is used to perform
        /// hit testing.</param>
        /// <param name="point">The position to be evaluated on the origin.</param>
        public static T TryFindFromPoint<T>(UIElement reference, Point point)
          where T : DependencyObject
        {
            DependencyObject element = reference.InputHitTest(point)
                                         as DependencyObject;
            if (element == null) return null;
            else if (element is T) return (T)element;
            else return TryFindParent<T>(element);
        }
    }
}

SampleView.xaml ・・・B

②Viewに、BindingDragDropRowBehaviorの参照を追加

ドラッグ&ドロップで並び替えをしたいDataGridに、 BindingDragDropBehavior を設定します。

そのために、まずはView(xaml)から BindingDragDropRowBehavior(正式にはnamespace)を参照可能とする設定を追加します。

xmlns:services="clr-namespace:Template2.WPF.Services"

▼SampleView.xaml(SampleViewがUserControlの場合)

<UserControl x:Class="Template2.WPF.Views.SampleView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:prism="http://prismlibrary.com/"             
             prism:ViewModelLocator.AutoWireViewModel="True"
             xmlns:services="clr-namespace:Template2.WPF.Services">
    ・・・

これで、SampleViewでは、servicesに格納されたクラス(BindingDragDropBehavior)が利用可能となります。

③DataGridに、 BindingDragDropRowBehavior の設定を追加

次に、DataGridに下記コードを追加します。 services:BindingDragDropRowBehaviorは、上記②が設定済みであれば参照可能です。

DataGdidに下記のプロパティを追加します。

x:Name="WorkerGroupMstEntitiesName"
SelectionMode="Single"
services:BindingDragDropRowBehavior.Enabled="True"
services:BindingDragDropRowBehavior.PopupControl="{Binding ElementName=DraggingPopup}"
AllowDrop="True"

実際の実装例は下記となります。

▼SampleView.xaml(DataGridへの実装例)

<DataGrid Style="{StaticResource commonDataGrid}"
            ItemsSource="{Binding WorkerGroupMstEntities}"
            Cursor="Hand"
            Margin="5"
            
            x:Name="WorkerGroupMstEntitiesName"
            SelectionMode="Single"
            services:BindingDragDropRowBehavior.Enabled="True"
            services:BindingDragDropRowBehavior.PopupControl="{Binding ElementName=DraggingPopup}"
            AllowDrop="True">

上記コードのservices:BindingDragDropRowBehavior.PopupControl="{Binding ElementName=DraggingPopup}は、後述するポップアップ表示に必要となります。

④DataGridドラッグ&ドロップ時のポップアップ表示設定を追加

ポップアップは、ドラッグ中にレコードのデータを表示する機能です。
選択、つまりドラッグしているレコードが分かり易くなります。

実装では、下記のように PopupDataGrid の外側 に配置します。

▼SampleView.xaml

<!-- DataGridをドラッグ&ドロップした際のポップアップ表示 -->
<Popup  x:Name="DraggingPopup"
        AllowsTransparency="True"
        IsHitTestVisible="False"
        Placement="RelativePoint"
        PlacementTarget="{Binding ElementName=WorkerGroupMstEntitiesName}">
    <!--  Popup construction Use properties of DraggedObject inside for Binding  -->
    <TextBlock Text="{Binding Path=WorkerGroupName}"/>
</Popup>
 
<DataGrid Style="{StaticResource commonDataGrid}"
            ItemsSource="{Binding WorkerGroupMstEntities}"
            Cursor="Hand"
            Margin="5"
            
            x:Name="WorkerGroupMstEntitiesName"
            SelectionMode="Single"
            services:BindingDragDropRowBehavior.Enabled="True"
            services:BindingDragDropRowBehavior.PopupControl="{Binding ElementName=DraggingPopup}"
            AllowDrop="True">
 
    <DataGrid.Columns>
        <DataGridTextColumn Header="作業者グループコード"
                            Binding="{Binding WorkerGroupCode}"
                            IsReadOnly="True">
        </DataGridTextColumn>
        <DataGridTextColumn Header="作業者グループ名称"
                            Binding="{Binding WorkerGroupName}"
                            MinWidth="400"
                            IsReadOnly="True">
        </DataGridTextColumn>
    </DataGrid.Columns>
 
</DataGrid>

Popup のNameは、DataGrid のPopupControlで設定した名称です。

<Popup>の中

x:Name="**DraggingPopup**"  

が、

<DataGdid>の中

services:BindingDragDropRowBehavior.PopupControl="{Binding ElementName=**DraggingPopup**}"  

に対応しています。

また、ドラッグ時にPopup表示させるテキストは、下記の部分で設定しています。

<Popup>の中

<TextBlock Text="{Binding Path=WorkerGroupName}"/>  

上記の Popup テキストブロックの Binding Path で設定したプロパティ(上記の場合、WorkerGroupName)がドラッグ時に表示されます。

以上です。