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
?
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.
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. :)
The easiest way is to leverage ObservableCollection
, and let the ListBox
or w/e control take care of the rest.
All you have to do is, basically, create drag & drop handler, figure out which item client wanted to move where(keep track of oldIndex / newIndex), and implement the swap:
var dragSourceItem = yourObservable[oldIndex];
var dragTargetItem = yourObservable[newIndex];
yourObservable[newIndex]=dragSourceItem;
yourObservable[oldIndex]=dragTargetItem;
ObservableCollection
will raise ´Replaced` action, WPF knows how to take care of.
Here's something to get you going: http://www.hardcodet.net/2009/03/moving-data-grid-rows-using-drag-and-drop
You'd basically want to wrap it into attached behavior, and implement the swap in ViewModel
.
Here's how I did it (with thanks to this blog):
XAML code:
<ListView x:Name="MyListView" CanDragItems="True" AllowDrop="True" HorizontalAlignment="Center" VerticalAlignment="Center" DragItemsStarting="MyListView_DragItemsStarting" Drop="MyListView_Drop">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" AllowDrop="True" Drop="TextBlock_Drop" DragOver="TextBlock_DragOver"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
C# code:
ObservableCollection<string> MyList = new ObservableCollection<string>();
string DraggedString;
TextBlock DraggedOverTextBlock;
protected override void OnNavigatedTo(NavigationEventArgs e)
{
MyList.Add("1");
MyList.Add("2");
MyList.Add("3");
MyList.Add("4");
MyList.Add("5");
MyList.Add("6");
MyList.Add("7");
MyList.Add("8");
MyList.Add("9");
MyList.Add("10");
MyListView.ItemsSource = MyList;
}
private void MyListView_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
{
DraggedString = e.Items[0] as String;
}
private void MyListView_Drop(object sender, DragEventArgs e)
{
if (DraggedString == null || DraggedOverTextBlock == null) return;
var indexes = new List<int> { MyList.IndexOf(DraggedString), MyList.IndexOf(DraggedOverTextBlock.Text) };
if (indexes[0] == indexes[1]) return;
indexes.Sort();
var values = new List<string> { MyList[indexes[0]], MyList[indexes[1]] };
MyList.RemoveAt(indexes[1]);
MyList.RemoveAt(indexes[0]);
MyList.Insert(indexes[0], values[1]);
MyList.Insert(indexes[1], values[0]);
DraggedString = null;
DraggedOverTextBlock = null;
}
private void TextBlock_DragOver(object sender, DragEventArgs e)
{
DraggedOverTextBlock = sender as TextBlock;
}