WinRT Drag and Drop

Turning on Drag and Drop on a GridView or ListView in WinRT is pretty straightforward, and in addition there’s built-in styling so that when you drag an item over a position in the list, the surrounding items push out of the way – the effect looks pretty cool.

Dragging items from one list onto another one is not as straightforward it seems. There are some good examples of dragging from one list and dropping onto another, but I couldn’t see one where the animated effect would also show.

In the example below I’ve created two simple ListViews side by side, bound to a view model.

dragdrop1

<Page
    x:Class="DragAndDropTest.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    xmlns:local="using:DragAndDropTest"
    DataContext="{Binding ViewModel, Source={StaticResource Locator}}">
    
    <Page.Resources>
        <DataTemplate x:Key="item1Template">
            <Border Background="Gray" Margin="5">
                <Grid Width="200" Height="40">
                    <TextBlock Text="{Binding}" Style="{StaticResource BodyTextStyle}"
                               HorizontalAlignment="Center" VerticalAlignment="Center"/>
                </Grid>
            </Border>
        </DataTemplate>

        <DataTemplate x:Key="item2Template">
            <Border Background="Orange" Margin="5">
                <Grid Width="200" Height="40">
                    <TextBlock Text="{Binding}" Style="{StaticResource BodyTextStyle}"
                               HorizontalAlignment="Center" VerticalAlignment="Center"/>
                </Grid>
            </Border>
        </DataTemplate>
    </Page.Resources>
    
    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Grid Width="500">
            <Grid.ColumnDefinitions>
                <ColumnDefinition />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>

            <local:DragDropListView 
                x:Name="theList1"
                Grid.Column="0"
                Width="300" Height="700"
                ItemsSource="{Binding Data1, Mode=TwoWay}" 
                ItemTemplate="{StaticResource item1Template}"
                Margin="0,10,0,0" 
                CanDragItems="True" 
                CanReorderItems="True"
                AllowDrop="True"
                Drop="OnDropList1"
                DragItemsStarting="theList1_DragItemsStarting" />

            <local:DragDropListView
                x:Name="theList2"
                Grid.Column="1"
                Width="300" Height="700"
                ItemsSource="{Binding Data2, Mode=TwoWay}"
                ItemTemplate="{StaticResource item2Template}"
                Margin="0,10,0,0"
                CanDragItems="True"
                CanReorderItems="True"
                AllowDrop="True"
                Drop="OnDropList2"
                DragItemsStarting="theList2_DragItemsStarting" />
        </Grid>
    </Grid>
</Page>

To achieve the animated effect we have to set the individual list item’s style with the Visual State manager to specific states, namely BottomReorderHint, TopReorderHint, and NoReorderHint. By subclassing a ListView and maintaining a boolean value, _hasHintedState, it can then be used to determine if the visual state of an item should be set. There are several Hinted states..

public class DragDropListView : ListView
{
    private int _dropIndex;
    private bool _hasHintedState;

    protected override void OnDragEnter(DragEventArgs e)
    {
        base.OnDragEnter(e);

        DropAt = 0;
        _hasHintedState = false;
    }

    protected override void OnDragOver(DragEventArgs e)
    {
        base.OnDragOver(e);

        var position = e.GetPosition(null);

        //Get the list of items under the current position
        var hitListViewItems =
            VisualTreeHelper.FindElementsInHostCoordinates(position, this)
                .OfType()
                .ToArray();

        if (!hitListViewItems.Any())
        {
            if (!_hasHintedState)
            {
                //We're over the list but not any particular items so add to the end
                DropAt = Items.Count;
            }

            return;
        }

        //Get all of the list items
        var itemContainers = Items.Select(x => ItemContainerGenerator.ContainerFromItem(x))
                .OfType()
                .ToArray();

        var dropItem = itemContainers.FirstOrDefault(hitListViewItems.Contains);
        if (dropItem == null)
        {
            //Don't change anything - we're not hitting any list item because one or more are already
            //in a hinted state
            return;
        }

        _dropIndex = ItemContainerGenerator.IndexFromContainer(dropItem);
        DropAt = _dropIndex;
            
        _hasHintedState = true;

        //Updated - we need to check for an offset if the list has been scrolled
        var offset = GetIndexOfFirstSelector(listViewBase);

        //Update the hinted state for all the items
        for (var i=0; i &lt; itemContainers.Count(); i++)
        {
            string hint = "NoReorderHint";
                
            if (i = _dropIndex)
                hint = "BottomReorderHint";

            VisualStateManager.GoToState(itemContainers[i], hint, true);
        }

    }

    protected override void OnDragLeave(DragEventArgs e)
    {
        base.OnDragLeave(e);

        //When leaving the Drag operation, remove any order hint states
        foreach (var item in Items.Select(x =>; ItemContainerGenerator.ContainerFromItem(x)).OfType())
                VisualStateManager.GoToState(item, "NoReorderHint", true);
    }

    private int GetIndexOfFirstSelector(ListViewBase listViewBase)
    {
        //Get the index of the first Selector item as this will provide an offset for setting the hinted state
        var offset = 0;

        var items = listViewBase.Items.Where(x => x != null).Select(x => listViewBase.ItemContainerGenerator.ContainerFromItem(x))
                                .ToArray();

        for (var i = 0; i < items.Count(); i++)
        {
            if (!(items[i] is SelectorItem))
                continue;

            offset = i;
            break;
        }

        return offset;
    }
    
    /// <summary>
    /// A property indicating the position in the list where the item(s) should be dropped
    /// </summary>
    public int DropAt { get; set; }
              
}

dragdrop2

dragdrop3

Remembering to clear all the hinted states when no longer dragging over the list, and it gives the desired appearance.

4 thoughts on “WinRT Drag and Drop

  1. Hi,
    great article!
    I found some problems when dropping to an horizontal ListView with a scrollbar (there are more items than the list can display): when dragging the element at the end of the list, scrolling the list, I’m not able to drop it at the end of the list.

    It seems that I can only drop before the selected item and not after. In a standard ListView this is possible placing the dragged element above or under the item I want to drop to.

    If I have two ListView with a different data template, is it possible to change the dragged item template when this is over the ListView I want to drop to?

  2. You’re right – well spotted. The reason that it doesn’t drop when the list is scrolled is that the list of items, itemContainers, in OnDragOver, is not going to be a list of SelectorItems that start at 0. If the list has been scrolled then we need to determine an offset value before setting the hinted state of items. I’ve updated the example code to take account of this.

    • Hi,
      in the code you’ve updated I found some problems:

      //Updated – we need to check for an offset if the list has been scrolled
      var offset = GetIndexOfFirstSelector(listViewBase);

      //Update the hinted state for all the items
      for (var i=0; i < itemContainers.Count(); i++)
      {
      string hint = “NoReorderHint”;

      if (i = _dropIndex)
      hint = “BottomReorderHint”;

      VisualStateManager.GoToState(itemContainers[i], hint, true);
      }

      1) listViewBase is not defined.
      2) Syntax error in if (i = _dropIndex)
      3) You define offset but it’s not used.

Leave a comment