Reorderable and Draggable Items in GridView and ListView – Boredom Challenge Day 24

Standard

A cool feature of the GridView and ListView controls is that by setting a few properties, we can allow the user to reorder the items on the GridView by dragging them around, or let the user drag an item from the GridView and drop it on other controls to perform an action. For example, we could let our user rearrange the items in their to-do list, or maybe create a kid’s game where they drag and drop items on correct colors.

An example reorderable to-do list.

An example to-do list while reordering items.

The controls that support reordering and dragging out of the box also come with their own animations, so we when we select an item in a GridView and drag it around, other items will move out of the way, and if we drag the item away, other items will go back to their original positions, which look quite nice and require no effort whatsoever from us.

The animation in GridView while dragging an item.

The animation in GridView while dragging an item.

In this article, we’ll see how we can add this functionality by creating a matching game, where the purpose will be to match the item with its picture.

Naturally, we’ll start with a blank Windows Store app in Visual Studio.

3

There are three properties that we will need to use: CanDragItems, CanReorderItems and AllowDrop. They may seem self-explanining, but there are some small issues we have to be aware of.

CanReorderItems property allows the user to select an item, drag it around and put it in another place. However, this property alone will not work if we also do not set AllowDrop to true, because if AllowDrop is set to false, well, a drop operation is not allowed on the element and we have to drop the item in order to reorder.

CanDragItems property, however, differs from CanReorderItems as it allows the items to be dragged and dropped on to other elements as a data payload. This way we can get the dragged and dropped item in the Drop event of the target element and do operations on it.

So, if you wish to just drag your items so you can reorder them, setting CanReorderItems and AllowDrop is enough. If you wish to drag the items but drop them on other controls, then CanDragItems is needed. If you wish them both, then set all of them to true. 🙂

Then let’s create our GridView like the following in MainPage.xaml:

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <GridView x:Name="GridViewMain" HorizontalAlignment="Left" Margin="100,100,0,50" Width="300" CanDragItems="True" CanReorderItems="True" AllowDrop="True">
            <GridView.Resources>
                <DataTemplate x:Key="DataTemplateGridViewMain">
                    <Grid Width="150" Height="100" Background="#FF3777B4">
                        <TextBlock Text="{Binding}" TextAlignment="Center" VerticalAlignment="Center" FontSize="20" FontFamily="Kristen ITC" TextWrapping="Wrap" />
                    </Grid>
                </DataTemplate>
            </GridView.Resources>
            <GridView.ItemTemplate>
                <StaticResource ResourceKey="DataTemplateGridViewMain"/>
            </GridView.ItemTemplate>
        </GridView>
    </Grid>

To make reordering feature work, the last thing we need to do is; either set its ItemsSource to an ObservableCollection, or just add the items to its Items collection. If you set the ItemsSource to a List, it won’t work. Let’s try adding some data to it in MainPage.xaml.cs and see how it works now.

using System.Collections.ObjectModel;
        ObservableCollection<string> itemsList = new ObservableCollection<string>();

        public MainPage()
        {
            this.InitializeComponent();
            this.Loaded += MainPage_Loaded;
        }

        void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            itemsList.Add("Banana");
            itemsList.Add("House");
            itemsList.Add("Nicolas Cage");
            itemsList.Add("Pencil");
            itemsList.Add("Cloud");
            itemsList.Add("Robot");
            itemsList.Add("Television");

            GridViewMain.ItemsSource = itemsList;
        }

Now run it and try reordering items around.

4

If you wish to reach the current order of the items, you can always use the ObservableCollection you set, because it will be synchronized with the order of the items in GridView. You can also change the order in the ObservableCollection yourself and it will be immediately reflected to the GridView.

So, now that reordering works, we’ll implement the dragging part. However, for the second list (the list of images), we can not use a control like GridView or ListView because it won’t work. When we drag and drop an item on a control, that control’s Drop event is fired, not the item that was contained within, so even if we put a UserControl in the ItemTemplate, we can not determine the drop target.

The solution to that would be adding the drop target items in the codebehind to a container of our choice. However, this is not going to be as flexible and dynamic as a GridView, so you need to handle the size of this container yourself in your own design.

We’ll be creating a UserControl to be used as a drop target. It will consist of an Image control and a Border (to highlight the object when the dragged item is hovering over it), and we’ll get two strings in its constructor: One of them will be used for comparing with the dropped item to see if they are the same, and the other will be the link to the image we’ll use.

5

In DropTargetUserControl.xaml (notice that we set AllowDrop to true and add a handler for the related drag and drop events):

<UserControl x:Name="DropUserControl"
    x:Class="ReorderDragApp.DropTargetUserControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:ReorderDragApp"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="125"
    d:DesignWidth="125" AllowDrop="True" Drop="DropUserControl_Drop" DragEnter="DropUserControl_DragEnter" DragLeave="DropUserControl_DragLeave">
    
    <Grid>
        <Image x:Name="ImageItem" Stretch="UniformToFill" Width="125" Height="125"/>
        <Border x:Name="BorderHighlight" BorderBrush="#FF0083FF" BorderThickness="5" Width="125" Height="125" Visibility="Collapsed"/>
    </Grid>
</UserControl>

In DropTargetUserControl.xaml.cs:

