4

I'm using DevExpress controls in my WinForms project. I need to re-order the group rows(include their details) by drag and drop using BehaviorManager(in addition to re-ordering the rows inside the details). For drag&drop rows I wrote following codes:

private void Behavior_DragOver(object sender, DragOverEventArgs e) 
{
        DragOverGridEventArgs args = DragOverGridEventArgs.GetDragOverGridEventArgs(e);
        e.InsertType = args.InsertType;
        e.InsertIndicatorLocation = args.InsertIndicatorLocation;
        e.Action = args.Action;
        Cursor.Current = args.Cursor;
        args.Handled = true;
}
private void Behavior_DragDrop(object sender, DragDropEventArgs e) 
{
        GridView targetGrid = e.Target as GridView;
        GridView sourceGrid = e.Source as GridView;
        if (e.Action == DragDropActions.None || targetGrid != sourceGrid)
            return;
        DataTable sourceTable = sourceGrid.GridControl.DataSource as DataTable;

        Point hitPoint = targetGrid.GridControl.PointToClient(Cursor.Position);
        GridHitInfo hitInfo = targetGrid.CalcHitInfo(hitPoint);

        int[] sourceHandles = e.GetData<int[]>();

        int targetRowHandle = hitInfo.RowHandle;
        int targetRowIndex = targetGrid.GetDataSourceRowIndex(targetRowHandle);

        List<DataRow> draggedRows = new List<DataRow>();
        foreach (int sourceHandle in sourceHandles) {
            int oldRowIndex = sourceGrid.GetDataSourceRowIndex(sourceHandle);
            DataRow oldRow = sourceTable.Rows[oldRowIndex];
            draggedRows.Add(oldRow);
        }

        int newRowIndex;

        switch (e.InsertType) {
            case InsertType.Before:
                newRowIndex = targetRowIndex > sourceHandles[sourceHandles.Length - 1]
                ? targetRowIndex - 1 : targetRowIndex;
                for (int i = draggedRows.Count - 1; i >= 0; i--) {
                    DataRow oldRow = draggedRows[i];
                    DataRow newRow = sourceTable.NewRow();
                    newRow.ItemArray = oldRow.ItemArray;
                    sourceTable.Rows.Remove(oldRow);
                    sourceTable.Rows.InsertAt(newRow, newRowIndex);
                }
                break;
            case InsertType.After:
                newRowIndex = targetRowIndex < sourceHandles[0] 
                ? targetRowIndex + 1 : targetRowIndex;
                for (int i = 0; i < draggedRows.Count; i++) {
                    DataRow oldRow = draggedRows[i];
                    DataRow newRow = sourceTable.NewRow();
                    newRow.ItemArray = oldRow.ItemArray;
                    sourceTable.Rows.Remove(oldRow);
                    sourceTable.Rows.InsertAt(newRow, newRowIndex);
                }
                break;
            default:
                newRowIndex = -1;
                break;
        }
        int insertedIndex = targetGrid.GetRowHandle(newRowIndex);
        targetGrid.FocusedRowHandle = insertedIndex;
        targetGrid.SelectRow(targetGrid.FocusedRowHandle);
   }

For example I want to replace Level3 and Level6 group rows position by drag & drop.

How can I do it? enter image description here

Masoud
  • 8,020
  • 12
  • 62
  • 123
  • 1
    The DevExpress sample this is based upon appears to be .NET Framework 4.8. Will this continue to be the case as you use it in your real application? (I ask this because it relates to the DPI-awareness of a possible solution). – IVSoftware Mar 26 '23 at 10:35
  • 1
    @IVSoftware: Yes, I will continue to use .NET Framework. – Masoud Mar 26 '23 at 12:25

3 Answers3

2

