Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reordering grouped rows in DevExpress GridControl using drag & drop

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

like image 430
Masoud Avatar asked Sep 19 '25 08:09

Masoud


1 Answers

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);
}
like image 107
IVSoftware Avatar answered Sep 22 '25 05:09

IVSoftware