using Windows.UI.Popups;
using Windows.UI.Xaml.Media.Imaging;
    public sealed partial class DropTargetUserControl : UserControl
    {
        string correctAnswer;

        public DropTargetUserControl(string itemName, string Image)
        {
            this.InitializeComponent();
            correctAnswer = itemName;
            ImageItem.Source = new BitmapImage(new Uri(Image));
        }

        async private void DropUserControl_Drop(object sender, DragEventArgs e)
        {
            BorderHighlight.Visibility = Visibility.Collapsed;

            string data = await e.Data.GetView().GetTextAsync();

            if (data == correctAnswer)
            {
                MessageDialog messageDialog = new MessageDialog("Correct!");
                await messageDialog.ShowAsync();
            }
            else
            {
                MessageDialog messageDialog = new MessageDialog("Wrong!");
                await messageDialog.ShowAsync();
            }
        }

        private void DropUserControl_DragEnter(object sender, DragEventArgs e)
        {
            BorderHighlight.Visibility = Visibility.Visible;
        }

        private void DropUserControl_DragLeave(object sender, DragEventArgs e)
        {
            BorderHighlight.Visibility = Visibility.Collapsed;
        }
    }

Our DropTargetUserControl is ready. The only thing left to do now is to create a container for the drop targets (I’ll just use a StackPanel).

One small thing is that, in order to get the data on the Drop event of the user control, we need to set that data everytime a drag operation started in GridView. We do that by adding an event handler for the DragItemsStarting event of the GridView. The final code in MainPage.xaml and MainPage.xaml.cs will look like this:

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <GridView x:Name="GridViewMain" HorizontalAlignment="Left" Margin="100,100,0,50" Width="300" CanDragItems="True" CanReorderItems="True" AllowDrop="True" DragItemsStarting="GridViewMain_DragItemsStarting">
            <GridView.Resources>
                <DataTemplate x:Key="DataTemplateGridViewMain">
                    <Grid Width="150" Height="100" Background="#FF3777B4">
                        <TextBlock Text="{Binding}" TextAlignment="Center" VerticalAlignment="Center" FontSize="20" FontFamily="Kristen ITC" TextWrapping="Wrap" />
                    </Grid>
                </DataTemplate>
            </GridView.Resources>
            <GridView.ItemTemplate>
                <StaticResource ResourceKey="DataTemplateGridViewMain"/>
            </GridView.ItemTemplate>
        </GridView>
        <StackPanel x:Name="StackPanelDropTarget" HorizontalAlignment="Left" Margin="500,100,0,50" Width="300" AllowDrop="True"/>
    </Grid>
        ObservableCollection<string> itemsList = new ObservableCollection<string>();

        public MainPage()
        {
            this.InitializeComponent();
            this.Loaded += MainPage_Loaded;
        }

        void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            itemsList.Add("Banana");
            itemsList.Add("House");
            itemsList.Add("Nicolas Cage");
            itemsList.Add("Pencil");
            itemsList.Add("Cloud");
            itemsList.Add("Robot");
            itemsList.Add("Television");

            GridViewMain.ItemsSource = itemsList;

            AddDropTargets();
        }

        private void AddDropTargets()
        {
            List<DropTargetUserControl> dropTargetList = new List<DropTargetUserControl>();

            dropTargetList.Add(new DropTargetUserControl("Banana", "http://images3.wikia.nocookie.net/__cb20130527193206/dragonball/images/3/32/Banana-1-.jpg") { Margin = new Thickness(0, 10, 0, 0) });
            dropTargetList.Add(new DropTargetUserControl("House", "http://media-cdn.tripadvisor.com/media/photo-s/02/be/12/ac/graingers-guest-house.jpg") { Margin = new Thickness(0, 10, 0, 0) });
            dropTargetList.Add(new DropTargetUserControl("Nicolas Cage", "http://www.dreadcentral.com/img/news/jun11/niccage.jpg") { Margin = new Thickness(0, 10, 0, 0) });
            dropTargetList.Add(new DropTargetUserControl("Pencil", "http://www.thegeminiweb.com/babyboomer/wp-content/uploads/2007/08/pencil.jpg") { Margin = new Thickness(0, 10, 0, 0) });
            dropTargetList.Add(new DropTargetUserControl("Cloud", "http://www.spraytec.com/images/cloud-seeding.jpg") { Margin = new Thickness(0, 10, 0, 0) });
            dropTargetList.Add(new DropTargetUserControl("Robot", "http://www.pedsforparents.com/images/3761.jpg") { Margin = new Thickness(0, 10, 0, 0) });
            dropTargetList.Add(new DropTargetUserControl("Television", "http://2.bp.blogspot.com/-5lIqJzVOlmU/UPpbsAbP86I/AAAAAAAAAMk/etVEFb6pcDM/s1600/television-sony-en-casa-de-mis-padres.jpg") { Margin = new Thickness(0, 10, 0, 0) });

            var shuffledList = dropTargetList.OrderBy(e => Guid.NewGuid());

            foreach (DropTargetUserControl dropTarget in shuffledList)
            {
                StackPanelDropTarget.Children.Add(dropTarget);
            }
        }

        private void GridViewMain_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
        {
            e.Data.SetText(e.Items[0].ToString());
        }

The app is complete. Now if we run the app, we can drag an item from the left and drop it on the items on the right, and we’ll get a message dialog that tells us if our selection is correct. The drop target will also be highlighted while we are hovering over it.

6

That’s it. 🙂

I wish to remind you again that you have to be careful with the sizes of the drop targets and its container. For example, since I didn’t do anything about it, if you try to run the app in a lower resolution than 1900×1200, the drop targets will not fit into the screen. If anyone reading this has a solution to this problem which allows us to put them dynamically without worrying about the resolution (apart from checking the screen size at the start of the app and arranging the drop targets based on it, which would be way too much work), then I’ll be glad to put that solution here. 🙂

Here‘s the source code for the example app.

Hope it’s been useful, and thank you for reading.

Advertisement

One thought on “Reorderable and Draggable Items in GridView and ListView – Boredom Challenge Day 24

Comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s