I tried some changes but I didn't applied successfully by using BehaviorManager. Finally, I used grid control grid_DragDrop,grid_DragOver,view_MouseDown and view_MouseMove events instead of behavior manager. Additionally, I added some code lines to the SetUpGrid function.

   public void SetUpGrid(GridControl grid, DataTable table) {
        GridView view = grid.MainView as GridView;
        grid.DataSource = table;
        view.OptionsBehavior.Editable = false;
        view.Columns["Level"].GroupIndex = 1;
        
        //new codes
        gridControl1.AllowDrop = true;
        grid.DragDrop += grid_DragDrop;
        grid.DragOver += grid_DragOver;
        view.MouseDown += view_MouseDown;
        view.MouseMove += view_MouseMove;
    }

    void view_MouseDown(object sender, MouseEventArgs e)
    {
        GridView view = sender as GridView;
        downHitInfo = null;
        GridHitInfo hitInfo = view.CalcHitInfo(new Point(e.X, e.Y));
        if (Control.ModifierKeys != Keys.None) return;
        if ((e.Button == MouseButtons.Left || e.Button == MouseButtons.Right) && hitInfo.RowHandle >= 0)
            downHitInfo = hitInfo;
    }

    void view_MouseMove(object sender, MouseEventArgs e)
    {
        GridView view = sender as GridView;
        if (e.Button == MouseButtons.Left && downHitInfo != null)
        {
            Size dragSize = SystemInformation.DragSize;
            Rectangle dragRect = new Rectangle(new Point(downHitInfo.HitPoint.X - dragSize.Width / 2,
                downHitInfo.HitPoint.Y - dragSize.Height / 2), dragSize);
            if (!dragRect.Contains(new Point(e.X, e.Y)))
            {
                DataTable table = view.GridControl.DataSource as DataTable;
                int[] rows = view.GetSelectedRows();
                List<DataRow> rwlist = new List<DataRow>();
                for (int i = rows.Length - 1; i >= 0; i--)
                {
                    DataRowView rv = (DataRowView)gridView1.GetRow(rows[i]);
                    rwlist.Add(rv.Row);
                }
                view.GridControl.DoDragDrop(rwlist, DragDropEffects.Move);
                downHitInfo = null;
                DevExpress.Utils.DXMouseEventArgs.GetMouseArgs(e).Handled = true;
            }
        }
    }
    

    void grid_DragOver(object sender, DragEventArgs e)
    {
        if (e.Data.GetDataPresent(typeof(List<DataRow>)))
        {
            e.Effect = DragDropEffects.Move;
        }
        else
            e.Effect = DragDropEffects.None;
    }

    GridHitInfo downHitInfo = null;
    void grid_DragDrop(object sender, DragEventArgs e)
    {
        GridControl grid = sender as GridControl;
        DataTable table = grid.DataSource as DataTable;
        GridView view = grid.MainView as GridView;
        Point pt = grid.PointToClient(new Point(e.X, e.Y));
        GridHitInfo hitInfo = view.CalcHitInfo(pt);
        int sourceRow = downHitInfo.RowHandle;
        int targetRow = hitInfo.RowHandle;
        List<DataRow> rows = e.Data.GetData(typeof(List<DataRow>)) as List<DataRow>;

        DataRowView drV = view.GetRow(targetRow >= 0 ? targetRow : sourceRow) as DataRowView;
        object TargetLevel = drV.Row["Level"];

        foreach (DataRow row in rows)
        {
            if (row.Table != table)
            {
                continue;
            }
            if (view.SortInfo.GroupCount > 0)
            {
                if (view.SortInfo.GroupCount == 1)
                {
                    if (view.GroupedColumns[0].FieldName != "Level")
                        return;
                }
                else
                    return;
            }
            if (targetRow < 0)
                return;
            if (targetRow == sourceRow)
            {
                return;
            }
            DataRow newRow = table.NewRow() as DataRow;
            newRow.ItemArray = row.ItemArray;
            newRow["Level"] = TargetLevel;
            if (targetRow >= 0)
            {
                row.Delete();
            }
            int to = targetRow;
            if (table.Rows.Count == to)
            {
                if (sourceRow >= targetRow)
                    to = table.Rows.Count - 1;
            }
            table.Rows.InsertAt(newRow, to);
            if (sourceRow < targetRow) targetRow--;

        }
    }
muludag
  • 134
  • 1
  • 8
