Search code examples
c#wpfdatagrid

Drag&Drop cannot work when column is sorted in DataGrid


The re-arrangement of rows work as intended initially, but if a column is sorted drag & drop of rows does not work.

Further, if I cancel column sorting in datagrid, drag and drop feature works fine.

using System.Collections.ObjectModel;

public class Product
{
    public int ProductId { get; set; }
    public string ProductName { get; set; }
    public string ProductPrice { get; set; }
}

public class ProductCollection : ObservableCollection<Product>
{
    public ProductCollection()
    {
        Add(new Product() { ProductId = 111, ProductName = "Books", ProductPrice = "500$" });
        Add(new Product() { ProductId = 222, ProductName = "Cameras", ProductPrice = "600$" });
        Add(new Product() { ProductId = 333, ProductName = "Cell Phones", ProductPrice = "700$" });
        Add(new Product() { ProductId = 444, ProductName = "Clothing", ProductPrice = "800$" });
        Add(new Product() { ProductId = 555, ProductName = "Shoes", ProductPrice = "900$" });
        Add(new Product() { ProductId = 666, ProductName = "Gift Cards", ProductPrice = "500$" });
        Add(new Product() { ProductId = 777, ProductName = "Crafts", ProductPrice = "400$" });
        Add(new Product() { ProductId = 888, ProductName = "Computers", ProductPrice = "430$" });
        Add(new Product() { ProductId = 999, ProductName = "Coins", ProductPrice = "460$" });
        Add(new Product() { ProductId = 332, ProductName = "Cars", ProductPrice = "4600$" });
        Add(new Product() { ProductId = 564, ProductName = "Boats", ProductPrice = "3260$" });
        Add(new Product() { ProductId = 346, ProductName = "Dolls", ProductPrice = "120$" });
        Add(new Product() { ProductId = 677, ProductName = "Gift Cards", ProductPrice = "960$" });

    }
}

MainPage.xaml

<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfRowDragDropSample"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="WpfRowDragDropSample.MainWindow"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <local:ProductCollection x:Key="ProductList"/>
    </Window.Resources>
    <Grid DataContext="{Binding Source={StaticResource ProductList}}">
        <DataGrid d:LayoutOverrides="Width" Margin="0,28,0,0" Name="productsDataGrid"
                  AutoGenerateColumns="False" ItemsSource="{Binding}"
                  SelectionMode="Extended" ColumnWidth="*" AllowDrop="True" >
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding ProductId}" Header="ProductId"></DataGridTextColumn>
                <DataGridTextColumn Binding="{Binding ProductName}" Header="ProductName"></DataGridTextColumn>
                <DataGridTextColumn Binding="{Binding ProductPrice}" Header="ProductPrice"></DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
        <TextBlock TextWrapping="Wrap" Text="DataGrid Row Drag And Drop Sample" VerticalAlignment="Top" Margin="3,1,0,0" Height="24" HorizontalAlignment="Left" Width="268" FontSize="14.667" FontWeight="Bold" FontStyle="Italic"/>       
    </Grid>
</Window>

MainPage.xaml.cs

using System.Windows.Controls.Primitives;

public delegate Point GetPosition(IInputElement element);
int rowIndex = -1;
public MainWindow()
{
    InitializeComponent();
    productsDataGrid.PreviewMouseLeftButtonDown += new MouseButtonEventHandler(productsDataGrid_PreviewMouseLeftButtonDown);
    productsDataGrid.Drop += new DragEventHandler(productsDataGrid_Drop);
}

void productsDataGrid_Drop(object sender, DragEventArgs e)
{
    if (rowIndex < 0)
        return;
    int index = this.GetCurrentRowIndex(e.GetPosition);           
    if (index < 0)
        return;
    if (index == rowIndex)
        return;           
    if (index == productsDataGrid.Items.Count - 1)
    {
        MessageBox.Show("This row-index cannot be drop");
        return;
    }
    ProductCollection productCollection = Resources["ProductList"] as ProductCollection;
    Product changedProduct = productCollection[rowIndex];
    productCollection.RemoveAt(rowIndex);
    productCollection.Insert(index, changedProduct);
}

