Search code examples
xamlgridviewwindows-runtimewindows-store-appswinrt-xaml

GridView: Get Index on Drop Event


How do I get the index or position where a GridViewItem is being dropped inside the OnDrop event of the GridView? As I have read around that it is possible with GridView.ItemContainerGenerator.ContainerFromItem(item) but for me ItemContainerGenerator is null.

This is my current code:

void gridMain_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
{
    var item = e.Items.First();
    var source = sender;
    e.Data.Properties.Add("item", item);
    e.Data.Properties.Add("source", sender);
}

void gridMain_Drop(object sender, DragEventArgs e)
{
    var item = e.Data.Properties.Where(p => p.Key == "item").Single();

    object source;
    e.Data.Properties.TryGetValue("source", out source);
    var s = ((GridView)source).ItemContainerGenerator.ContainerFromItem(item);
}

Any hint or suggestion will be really helpful.


Solution

  • Use GetPosition method of DragEventArgs to find the position where item was dropped and then calculate the actual index, see code snippet below for the handler. Similar question was asked here using this MSDN example as an answer (Scenario 3).

    private void GridView_Drop(object sender, DragEventArgs e)
    {
        GridView view = sender as GridView;
    
        // Get your data
        var item = e.Data.Properties.Where(p => p.Key == "item").Single();
    
        //Find the position where item will be dropped in the gridview
        Point pos = e.GetPosition(view.ItemsPanelRoot);
    
        //Get the size of one of the list items
        GridViewItem gvi = (GridViewItem)view.ContainerFromIndex(0);
        double itemHeight = gvi.ActualHeight + gvi.Margin.Top + gvi.Margin.Bottom;
    
        //Determine the index of the item from the item position (assumed all items are the same size)
        int index = Math.Min(view.Items.Count - 1, (int)(pos.Y / itemHeight));
    
        // Call your viewmodel with the index and your data.
    }
    

    EDIT: Please, consider this as just a prototype. I tried it and it has worked properly, but you may revise it according to your scenario (tweak delay timeout, differentiate more TaskCompletionSource at once, etc.).

    The idea is to start a task after Remove action to check whether the item was only removed, or reordered.

    private async void observableCollection_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
        {
            object removedItem = e.OldItems[0];
            var reorderTask = NoticeReorderAsync(removedItem);
            try
            {
                var task = await Task.WhenAny(reorderTask, Task.Delay(100));
    
                if (reorderTask == task)
                {
                    // removedItem was in fact reordered
                    Debug.WriteLine("reordered");
                }
                else
                {
                    TryCancelReorder();
                    // removedItem was really removed
                    Debug.WriteLine("removedItem");
                }
            }
            catch (TaskCanceledException ex)
            {
                Debug.WriteLine("removedItem (from exception)");
            }
            finally
            {
                tcs = null;
            }
        }
        else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
        {
            object addedItem = e.NewItems[0];
            bool added = NoticeAdd(addedItem);
            if (added)
            {
                // addedItem was just added, not reordered
                Debug.WriteLine("added");
            }
        }
    
    }
    
    TaskCompletionSource<object> tcs;
    
    private void TryCancelReorder()
    {
        if (tcs != null)
        {
            tcs.TrySetCanceled();
            tcs = null;
        }
    }
    
    private Task NoticeReorderAsync(object removed)
    {
        TryCancelReorder();
        tcs = new TaskCompletionSource<object>(removed);
    
        return tcs.Task;
    }
    
    private bool NoticeAdd(object added)
    {
        if (tcs != null)
        {
            try
            {
                if (object.Equals(tcs.Task.AsyncState, added))
                {
                    tcs.TrySetResult(added);
                    return false;
                }
                else
                {
                    tcs.TrySetCanceled();
                    return true;
                }
            }
            finally
            {
                tcs = null;
            }
        }
        return true;
    }