2

When a column is grouped, it is also sorted at the same time. Therefore you have to prevent sorting (at least for the Level column). I have here an old version of DevExpress without BehaviorManager. But you can also achieve the behavior with this old version.
You can use DragDrop, DragOver, MouseDown and MouseMove (like muludag answer). Drag and drop is only possible with the Grouped column Level (Update by PinBack):

public class LevelDragDropInfo
{
    public GridHitInfo DragDropHitInfo { get; set; }
    public decimal Level { get; set; }
    public List<decimal> LevelExpanded { get; private set; }

    public void SnapShotGroups(GridView poGridView)
    {
        this.LevelExpanded = new List<decimal>();

        if (poGridView.GroupedColumns.Count > 0)
        {
            for (int i = -1; poGridView.IsValidRowHandle(i); i--)
            {
                if (poGridView.GetRowExpanded(i))
                {
                    var loRow = poGridView.GetDataRow(poGridView.GetDataRowHandleByGroupRowHandle(i)) as DataRow;
                    if (loRow != null)
                    {
                        this.LevelExpanded.Add(loRow.Field<decimal>("Level"));
                    }

                }
            }
        }
    }

    public void RestoreGroups(GridView poGridView)
    {
        poGridView.CollapseAllGroups();
        if (this.LevelExpanded?.Count > 0)
        {
            for (int i = -1; poGridView.IsValidRowHandle(i); i--)
            {
                var loRow = poGridView.GetDataRow(poGridView.GetDataRowHandleByGroupRowHandle(i)) as DataRow;
                if (loRow != null &&
                    this.LevelExpanded.Contains(loRow.Field<decimal>("Level")))
                {
                    poGridView.SetRowExpanded(i, true, false);
                }
            }
        }
    }

}

private LevelDragDropInfo moLevelDragDropInfo;

public void SetUpGrid(GridControl grid, DataTable table)
{
    GridView view = grid.MainView as GridView;
    grid.DataSource = table;
    grid.AllowDrop = true;
    view.OptionsBehavior.Editable = false;

    view.Columns["Level"].SortMode = ColumnSortMode.Custom;
    //Prevent Sorting on Grouping
    view.CustomColumnSort += this.View_CustomColumnSort;
    view.Columns["Level"].GroupIndex = 1;
   
    //Drag/Drop Events
    grid.DragDrop += this.Grid_DragDrop;
    grid.DragOver += this.Grid_DragOver;
    view.MouseDown += this.View_MouseDown;
    view.MouseMove += this.View_MouseMove;
}

private void View_CustomColumnSort(object sender, DevExpress.XtraGrid.Views.Base.CustomColumnSortEventArgs e)
{
    if (e.Column.FieldName == "Level")
    {
        e.Handled = true;
    }
}

public void View_MouseDown(object sender, MouseEventArgs e)
{
    this.moLevelDragDropInfo = null;

    GridView loView = sender as GridView;
    GridHitInfo loHitInfo = loView.CalcHitInfo(new Point(e.X, e.Y));

    if (e.Button == MouseButtons.Left
        && loHitInfo.InGroupRow
        && loHitInfo.RowInfo is GridGroupRowInfo)
    {
        if (loHitInfo.RowInfo is GridGroupRowInfo loGridGroupRowInfo)
        {
            if (loGridGroupRowInfo.RowKey is DevExpress.Data.GroupRowInfo loGroupRowInfo)
            {
                this.moLevelDragDropInfo = new LevelDragDropInfo()
                {
                    DragDropHitInfo = loHitInfo,
                    Level = Convert.ToDecimal(loGroupRowInfo.GroupValue)
                };
            }
        }
    }
}

private void View_MouseMove(object sender, MouseEventArgs e)
{
    GridView loView = sender as GridView;
    if (this.moLevelDragDropInfo != null && e.Button == MouseButtons.Left)
    {
        System.Drawing.Point loPoint = new System.Drawing.Point(e.X, e.Y);
        if ((Math.Abs(loPoint.X - this.moLevelDragDropInfo.DragDropHitInfo.HitPoint.X) > 5 || Math.Abs(loPoint.Y - this.moLevelDragDropInfo.DragDropHitInfo.HitPoint.Y) > 5))
        {
            loView.GridControl.DoDragDrop(this.moLevelDragDropInfo, DragDropEffects.All);
        }
    }
}