void productsDataGrid_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    rowIndex = GetCurrentRowIndex(e.GetPosition);
    if (rowIndex < 0)
        return;
    productsDataGrid.SelectedIndex = rowIndex;
    Product selectedEmp = productsDataGrid.Items[rowIndex] as Product;
    if (selectedEmp == null)
        return;
    DragDropEffects dragdropeffects = DragDropEffects.Move;
    if (DragDrop.DoDragDrop(productsDataGrid, selectedEmp, dragdropeffects)
                        != DragDropEffects.None)
    {               
        productsDataGrid.SelectedItem = selectedEmp;
    }
}

private bool GetMouseTargetRow(Visual theTarget, GetPosition position)
{
    Rect rect = VisualTreeHelper.GetDescendantBounds(theTarget);
    Point point = position((IInputElement)theTarget);
    return rect.Contains(point);           
}

private DataGridRow GetRowItem(int index)
{
    if (productsDataGrid.ItemContainerGenerator.Status
            != GeneratorStatus.ContainersGenerated)
        return null;
    return productsDataGrid.ItemContainerGenerator.ContainerFromIndex(index)
                                                    as DataGridRow;
}

private int GetCurrentRowIndex(GetPosition pos)
{
    int curIndex = -1;
    for (int i = 0; i < productsDataGrid.Items.Count; i++)
    {
        DataGridRow itm = GetRowItem(i);
        if (GetMouseTargetRow(itm, pos))
        {
            curIndex = i;
            break;
        }
    }
    return curIndex;
}    

Image 1: Now select a row to drop on place of other row like I select computers product row to put on the top of the grid.

Image 2: Drag and drop selected row to other place.

I would like drag & drop has higher priority than column sorting so both can work simultaneously.


