Question ListViewItem Drag & Drop With Group Header

elroyskimms

Member
Joined
Jul 10, 2015
Messages
19
Programming Experience
10+
You'll find sample code to illustrate the problem below. Make a new Form in a VS 2013 WinForm Project, paste and run.

I have 2 ListView controls and I have Drag & Drop working between the controls. Items can be moved from one control to the other. Items can be rearranged within the control as well.

When I enable Groups within the ListView controls (change .ShowGroups = False to .ShowGroups = True), everything continues to work... mostly. When I Drag an Item and Drop it directly onto another Item, everything works as intended and the items are re-arranged properly. However, when I Drag an Item and Drop it on the Group Header of another Item, the Dragged item is being moved to the end of the List (as if it were dropped at the end of the list intentionally). If you do a ListView.HitTest you will see that there is no difference in the ListViewHitTestInfo if you Drop on a Group Header or if you Drop at the end of the list. I need to be able to differentiate between a Drop on a Group Header and a Drop at the end of the ListView.

When I drop on the Header, I want to treat it like a Drop on an Item, and rearrange the Items accordingly. I can't assume that the user will never drop an Item on the Header row. Is there any way to identify the Drop point as being a Group Header?

-E


Code:
VB.NET:
Public Class Form1
    'Drag/Drop code taken from [URL]https://www.daniweb.com/software-development/vbnet/threads/342742/drag-and-drop-with-two-listviews[/URL]-
    'Modified by me for Group support and reordering of ListViewItems

    Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        'Adds to listview items to the form
        Me.Size = New Size(400, 400)
        Me.Controls.Clear()
        Dim Listview1 As New ListView
        Dim Listview2 As New ListView
        Listview1.Location = New Point(50, 50)
        Listview1.Name = "ListView1"
        Listview2.Location = New Point(200, 50)
        Listview2.Name = "ListView2"
        Me.Controls.Add(Listview1)
        Me.Controls.Add(Listview2)
        For Each c As Control In Me.Controls
            If TypeOf c Is ListView Then
                With CType(c, ListView)
                    .AllowDrop = True
                    .Columns.Add("Name")
                    .Columns.Add("Type")
                    .FullRowSelect = True
                    .MultiSelect = False
                    .Size = New Size(150, 300)
                    .View = View.Details
                    .ShowGroups = False
                    AddHandler .ItemDrag, AddressOf ListView_ItemDrag
                    AddHandler .DragEnter, AddressOf ListView_DragEnter
                    AddHandler .DragDrop, AddressOf ListView_DragDrop
                    For i As Integer = 1 To 5
                        Dim lvItem As New ListViewItem(New String() {c.Name, i})
                        Dim grp As New ListViewGroup(c.Name & ":" & i, HorizontalAlignment.Center)
                        grp.Items.Add(lvItem)
                        .Items.Add(lvItem)
                        .Groups.Add(grp)
                    Next
                End With
            End If
        Next
    End Sub

    Private Sub ListView_ItemDrag(ByVal sender As Object, ByVal e As System.Windows.Forms.ItemDragEventArgs)
        If sender Is Nothing OrElse Not TypeOf sender Is ListView Then Exit Sub
        With CType(sender, ListView)
            .DoDragDrop(e.Item, DragDropEffects.Move)
        End With
    End Sub

    Private Sub ListView_DragEnter(ByVal sender As Object, ByVal e As System.Windows.Forms.DragEventArgs)
        If sender Is Nothing OrElse Not TypeOf sender Is ListView Then Exit Sub
        'If this is a listview item then allow the drag
        If e.Data.GetDataPresent(GetType(ListViewItem)) Then
            e.Effect = DragDropEffects.Move
        End If
    End Sub

    Private Sub ListView_DragDrop(ByVal sender As Object, ByVal e As System.Windows.Forms.DragEventArgs)
        If sender Is Nothing OrElse Not TypeOf sender Is ListView Then Exit Sub
        'Remove the item from the current listview and drop it in the new listview
        With CType(sender, ListView)
            If e.Data.GetDataPresent(GetType(ListViewItem)) Then
                Dim draggedItem As ListViewItem = CType(e.Data.GetData(GetType(ListViewItem)), ListViewItem)
                'If this is the last Item in the group, remove the group from this list view or else you leave behind empty groups
                If draggedItem.Group.Items.Count = 1 Then .Groups.Remove(draggedItem.Group)
                draggedItem.ListView.Items.Remove(draggedItem)
                Dim DropPoint As Point = .PointToClient(New Point(e.X, e.Y))
                Dim ExistingItem As ListViewItem = .GetItemAt(DropPoint.X, DropPoint.Y)
                Dim itemIndex As Integer = .Items.IndexOf(ExistingItem)
                Dim groupIndex As Integer = -1
                If Not ExistingItem Is Nothing Then groupIndex = .Groups.IndexOf(ExistingItem.Group)
                'If there was no item in that spot, add this to the end of the list
                If itemIndex < 0 Then
                    itemIndex = .Items.Count
                End If
                If groupIndex < 0 Then
                    groupIndex = .Groups.Count
                End If
                Dim grp As New ListViewGroup(draggedItem.Text & ":" & draggedItem.SubItems(1).Text, HorizontalAlignment.Center)
                grp.Items.Add(draggedItem)
                If groupIndex >= .Groups.Count Then
                    .Groups.Add(draggedItem.Group)
                Else
                    .Groups.Insert(groupIndex + 1, draggedItem.Group)
                End If
                .Items.Insert(itemIndex, draggedItem)
            End If
        End With
    End Sub