private void Grid_DragOver(object sender, DragEventArgs e)
{
    GridControl loGrid = sender as GridControl;
    GridView loView = loGrid.MainView as GridView;
    GridHitInfo loDropHitInfo = loView.CalcHitInfo(loGrid.PointToClient(new Point(e.X, e.Y)));

    if (e.Data.GetDataPresent(typeof(LevelDragDropInfo)) &&
        (loDropHitInfo.InGroupRow || loDropHitInfo.InDataRow))
    {
        e.Effect = DragDropEffects.Move;
        return;
    }

    e.Effect = DragDropEffects.None;
}

private void Grid_DragDrop(object sender, DragEventArgs e)
{
    try
    {
        GridControl loGrid = sender as GridControl;
        DataTable loDataTable = loGrid.DataSource as DataTable;
        GridView loView = loGrid.MainView as GridView;
        GridHitInfo loDropHitInfo = loView.CalcHitInfo(loGrid.PointToClient(new Point(e.X, e.Y)));
        LevelDragDropInfo loLevelDragDropInfo = e.Data.GetData(typeof(LevelDragDropInfo)) as LevelDragDropInfo;
        GridGroupRowInfo loDropGroupRowInfo = loDropHitInfo.RowInfo as GridGroupRowInfo;
        GridDataRowInfo loDropDataRowInfo = loDropHitInfo.RowInfo as GridDataRowInfo;
        decimal? lnLevelTo = null;

        if (loDropGroupRowInfo != null)
        {
            lnLevelTo = Convert.ToDecimal((loDropGroupRowInfo.RowKey as DevExpress.Data.GroupRowInfo).GroupValue);
        }
        else if (loDropDataRowInfo != null)
        {
            DataRow loRow = loView.GetDataRow(loDropDataRowInfo.RowHandle) as DataRow;
            if (loRow != null)
            {
                lnLevelTo = loRow.Field<decimal>("Level");
            }
        }

        if (loLevelDragDropInfo != null && lnLevelTo.HasValue)
        {
            decimal lnLevelFrom = loLevelDragDropInfo.Level;
            List<object[]> loMoveRows = new List<object[]>();

            if (lnLevelFrom == lnLevelTo)
                return;

            loLevelDragDropInfo.SnapShotGroups(loView);

            //Remove from Table
            foreach (DataRow loRow in loDataTable.Rows.Cast<DataRow>().ToList())
            {
                if (loRow.Field<decimal>("Level") == lnLevelFrom)
                {
                    loMoveRows.Add(loRow.ItemArray);
                    loDataTable.Rows.Remove(loRow);
                }
            }

            //Insert before
            //int lnInsertIndex = loDataTable.Rows.IndexOf(loDataTable.Rows
            //    .Cast<DataRow>()
            //    .First(item => item.Field<decimal>("Level") == lnLevelTo));

            //Insert after
            int lnInsertIndex = loDataTable.Rows.IndexOf(loDataTable.Rows
                .Cast<DataRow>()
                .Last(item => item.Field<decimal>("Level") == lnLevelTo)) + 1;

            loMoveRows.Reverse();
            foreach (var loValues in loMoveRows)
            {
                DataRow loNewRow = loDataTable.NewRow();
                loNewRow.ItemArray = loValues;
                loDataTable.Rows.InsertAt(loNewRow, lnInsertIndex);
            }

            loLevelDragDropInfo.RestoreGroups(loView);
            this.moLevelDragDropInfo = null;
        }
    }
    catch (Exception exp)
    {
        MessageBox.Show(exp.ToString());
    }
}

Edit by Masoud:

