Search code examples
c#windows-runtimewinrt-xaml

winRT drag and drop, swap two items instead of inserting


I'm a long time WPF user but new to WinRT. I'm wondering if there's a built in way or easy way to integrate swapping functionality in containers so that a swap exchanges two items in the container. The behavior desired is drag an item and drop it on another item and have both the dragged item and the item it's dragged onto get their positions in the container swapped).

Example I have a list with 1 2 3 4 5 6 7 8, if I drag 7 "on" 4 I want the two items swapped so that the resulting list becomes 1 2 3 7 5 6 4 8

I'm currently using a GridView with an ItemsWrapGrid as it's container to display a lot of picture thumbnails. I need to be able to reorder them with the most commonly required action being a swap in the positions of two images.

Or if there's no built in way, can you hint me at what the "proper" direction to start doing it from scratch would be in WinRT? I'm thinking handle the drag and drop not at the container but at the item level, and manually swap the items in the ObservableCollection?


Solution

  • Both existing answers will do the swapping for you, at the data level. Here's what can be done to make the UI more user-friendly.

    IMHO, the best UX to handle the swapping is, when you drag an item and move it over another, the latter should appear to where the dragged item was originally at. This clearly tells the user where exactly the items will go. Just like what's shown on the gif image below.

    drag, drop & swap

    To do this you will need to create an Image and use RenderTargetBitmap to copy the look of the drop item to its source, when the drag item moves over the drop item. Of course when the drag action starts, you need to get the position of the drag item so you know where exactly to put this image.

    Then, once the item is dropped, you should clear and hide the image and do the data exchange.

    private void GridView_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
    {
        // get item container, i.e. GridViewItem
        var itemContainer = (GridViewItem)this.MyGridView.ContainerFromItem(e.Items[0]);
    
        // get drag item index from its item container
        _dragItemIndex = this.MyGridView.IndexFromContainer(itemContainer);
    
        // get drag item position
        var position = itemContainer.GetRelativePosition(this.LayoutRoot);
    
        // set the width and height of the image
        this.DropItemImage.Width = itemContainer.ActualWidth;
        this.DropItemImage.Height = itemContainer.ActualHeight;
    
        // move the image to this location
        this.DropItemImage.RenderTransformOrigin = new Point(0, 0);
        this.DropItemImage.RenderTransform.Animate(null, position.X, "TranslateX", 0, 0);
        this.DropItemImage.RenderTransform.Animate(null, position.Y, "TranslateY", 0, 0);
    }
    
    private void GridView_Drop(object sender, DragEventArgs e)
    {
        // first we need to reset the image
        this.DropItemImage.Source = null;
    
        // get the drop & drop items
        var dragItem = _groups[_dragItemIndex];
        var dropItem = _groups[_dropItemIndex];
    
        // then we swap their positions
        _groups.RemoveAt(_dragItemIndex);
        _groups.Insert(_dragItemIndex, dropItem);
        _groups.RemoveAt(_dropItemIndex);
        _groups.Insert(_dropItemIndex, dragItem);
    }
    
    private object _previous;
    private async void ItemRoot_DragOver(object sender, DragEventArgs e)
    {
        // first we get the DataContext from the drop item in order to retrieve its container
        var vm = ((Grid)sender).DataContext;
    
        // get the item container
        var itemContainer = (GridViewItem)this.MyGridView.ContainerFromItem(vm);
    
        // this is just to stop the following code to be called multiple times druing a DragOver
        if (_previous != null && _previous == itemContainer)
        {
            return;
        }
        _previous = itemContainer;
    
        // get drop item index from its item container
        _dropItemIndex = this.MyGridView.IndexFromContainer(itemContainer);
    
        // copy the look of the drop item to an image
        var bitmap = new RenderTargetBitmap();
        await bitmap.RenderAsync(itemContainer);
        this.DropItemImage.Source = bitmap;
    
        // animate the image to make its appearing more interesting
        this.DropItemImage.Animate(0, 0.4, "Opacity", 200, 0);
        this.DropItemImage.RenderTransformOrigin = new Point(0.5, 0.5);
        this.DropItemImage.RenderTransform.Animate(0.8, 1, "ScaleX", 200, 0, new ExponentialEase { EasingMode = EasingMode.EaseIn });
        this.DropItemImage.RenderTransform.Animate(0.8, 1, "ScaleY", 200, 0, new ExponentialEase { EasingMode = EasingMode.EaseIn });
    }
    

    I have included a small sample project here just so you can check out how the animations are done. Please note that the data swapping part is not included, as I said, the other answers already explain it very well. :)