AvaloniaUI / Avalonia

Develop Desktop, Embedded, Mobile and WebAssembly apps with C# and XAML. The most popular .NET UI client technology
https://avaloniaui.net
MIT License
25.98k stars 2.25k forks source link

DataGrid Drag-and-drop #13581

Open alexandrehtrb opened 1 year ago

alexandrehtrb commented 1 year ago

When using a DataGrid, I would like to perform drag-and-drop to rearrange rows.

The TreeDataGrid drag-and-drop works, but there are some bugs and required effort to migrate from DataGrid to TreeDataGrid.

@aldelaro5 showed in a discussion how to make drag-and-drop for DataGrid. I tried and it works.

However, there is a bug in the proposed solution that when someone tries to select a text from a DataGridTextCell, it understands as if the person wants to perform drag-and-drop. Basically, drag-and-drop should be disabled if a DataGrid cell is being edited.

I believe this feature is very much needed, in fact, drag-and-drop should come by default in DataGrid.

Update

There is a full working solution in the Avalonia.Xaml.Behaviors repo, check this PR

timunie commented 1 year ago

check Avalonia.Xaml.Behaviors . The samples should give you a clue how it can be implemented.

alexandrehtrb commented 1 year ago

How can I check if a DataGrid cell is being edited? To block drag-and-drop if edition is being made

timunie commented 1 year ago

I personally would make DataGridRowHeader to be a drag moving thumb if I need it to be editable

timunie commented 1 year ago

image

can't share the code, but may give a clue

alexandrehtrb commented 1 year ago

@timunie , thanks a lot man!!

I will show here how I managed to do this, if anyone comes looking up for a solution.

1) Include in your .csproj the Avalonia.Xaml.Behaviors NuGet package.

2) Create a .xaml for your DataGrid drag-and-drop styles, then reference this file in App.xaml styles:

<Styles
  xmlns="https://github.com/avaloniaui"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity"
  xmlns:iac="clr-namespace:Avalonia.Xaml.Interactions.Custom;assembly=Avalonia.Xaml.Interactions.Custom"
  xmlns:idd="clr-namespace:Avalonia.Xaml.Interactions.DragAndDrop;assembly=Avalonia.Xaml.Interactions.DragAndDrop"
  xmlns:b="using:YourNamespace.Behaviors">

  <Style Selector="DataGrid.DragAndDrop">
    <Setter
      Property="RowHeaderWidth"
      Value="24" />
  </Style>

  <!-- This makes only the DataGridRowHeader available for dragging, instead of making the entire row draggable -->
  <!-- Which prevents a conflict between text selection in a cell and drag-and-drop -->
  <Style Selector="DataGrid.DragAndDrop DataGridRowHeader">
    <Setter Property="(i:Interaction.Behaviors)">
      <i:BehaviorCollectionTemplate>
        <i:BehaviorCollection>
          <idd:ContextDragBehavior HorizontalDragThreshold="3" VerticalDragThreshold="3" />
        </i:BehaviorCollection>
      </i:BehaviorCollectionTemplate>
    </Setter>
    <Setter Property="Content">
      <Template>
        <Image
          Margin="12,0,12,0"
          Width="12"
          Height="12"
          VerticalAlignment="Center"
          HorizontalAlignment="Center">
          <Image.Source>
            <!-- Use your own image here, I used this: https://www.svgrepo.com/svg/347759/grabber -->
            <DrawingImage Drawing="{StaticResource IconGrabber}" />
          </Image.Source>
        </Image>
      </Template>
    </Setter>
  </Style>

  <Style Selector="DataGrid.MyItemsDragAndDrop">
    <Style.Resources>
      <b:MyItemsDataGridDropHandler x:Key="MyItemsDataGridDropHandler" />
    </Style.Resources>
    <Setter Property="(i:Interaction.Behaviors)">
      <i:BehaviorCollectionTemplate>
        <i:BehaviorCollection>
          <idd:ContextDropBehavior Handler="{StaticResource MyItemsDataGridDropHandler}" />
        </i:BehaviorCollection>
      </i:BehaviorCollectionTemplate>
    </Setter>
  </Style>
</Styles>

3) Create a DataGridDropHandler for your table and items view model:

using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.VisualTree;
using Avalonia.Xaml.Interactions.DragAndDrop;
using YourNamespace.ViewModels;

namespace YourNamespace.Behaviors;

public class MyItemsDataGridDropHandler : DropHandlerBase
{
    private bool Validate<T>(DataGrid dg, DragEventArgs e, object? sourceContext, object? targetContext, bool bExecute)
    {
        if (sourceContext is not ItemViewModel sourceItem
            || targetContext is not MainWindowViewModel vm
            || listBox.GetVisualAt(e.GetPosition(listBox)) is not Control targetControl
            || targetControl.DataContext is not ItemViewModel targetItem)
        {
            return false;
        }

        var items = vm.Items;
        var sourceIndex = items.IndexOf(sourceItem);
        var targetIndex = items.IndexOf(targetItem);

        if (sourceIndex < 0 || targetIndex < 0)
        {
            return false;
        }

        switch (e.DragEffects)
        {
            case DragDropEffects.Copy:
            {
                if (bExecute)
                {
                    var clone = new ItemViewModel() { Title = sourceItem.Title + "_copy" };
                    InsertItem(items, clone, targetIndex + 1);
                }
                return true;
            }
            case DragDropEffects.Move:
            {
                if (bExecute)
                {
                    MoveItem(items, sourceIndex, targetIndex);
                }
                return true;
            }
            case DragDropEffects.Link:
            {
                if (bExecute)
                {
                    SwapItem(items, sourceIndex, targetIndex);
                }
                return true;
            }
            default:
                return false;
        }
    }

    public override bool Validate(object? sender, DragEventArgs e, object? sourceContext, object? targetContext, object? state)
    {
        if (e.Source is Control && sender is DataGrid dg)
        {
            return Validate<ItemViewModel>(dg, e, sourceContext, targetContext, false);
        }
        return false;
    }

    public override bool Execute(object? sender, DragEventArgs e, object? sourceContext, object? targetContext, object? state)
    {
        if (e.Source is Control && sender is DataGrid dg)
        {
            return Validate<ItemViewModel>(dg, e, sourceContext, targetContext, true);
        }
        return false;
    }
}

4) In your DataGrid .xaml, include the properties HeadersVisibility="All" and Classes="DragAndDrop MyItemsDragAndDrop":

<DataGrid
        Name="dgMyItems"
        AutoGenerateColumns="False"
        ItemsSource="{Binding Items}"
        CanUserResizeColumns="True"
        HeadersVisibility="All"
        Classes="DragAndDrop MyItemsDragAndDrop">
...
</DataGrid>

Final result:

https://github.com/AvaloniaUI/Avalonia/assets/27026741/b8c8db9b-654a-4e1c-be18-ff0372cf4081

zndxcvbn commented 1 week ago

How can I add an icon to the first column above the drag cells?