By following changes we can reorder the grouped rows and rows both:

 private void View_MouseDown(object sender, MouseEventArgs e)
 {
        this.moDragDropHitInfo = null;

        GridView loView = sender as GridView;
        GridHitInfo loHitInfo = loView.CalcHitInfo(new Point(e.X, e.Y));

        this.moDragDropHitInfo = loHitInfo;

        if (moDragDropHitInfo.InRowCell)
        {
            gridControl1.AllowDrop = false;
        }
        else if (moDragDropHitInfo.InGroupRow)
        {
            gridControl1.AllowDrop = true;
        }
 }

 private void View_MouseMove(object sender, MouseEventArgs e)
 {
        GridView loView = sender as GridView;
        if (this.moDragDropHitInfo != null && e.Button == MouseButtons.Left)
        {
            System.Drawing.Point loPoint = new System.Drawing.Point(e.X, e.Y);
            if ((Math.Abs(loPoint.X - this.moDragDropHitInfo.HitPoint.X) > 5 || Math.Abs(loPoint.Y - this.moDragDropHitInfo.HitPoint.Y) > 5))
            {
                if (this.moDragDropHitInfo.RowInfo is GridGroupRowInfo)
                {
                    gridControl1.AllowDrop = true;
                    loView.GridControl.DoDragDrop(this.moDragDropHitInfo.RowInfo as GridGroupRowInfo, DragDropEffects.All);
                }
                else
                {
                    gridControl1.AllowDrop = false;
                }
            }
        }
  }
PinBack
  • 2,499
  • 12
  • 16
  • 1
    Thank you, I edited your answer to reorder the grouped rows and rows both. now I have 2 more questions: 1)using `BehaviorManager` I can specify to drop the dragging row Before or After target row, is it possible to do that using your code? and 2)When I reorder a grouped row is it possible to keep expand or collapse status of dragged group row? – Masoud Mar 25 '23 at 09:25
  • 1
    I have updated my answer. Now you have a helper class `LevelDragDropInfo` where you can hold the Drag Level and keep the expanded groups. With the code snipped a group can also be dropped on a DataRow. – PinBack Mar 27 '23 at 08:43
2

This answer uses a custom GridViewEx class inherited from GridView which allows the objective of reordering grouped rows using drag & drop to be achieved in a clean manner without a lot of code mixed in with the main form. Your post and additional comments state two requirements, and I have added a third goal to make the look and feel similar to the drag drop functionality that already exists for the records.

  • Before or After Target Row
  • Expanded rows stay expanded
  • Drag feedback similar to record version

This custom version is simply swapped out manually in the Designer.cs file.

level1-above-level5


The feedback line in this example is blue for "Before Target Row" and red for "After Target Row".

level2-below-level6


Groups that are expanded before an operation remain in that state.

expanded op


GridViewEx - The demo project is available to clone from GitHub.

GridViewEx maintains its own GroupDragDropState to avoid potential conflicts with BehaviorManager operations.

enum GroupDragDropState
{
    None,
    Down,
    Drag,
    Drop,
}

The Drag state is entered if the mouse-down cursor travels more than 10 positions in any direction.

internal class GridViewEx : GridView
{
    public GridViewEx()
    {
        MouseDown += OnMouseDown;
        MouseMove += OnMouseMove;
        MouseUp += OnMouseUp;
        CustomDrawGroupRow += OnCustomDrawGroupRow;
        DataSourceChanged += OnDataSourceChanged;
        DisableCurrencyManager = true; // Use this setting From sample code.
        GroupRowCollapsing += OnGroupRowCollapsing;
        CustomColumnSort += OnCustomColumnSort;
    }

    protected virtual void OnMouseDown(object sender, MouseEventArgs e)
    {
        var hittest = CalcHitInfo(e.Location);
        var screenLocation = PointToScreen(e.Location);
        _mouseDownClient = e.Location;
        _isGroupRow = hittest.RowInfo != null && hittest.RowInfo.IsGroupRow;
        if (_isGroupRow)
        {
            var gridGroupInfo = (GridGroupRowInfo)hittest.RowInfo;
            _dragFeedbackLabel.RowBounds = hittest.RowInfo.Bounds.Size;
            DragRowInfo = gridGroupInfo;
            _isExpanded = gridGroupInfo.IsGroupRowExpanded;
        }
    }

