Search code examples
listviewwindows-store-appswin-universal-app

Windows Store App drag and drop between ListViews


I am building a Windows Store App / Universal App targeting Windows 8.1 and Windows 10 and I would like to be able to drag and drop items between ListViews and be able to position the item in a specific spot in the ListView. The main problem I'm having is that I can't find a good way to determine the list index of where the item was dropped.

I found a sample (XAML ListView reorder) but an important difference is that the items in my list have variable heights so the simple calculation this sample project uses to infer the index won't work for me.

I am able to get the x,y position of where within the ListView the item was dropped, but I'm having trouble using that position to figure out the index. I've found mentions of people using ListView.GetItemAt(x, y) or ListView.HitTest(x, y) but as other have found, those methods don't seem to exist in Windows Universal apps. I've also tried using VisualTreeHelper.FindElementsInHostCoordinates() but I'm either not using it properly or I'm not understanding its purpose because I can't get it to return results.

Here is some example code that I've tried:

private void ListView_OnDrop(object sender, DragEventArgs e)
{
    var targetListView = (ListView)sender;

    var positionRelativeToTarget = e.GetPosition(targetListView);

    var rect = new Rect(positionRelativeToTarget, new Size(10, 15));
    var elements = VisualTreeHelper.FindElementsInHostCoordinates(rect, targetListView);

    // Trying to get the index in the list where the item was dropped
    // 'elements' is always empty
}

For reference, I'm using C#, XAML, and Visual Studio 2013.

Thanks!


Solution

  • I found a solution that is good enough for my purposes. Essentially what I ended up doing was handling the drop event on both the ListView and the list item because it's easy to figure out the index if the drop happens on a list item. I still had to handle the drop on the ListView too though for when an item is dropped in between.

    Here is the code I ended up with:

    MyView.xaml

    <UserControl.Resources>    
        <DataTemplate x:Key="MyItemTemplate">
            <local:MyControl AllowDrop="True" Drop="ListItem_OnDrop" />
        </DataTemplate>
    </UserControl.Resources>
    
    <Grid>
        <ListView ItemTemplate="{StaticResource MyItemTemplate}"
                  CanDragItems="True" AllowDrop="True"
                  DragItemsStarting="ListView_OnDragItemsStarting"
                  DragOver="ListView_OnDragOver"
                  DragLeave="ListView_OnDragLeave"
                  Drop="ListView_OnDrop" />
    </Grid>
    

    MyView.xaml.cs

    public sealed partial class MyView
    {
        private readonly SolidColorBrush listViewDragOverBackgroundBrush = new SolidColorBrush(Color.FromArgb(255, 247, 247, 247));
    
        public MyView()
        {
            InitializeComponent();
        }
    
        private IMyViewModel ViewModel
        {
            get { return DataContext as IMyViewModel; }
        }
    
        private void ListView_OnDragItemsStarting(object sender, DragItemsStartingEventArgs e)
        {
            e.Data.Properties.Add("dataItem", e.Items[0] as IMyItemViewModel);
        }
    
        private void ListView_OnDragOver(object sender, DragEventArgs e)
        {
            var dropTarget = sender as ListView;
            if (dropTarget == null)
            {
                return;
            }
    
            dropTarget.Background = listViewDragOverBackgroundBrush;
        }
    
        private void ListView_OnDragLeave(object sender, DragEventArgs e)
        {
            var dropTarget = sender as ListView;
            if (dropTarget == null)
            {
                return;
            }
    
            dropTarget.Background = null;
        }
    
        private void ListView_OnDrop(object sender, DragEventArgs e)
        {
            var draggedItem = e.Data.Properties["dataItem"] as IMyItemViewModel;
            var targetListView = sender as ListView;
    
            if (targetListView == null || draggedItem == null)
            {
                return;
            }
    
            targetListView.Background = null;
    
            var droppedPosition = e.GetPosition(targetListView);
            var itemsSource = targetListView.ItemsSource as IList;
            const double extraHeightThatImNotSureWhereItCameFrom = 8d;
            var highWaterMark = 3d;  // This list starts with 3px of padding
            var dropIndex = 0;
            var foundDropLocation = false;
    
            for (int i = 0; i < itemsSource.Count && !foundDropLocation; i++)
            {
                var itemContainer = (ListViewItem)targetListView.ContainerFromIndex(i);
    
                highWaterMark = highWaterMark + itemContainer.ActualHeight - extraHeightThatImNotSureWhereItCameFrom;
    
                if (droppedPosition.Y <= highWaterMark)
                {
                    dropIndex = i;
                    foundDropLocation = true;
                }
            }
    
            if (foundDropLocation)
            {
                // Handle the drag/drop at a specific location
                // DropPosition is an enumeration I made that has Before & After
                ViewModel.CompleteDragDrop(draggedItem, DropPosition.Before, dropIndex);
            }
            else
            {
                // Add to the end of the list. Also works for an empty list.
                ViewModel.CompleteEvidenceDragDrop(draggedItem, DropPosition.After, itemsSource.Count - 1);
            }
        }
    
        private void ListItem_OnDrop(object sender, DragEventArgs e)
        {
            e.Handled = true;
    
            var draggedItem = e.Data.Properties["dataItem"] as IMyItemViewModel;
            var dropTarget = sender as MyControl;
    
            if (dropTarget == null || draggedItem == null)
            {
                return;
            }
    
            var parentList = dropTarget.Closest<ListView>();
            var dropPosition = dropTarget.GetDropPosition(e);
    
            parentList.Background = null;
    
            ViewModel.CompleteDragDrop(draggedItem, dropPosition, dropTarget.DataContext as IMyItemViewModel);
        }
    }
    

    ExtensionMethods

    public static class ExtensionMethods
    {
        public static T Closest<T>(this DependencyObject obj) where T : DependencyObject
        {
            if (obj == null)
            {
                return null;
            }
    
            while (true)
            {
                var parent = VisualTreeHelper.GetParent(obj);
    
                if (parent == null)
                {
                    return null;
                }
    
                if (parent.GetType() == typeof(T))
                {
                    return (T)parent;
                }
    
                obj = parent;
            }
        }
    
        public static DropPosition GetDropPosition(this FrameworkElement dropTarget, DragEventArgs e)
        {
            var positionRelativeToTarget = e.GetPosition(dropTarget);
    
            var dropBefore = positionRelativeToTarget.Y < (dropTarget.ActualHeight / 2);
    
            return dropBefore ? DropPosition.Before : DropPosition.After;
        }
    }