Search code examples
xamlgridviewuwpxbox-onereorderlist

UWP Reorder Gridviewitems on Xbox


I am making a UWP app which is supposed to be on xbox for now and maybe in future ill release it on pc and other platforms. I know that on PC and for mobile we can enable this feature with following 2 properties on the GridView or ListView.

CanReorderItems=True
CanDrop=True

But according to Microsoft Docs, drag and drop feature is not available or supported on xbox.

So what are any other options to achieve this reorder feature on xbox GridView?

UPDATE 1

So here is my backend code for the gridview. selection mode is single but I am not using selectionchanged event because that just creates lot of confusion and for now just assume that we always need to swap the items I will set the boolean later once the swapping in working perfectly.

private void SamplePickerGridView_ChoosingItemContainer(Windows.UI.Xaml.Controls.ListViewBase sender, ChoosingItemContainerEventArgs args)
    {
        if (args.ItemContainer != null)
        {
            return;
        }
        GridViewItem container = (GridViewItem)args.ItemContainer ?? new GridViewItem();
        //should be xbox actually after pc testing
        if (DeviceTypeHelper.GetDeviceFormFactorType() == DeviceFormFactorType.Desktop)
        {
            container.GotFocus += Container_GotFocus;
            container.LostFocus += Container_LostFocus;
            //container.KeyDown += Container_KeyDown;
        }
        args.ItemContainer = container;
    }
    private TVShow GotItem, LostItem;
    private void Container_LostFocus(object sender, RoutedEventArgs e)
    {

        LostItem = OnNowAllGridView.ItemFromContainer(e.OriginalSource as GridViewItem) as TVShow;
        GotItem = null;

    }

    private void Container_GotFocus(object sender, RoutedEventArgs e)
    {

        GotItem = OnNowAllGridView.ItemFromContainer(e.OriginalSource as GridViewItem) as TVShow;
        if (GotItem != null && LostItem != null)
        {
            var focusedItem = GotItem;
            var lostitem = LostItem;
            var index1 = ViewModel.Source.IndexOf(focusedItem);
            var index2 = ViewModel.Source.IndexOf(lostitem);
            ViewModel.Source.Move(index1, index2);
        }
        LostItem = null;

    }

u can try the code with adaptivegridview or just normal gridview of uwp if it works with that it should work with adaptivegridview as well.

Current Bheaviour items are swaped but the focus remains at same index.

Expected the focus should also move along with the item.


Solution

  • Your finding is true, drag and drop is not supported on Xbox out of the box (although when mouse support comes to Xbox in the future, I guess it will work).

    So if you need this functionality, you will have to implement it manually from the start. One option would be to add a button, that will display on Xbox only and will read like Reorder Grid.

    When this "reorder" mode were enabled, you have several solutions available.

    The easiest solution for you would be to set the SelectionMode to Single and when a item is selected, you would bring it to fromt of the underlying collection.

    collection.Remove( selectedItem );
    collection.Insert( 0, selectedItem );
    

    This bring to front solution was implemented on the Xbox One dashboard for reordering tiles.

    Second option would be to set the SelectionMode to Multiple, where user would first select one item and then a second one. After that you could move the first selected item before the second selected:

    collection.Remove( firstSelectedItem );
    var targetIndex = collection.IndexOf( secondSelectedItem );
    collection.Insert( targetIndex, firstSelectedItem );
    

    The last solution is the most complex. With SelectionMode = Single you would select a single item and then observe the direction in which the user focus moves and move the tile "in real time". This is the most user friendly, but hardest to implement reliably.

    Just as an outline of the third solution - you could capture the GotFocus event if you implement a custom template of the GridView:

    <GridView.ItemsPanel>
        <ItemsPanelTemplate>
            <ItemsWrapGrid Orientation="Horizontal" 
                           GotFocus="GridViewItem_GotFocus"/>
        </ItemsPanelTemplate>
    </GridView.ItemsPanel>
    

    Now within this GotFocus handler you could retrieve the item that has currently focus from the EventArgs.OriginalSource. This way you could know which item got the focus and you could swap it with the item the user selected.

    Update - hacky solution

    I have come up with a hacky approach that solves the GotFocus/LostFocus mess.

    The problem with GotFocus is that when we move the item in collection, the focus gets confused. But what if we didn't physically move the items at all?

    Suppose your item type is TVShow. Let's create a wrapper around this type:

    public class TVShowContainer : INotifyPropertyChanged
    {
        private TVShow _tvShow;
    
        public TVShow TvShow
        {
            get => _tvShow;
            set
            {
                _tvShow = value; 
                OnPropertyChanged();
            }
        }
    
        public event PropertyChangedEventHandler PropertyChanged;
    
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    

    Now change the collection item type to this new "wrapper" type. Of course, you also have to update your GridView DataTemplate to have the right references. Instead of "{Binding Property}" you will now need to use "{Binding TvShow.Property}", or you can set the DataContext="{Binding TvShow}" attribute to the root element inside the DataTemplate.

    But you may now see where I am going with this. Currently you are using Move method to move the items in the collection. Let's replace this with a swap:

    var item1 = focusedItem.TvShow;
    focusedItem.TvShow = LostItem.TvShow;
    LostItem.TvShow = item1;
    

    This is a big difference, because we no longer change the collection itself, but just move the references to items that are wrapped in a "static" container. And thanks to bindings the items will properly display where they should.

    This is still a hacky solution, because it requires you to wrap your items just for the sake of the reordering, but it at least works. I am however still interested in finding a better way to do this.