    protected virtual void OnMouseMove(object sender, MouseEventArgs e)
    {
        if (Control.MouseButtons.Equals(MouseButtons.Left))
        {
            _mouseDeltaX = _mouseDownClient.X - e.Location.X;
            _mouseDeltaY = _mouseDownClient.Y - e.Location.Y;
            if (Math.Abs(_mouseDeltaX) > 10 || Math.Abs(_mouseDeltaY) > 10)
            {
                GroupDragDropState = GroupDragDropState.Drag;
            }
            var hittest = CalcHitInfo(e.Location);
            if ((hittest.RowInfo == null) || hittest.RowInfo.Equals(DragRowInfo) || !hittest.RowInfo.IsGroupRow)
            {
                CurrentGroupRowInfo = null;
            }
            else
            {
                CurrentGroupRowInfo = (GridGroupRowInfo)hittest.RowInfo;
                var deltaY = e.Location.Y - CurrentGroupRowInfo.Bounds.Location.Y;
                var mid = CurrentGroupRowInfo.Bounds.Height / 2;
                DropBelow = deltaY >= mid;
            }
        }
    }

    protected virtual void OnMouseUp(object sender, MouseEventArgs e)
    {
        switch (GroupDragDropState)
        {
            case GroupDragDropState.None:
                break;
            case GroupDragDropState.Down:
                GroupDragDropState = GroupDragDropState.None;
                break;
            case GroupDragDropState.Drag:
                GroupDragDropState = GroupDragDropState.Drop;
                break;
            case GroupDragDropState.Drop:
                GroupDragDropState = GroupDragDropState.None;
                break;
        }
    }
    private Point _mouseDownClient = Point.Empty;
    private int _mouseDeltaX = 0;
    private int _mouseDeltaY = 0;
    .
    .
    .
}

Drag Feedback

Entering the drag state takes a screenshot of the clicked row. To ensure the proper operation of the Graphics.CopyFromScreen method, the appconfig.cs has been modified to per-screen DPI awareness.

appconfig.cs

<?xml version="1.0"?>
<configuration>
    <startup>   
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/>
    </startup>
    <System.Windows.Forms.ApplicationConfigurationSection>
        <add key="DpiAwareness" value="PerMonitorV2" />
    </System.Windows.Forms.ApplicationConfigurationSection>
</configuration>

GridViewEx.cs

protected virtual void OnGroupDragDropStateChanged()
{
    switch (GroupDragDropState)
    {
        case GroupDragDropState.None:
            break;
        case GroupDragDropState.Down:
            break;
        case GroupDragDropState.Drag:
            if (_isGroupRow)
            {
                getRowScreenshot();
            }
            _dragFeedbackLabel.Visible = true;
            break;
        case GroupDragDropState.Drop:
            _dragFeedbackLabel.Visible = false;
            OnDrop();
            break;
        default:
            break;
    }
}

void getRowScreenshot()
{
    // MUST be set to DPI AWARE in config.cs
    var ctl = GridControl;
    var screenRow = ctl.PointToScreen(DragRowInfo.Bounds.Location);
    var screenParent = ctl.TopLevelControl.Location;

    using (var srceGraphics = ctl.CreateGraphics())
    {
        var size = DragRowInfo.Bounds.Size;

        var bitmap = new Bitmap(size.Width, size.Height, srceGraphics);
        var destGraphics = Graphics.FromImage(bitmap);
        destGraphics.CopyFromScreen(screenRow.X, screenRow.Y, 0, 0, size);
        _dragFeedbackLabel.BackgroundImage = bitmap;
    }
}

This image is assigned to the BackgroundImage of the _dragFeedbackLabel member which is a borderless form that can be drawn outside the rectangle of the main form. When visible, this form tracks the mouse cursor movement by means of a MessageFilter intercepting WM_MOUSEMOVE messages.

class DragFeedback : Form, IMessageFilter
{
    const int WM_MOUSEMOVE = 0x0200;