Solution

  • Your sort doesn't work because you are trying to modify the underlying collection while the DataGrid is actively sorting the CollectionView (Microsoft Docs: Collection views).

    You basically have introduced a second sort criteria. The first is the column's sort criteria (e.g., numeric sort). After this primary sort criteria you want to sort by index (essentially you are changing the index of a dragged row).
    This means, instead of moving items around you must project your criteria into a sort description. In other words, if you don't merge those two sorts the column sort will always override your source collection modifications. You must "move" items by sorting them.

    First of all, you must work on collections view only. When the DataGrid sorts, it sorts the ICollectionView of the underlying collection. This means, if you are looking for an index of the sorted (or filtered) DataGrid (or ItemsControl in general), you must retrieve this index from the collection view, for example:

    ItemsControl.Items.IndexOf(item);
    

    And if you are looking for an item of the sorted/filtered ItemsControl:

    ItemsControl.Items.GetItemAt(itemIndex);
    

    Otherwise what you see is not what you get. The indices of a sorted CollectionView and the underlying unsorted collection like ProductCollection are not identical.
    When debugging your original code you should see that you are moving the item in the underlying ProductCollection. But still you see the items not being arranged accordingly in the DataGrid.
    This is because the DataGrid doesn't care how the underlying ProductCollection is ordered as long there is a sort or filter applied to the CollectionView (when clicking a DataGrid column the DataGrid implicitly assigns a SortDescription to the CollectionView it binds to).

    In your special case, where you want to apply your custom sort criteria after the primary sort criteria of the column was applied, you can simplify the sorting: to improve performance you don't have to execute the column sorting every time the user drags a row.
    It is sufficient if you track the index of the items in the collection view after the initial column sort. In other words, after the initial column sort (e.g. by property name) you save a snapshot of the current indices. Because we are now only interested in the dragged row's index relative to the displayed view in the DataGrid. We change the index of the sorted CollectionView and not of the underlying ProductCollection collection.

    To do so we apply our custom sort algorithm once a drop occurred. We can assign a custom sort algorithm by assigning an IComparer to the ListCollectionView.CustomSort property.

    All we do is to change the index of the dropped item and the index of the other items. But because we are working on the ICollectionView we can't really move items. Additionally we have to maintain the original indices that were the result of the initial column sort.
    To get around this we have to abstract away the item index after a previous column sort, so that we can use them as our sort criteria. We could add a new property to the row data model. A cleaner solution would be to create a table to track and manipulate those abstracted row indices. Table structures like Dictionary<K,V> are perfect for fast lookup, what is needed here.

    To achieve this, the following example has introduced a SortedIndex type. It is a linked index object that is able to insert other SortIndex nodes and automatically adjust the index of the following nodes.
    It basically behaves like a doubly linked list. SortIndex will simplify the maintenance of the collection indices. Because we don't have to find the insertion item in the collection in order to virtually move all the following items, we also improve the performance.

    MainPage.xaml

    <Window>  
      <DataGrid ItemsSource="{Binding ProductList}" 
                Sorting="OnProductsDataGridSorting"
                AutoGenerateColumns="False"
                AllowDrop="True">
      <DataGrid.Columns>
        <DataGridTextColumn Binding="{Binding ProductId}" Header="ProductId" />
        <DataGridTextColumn Binding="{Binding ProductName}" Header="ProductName" />
        <DataGridTextColumn Binding="{Binding ProductPrice}" Header="ProductPrice" />
       </DataGrid.Columns>
     </DataGrid>
    </Window>
    

    MainPage.xaml.cs

    partial class MainPage : Window
    {
      public ProductCollection ProductList { get; }
      private bool IsSortIndexingPending { get; set; }
      private Dictionary<Product, SortIndex> ProductSortPriorityTable { get; }
      public delegate Point GetPosition(IInputElement element);
      int rowIndex = -1;
    
      public MainPage()
      {
        InitializeComponent();
    
        this.ProductList = new ProductCollection();
        this.ProductSortPriorityTable = new Dictionary<Product, SortIndex>();
        this.IsSortIndexingPending = true;
    
        productsDataGrid.PreviewMouseLeftButtonDown += new MouseButtonEventHandler(productsDataGrid_PreviewMouseLeftButtonDown);
        productsDataGrid.Drop += new DragEventHandler(productsDataGrid_Drop);
      }
    
      private void OnProductsDataGridSorting(object sender, DataGridSortingEventArgs e)
      {
        this.CurrentColumnSortComparer = ((CollectionView)CollectionViewSource.GetDefaultView(this.ProductList)).Comparer;
        this.ProductSortPriorityTable.Clear();
        this.IsSortIndexingPending = true;
      }
    
      void productsDataGrid_Drop(object sender, DragEventArgs e)
      {
        /*** Your existing code (unchanged) ***/
        
        if (this.rowIndex < 0)
          return;
        int index = GetCurrentRowIndex(e.GetPosition);
        if (index < 0)
          return;
        if (index == this.rowIndex)
          return;
        if (index == this.productsDataGrid.Items.Count - 1)
        {
          _ = MessageBox.Show("This row-index cannot be drop");
          return;
        }
    
        /*** New code ***/
    
        var collectionView = (ListCollectionView)CollectionViewSource.GetDefaultView(this.ProductList);
        IndexCurrentSortedProductListView(CollectionView collectionView);
    
        var droppedProductItem = (Product)collectionView.GetItemAt(this.rowIndex);
        SortIndex droppedProductItemSortIndex = this.ProductSortPriorityTable[droppedProductItem];
        var dropTargetProductItem = (Product)collectionView.GetItemAt(index);
        SortIndex dropTargetProductItemSortIndex = this.ProductSortPriorityTable[dropTargetProductItem];
    
        // Virtually move the dropped row item. 
        // Because the CollectionView does not support moving items, 
        // we have to rearrange the collection view by applying a new sort.
        // This index based sort uses the pre-sorted indices 
        // and not the original indices from the underlying ProductList collection.
        dropTargetProductItemSortIndex.InsertNext(droppedProductItemSortIndex);
    
        // Finally sort by new index based on the pre-sorted collection view
        collectionView.CustomSort = Comparer<Product>.Create((product1, product2) =>
        {
          int rowItemPriority1 = this.ProductSortPriorityTable[product1].Index;
          int rowItemPriority2 = this.ProductSortPriorityTable[product2].Index;
          return rowItemPriority1.CompareTo(rowItemPriority2);
        });
      }
    
      private void IndexCurrentSortedProductListView(CollectionView collectionView)
      {
        if (this.IsSortIndexingPending)
        {
          for (int rowIndex = 0; rowIndex < this.ProductList.Count; rowIndex++)
          {
            SortIndex sortIndex;
            if (rowIndex > 0)
            {
              int previousRowIndex = rowIndex - 1;
              var previousRowItem = (Product)collectionView.GetItemAt(previousRowIndex);
              SortIndex previousSortIndex = this.ProductSortPriorityTable[previousRowItem];
              sortIndex = new SortIndex(rowIndex, previousSortIndex, null);
              previousSortIndex.SetNext(sortIndex);
            }
            else
            {
              sortIndex = new SortIndex(rowIndex);
            }
    
            var rowItem = (Product)collectionView.GetItemAt(rowIndex);
            this.ProductSortPriorityTable.Add(rowItem, sortIndex);
          }
    
          this.IsSortIndexingPending = false;
        }
      }
    
      void productsDataGrid_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
      {
        /*** Nothing has changed here ***/
      }
    
      private bool GetMouseTargetRow(Visual theTarget, GetPosition position)
      {   
        /*** Nothing has changed here ***/
      }
    
      private DataGridRow GetRowItem(int index)
      {
        /*** Nothing has changed here ***/
      }
    
      private int GetCurrentRowIndex(GetPosition pos)
      {    
        /*** Nothing has changed here ***/
      }
    }
    

    SortIndex.cs

    internal class SortIndex
    {
      public SortIndex(int index) : this(index, null, null)
      {
      }
    
      public SortIndex(int index, SortIndex? previous, SortIndex? next)
      {
        this.Index = index;
        this.Previous = previous;
        this.Next = next;
      }
    
      public int Index { get; private set; }
      public SortIndex? Previous { get; private set; }
      public SortIndex? Next { get; private set; }
      public bool HasNext => this.Next is not null;
      public bool HasPrevious => this.Previous is not null;
    
      public void InsertNext(SortIndex nextSortIndex)
      {
        if (nextSortIndex.HasPrevious)
        {
          nextSortIndex.Previous.Next = nextSortIndex.Next;
        }
    
        if (nextSortIndex.HasNext)
        {
          nextSortIndex.Next.Previous = nextSortIndex.Previous;
        }
    
        nextSortIndex.Previous = this;
        nextSortIndex.Next = this.Next;
        if (this.HasNext)
        {
          this.Next.Previous = nextSortIndex;
        }
    
        this.Next = nextSortIndex;
        SetNextIndex(this.Index + 1);
      }
    
      public void SetNext(SortIndex nextSortIndex)
      {
        this.Next = nextSortIndex;
        nextSortIndex.Previous = this;
        SetNextIndex(this.Index + 1);
      }
    
      public void SetPrevious(SortIndex previousSortIndex)
      {
        this.Previous = previousSortIndex;
        previousSortIndex.Next = this;
        SetIndex(previousSortIndex.Index + 1);
      }
    
      public void Increment()
      {
        this.Index++;
        this.Next?.Increment();
      }
    
      public void SetIndex(int index)
      {
        this.Index = index;
        SetNextIndex(this.Index + 1);
      }
    
      private void SetNextIndex(int nextIndex) => this.Next?.SetIndex(nextIndex);
    }