End Class
 
After thinking about this over the weekend, I decided to use the Bounds (Top/Bottom) of each item in the list to determine if the Drag/Drop was dropping between items (on a header group) or at the end of the list. I also corrected an error in the Drop code. In the old version, when rearranging items within the same ListView, the Dragged item was being removed before the ExistingItem was being identified. When the Dragged item was removed, the list of items changed and what used to be in the DropPoint was now moved up in the list. I corrected it below, along with the solution to dropping on group headers.

While the solution using Bounds works, I was hoping for something better. On the .Net side of things, when working with a DataGrid, Header items have their own ItemType: https://msdn.microsoft.com/en-us/library/system.web.ui.webcontrols.listitemtype(v=vs.110).aspx. When you iterate through the list of items, the header rows are returned with the items and their ItemType is easily identified. I had assumed the WinForms side had a similar ItemType, but I was wrong. It treats the headers as nothing, which makes no sense at all. I'm sure that there is probably a way to do this through Reflection, but I was hoping that Microsoft would have exposed the header items without forcing us to tear apart their code.

-E

VB.NET:
    Private Sub ListView_DragDrop(ByVal sender As Object, ByVal e As System.Windows.Forms.DragEventArgs)
        If sender Is Nothing OrElse Not TypeOf sender Is ListView Then Exit Sub
        'Remove the item from the current listview and drop it in the new listview
        With CType(sender, ListView)
            If e.Data.GetDataPresent(GetType(ListViewItem)) Then
                Dim draggedItem As ListViewItem = CType(e.Data.GetData(GetType(ListViewItem)), ListViewItem)
                Dim DropPoint As Point = .PointToClient(New Point(e.X, e.Y))
                Dim ExistingItem As ListViewItem = .GetItemAt(DropPoint.X, DropPoint.Y)
                If ExistingItem Is Nothing Then
                    Select Case DropPoint.Y
                        Case Is > .Items(.Items.Count - 1).Bounds.Bottom
                            'Determine if the DropPoint is BELOW the last item
                            ExistingItem = Nothing
                        Case Else
                            'The DropPoint is BETWEEN 2 items, cycle through the items find out where
                            For i As Integer = 0 To .Items.Count - 2
                                'We can skip the last item, we already tested it
                                If .Items(i).Bounds.Top > DropPoint.Y Then
                                    'We've found the item that is directly below the header in the DropPoint
                                    ExistingItem = .Items(i)
                                    Exit For
                                End If
                            Next
                    End Select
                End If
                'Perform ALL removals AFTER finding the DropPoint and ExistingItem, because the item list
                'changes and everything can be shifted around
                'If this is the last Item in the group, remove the group from this list view or else you leave behind empty groups
                If draggedItem.Group.Items.Count = 1 Then .Groups.Remove(draggedItem.Group)
                draggedItem.ListView.Items.Remove(draggedItem)
                Dim itemIndex As Integer = .Items.IndexOf(ExistingItem)
                Dim groupIndex As Integer = -1
                If Not ExistingItem Is Nothing Then groupIndex = .Groups.IndexOf(ExistingItem.Group)
                'If there was no item in that spot, add this to the end of the list
                If itemIndex < 0 Then
                    itemIndex = .Items.Count
                End If
                If groupIndex < 0 Then
                    groupIndex = .Groups.Count
                End If
                Dim grp As New ListViewGroup(draggedItem.Text, HorizontalAlignment.Center)
                grp.Items.Add(draggedItem)
                If groupIndex >= .Groups.Count Then
                    .Groups.Add(draggedItem.Group)
                Else
                    .Groups.Insert(groupIndex + 1, draggedItem.Group)
                End If
                .Items.Insert(itemIndex, draggedItem)
            End If
        End With
    End Sub
 
Last edited:
Back
Top