    public DragFeedback()
    {
        StartPosition = FormStartPosition.Manual;
        FormBorderStyle = FormBorderStyle.None;
        BackgroundImageLayout = ImageLayout.Stretch;
        Application.AddMessageFilter(this);
        Disposed += (sender, e) => Application.RemoveMessageFilter(this);
    }

    protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified)
    {
        if (RowBounds == null)
        {
            base.SetBoundsCore(x, y, width, height, specified);
        }
        else
        {
            base.SetBoundsCore(x, y, RowBounds.Width, RowBounds.Height, specified);
        }
    }

    public bool PreFilterMessage(ref Message m)
    {
        if(MouseButtons == MouseButtons.Left && m.Msg.Equals(WM_MOUSEMOVE)) 
        {
            Location = MousePosition;
        }
        return false;
    }

    Point _mouseDownPoint = Point.Empty;
    Point _origin = Point.Empty;

    public Size RowBounds { get; internal set; }
    public new Image BackgroundImage
    {
        get => base.BackgroundImage;
        set
        {
            if((value == null) || (base.BackgroundImage == null))
            {
                base.BackgroundImage = value;
            }
        }
    }
    protected override void OnVisibleChanged(EventArgs e)
    {
        base.OnVisibleChanged(e);
        if(!Visible)
        {
            base.BackgroundImage?.Dispose(); ;
            base.BackgroundImage = null;
        }
    }
}

The row dividers are drawn by handling the GridView.CustomDrawGroupRow event.

protected virtual void OnCustomDrawGroupRow(object sender, RowObjectCustomDrawEventArgs e)
{
    if (e.Info is GridRowInfo ri)
    {
        using (var pen = new Pen(DropBelow ? Brushes.LightSalmon : Brushes.Aqua, 4F))
        {
            switch (GroupDragDropState)
            {
                case GroupDragDropState.Drag:
                    if (CurrentGroupRowInfo != null)
                    {
                        if (ri.RowHandle == CurrentGroupRowInfo.RowHandle)
                        {
                            e.DefaultDraw();
                            int y;
                            if (DropBelow)
                            {
                                y = ri.Bounds.Y + CurrentGroupRowInfo.Bounds.Height - 2;
                            }
                            else
                            {
                                y = ri.Bounds.Y + 1;
                            }
                            e.Graphics.DrawLine(pen,
                                ri.Bounds.X, y,
                                ri.Bounds.X + ri.Bounds.Width, y);
                            e.Handled = true;
                        }
                    }
                    break;
            }
        }
    }
}

When the DropBelow value toggles at the midpoint of the drag over or when the target row changes, the affected row(s) need to be redrawn.

public bool DropBelow
{
    get => _dropBelow;
    set
    {
        if (!Equals(_dropBelow, value))
        {
            _dropBelow = value;
#if true
            // "Minimal redraw" version
            RefreshRow(CurrentGroupRowInfo.RowHandle);
#else
            // But if drawing artifacts are present, refresh
            // the entire control surface instead.
            GridControl.Refresh();
#endif
        }
    }
}
bool _dropBelow = false;

OnDrop

The ItemsArray for the removed records is stashed in a dictionary to allow reassignment before inserting the same DataRow instance at a new index. Adjustments to the insert operation are made depending on the value of the DropBelow boolean which was set in the MouseMove handler.

protected virtual void OnDrop()
{
    var dataTable = (DataTable)GridControl.DataSource;
    if (!((DragRowInfo == null) || (CurrentGroupRowInfo == null)))
    {
        Debug.WriteLine($"{DragRowInfo.GroupValueText} {CurrentGroupRowInfo.GroupValueText}");
        var drags =
            dataTable
            .Rows
            .Cast<DataRow>()
            .Where(_ => _[CurrentGroupRowInfo.Column.FieldName]
            .Equals(DragRowInfo.EditValue)).ToArray();

        var dict = new Dictionary<DataRow, object[]>();
        foreach (var dataRow in drags)
        {
            dict[dataRow] = dataRow.ItemArray;
            dataTable.Rows.Remove(dataRow);
        }

        DataRow receiver =
            dataTable
            .Rows
            .Cast<DataRow>()
            .FirstOrDefault(_ =>
                _[CurrentGroupRowInfo.Column.FieldName]
                .Equals(CurrentGroupRowInfo.EditValue));
        int insertIndex;

        if (DropBelow)
        {
            receiver =
                dataTable
                .Rows
                .Cast<DataRow>()
                .LastOrDefault(_ =>
                    _[CurrentGroupRowInfo.Column.FieldName]
                    .Equals(CurrentGroupRowInfo.EditValue));

            insertIndex = dataTable.Rows.IndexOf(receiver) + 1;
        }
        else
        {
            receiver =
                dataTable
                .Rows
                .Cast<DataRow>()
                .FirstOrDefault(_ =>
                    _[CurrentGroupRowInfo.Column.FieldName]
                    .Equals(CurrentGroupRowInfo.EditValue));

            insertIndex = dataTable.Rows.IndexOf(receiver);
        }
        foreach (var dataRow in drags.Reverse())
        {
            dataRow.ItemArray = dict[dataRow];
            dataTable.Rows.InsertAt(dataRow, insertIndex);
        }
        try
        {
            var parentRowHandle = GetParentRowHandle(insertIndex);
            if (_isExpanded)
            {
                ExpandGroupRow(parentRowHandle);
            }
            FocusedRowHandle = parentRowHandle;
        }
        catch (Exception ex)
        {
            Debug.Assert(false, ex.Message);
        }
    }
}

Misc

GridViewEx takes a blanket approach to enabling custom sort only, and preliminary testing shows that drag drop in its present form works the same for the Keyword grouping as it does for Level.

/// <summary>
/// Disable automatic sorting. 
/// </summary>
protected virtual void OnDataSourceChanged(object sender, EventArgs e)
{
    foreach (GridColumn column in Columns)
    {
        column.SortMode = ColumnSortMode.Custom;
    }
    ExpandGroupLevel(1);
}

protected virtual void OnCustomColumnSort(object sender, CustomColumnSortEventArgs e)
{
    e.Handled = true;
}

/// <summary>
/// Disallow collapses during drag operations
/// </summary>
protected virtual void OnGroupRowCollapsing(object sender, RowAllowEventArgs e)
{
    e.Allow = GroupDragDropState.Equals(GroupDragDropState.None);
}
IVSoftware
  • 5,732
  • 2
  • 12
  • 23
  • 1
    Thanks, Why when I'm dragging the group row, orange lines remain on screen until drop the row? https://imgbox.com/rdzW58xa – Masoud Mar 27 '23 at 05:29
  • One moment, let me try and reproduce this. Is this just running the code from my repo? I'm pretty certain I can fix whatever is misbehaving. – IVSoftware Mar 27 '23 at 13:19
  • Also is your DevExpress version the same @ 22.1.6? – IVSoftware Mar 27 '23 at 13:28
  • OK try this. In the `DropBelow` property change there is a call that redraws (for economy's sake) a _single_ row: `RefreshRow(CurrentGroupRowInfo.RowHandle)` and the first thing I would try is invalidating the entire control instead: `GridControl.Refresh()`. Both versions are working fine on this end so the _general_ solution to this little bug is to make sure the rows are redrawing. A strategically placed `Debug.WriteLine` in the `OnCustomDrawGroupRow` method can tell you that. – IVSoftware Mar 27 '23 at 13:34
  • I should explain why "single row" worked in the first place: This particular `GridView` base control version fires `CustomDrawGroupRow` _two times_, one for the row the mouse is leaving and one for the row it's entering. If for some reason only one event is firing than you have to redraw _all_ the rows instead. – IVSoftware Mar 27 '23 at 13:44
  • Here's that [clone](https://github.com/IVSoftware/E764-with-level-drag-drop.git) link one more time for the full demo project with all the nuances attached. I have it in the body of the answer but it may be a little bit lost in all the other info there. – IVSoftware Mar 27 '23 at 13:59