Reputation: 141
I am building a Windows Store App / Universal App targeting Windows 8.1 and Windows 10 and I would like to be able to drag and drop items between ListViews and be able to position the item in a specific spot in the ListView. The main problem I'm having is that I can't find a good way to determine the list index of where the item was dropped.
I found a sample (XAML ListView reorder) but an important difference is that the items in my list have variable heights so the simple calculation this sample project uses to infer the index won't work for me.
I am able to get the x,y position of where within the ListView the item was dropped, but I'm having trouble using that position to figure out the index. I've found mentions of people using ListView.GetItemAt(x, y) or ListView.HitTest(x, y) but as other have found, those methods don't seem to exist in Windows Universal apps. I've also tried using VisualTreeHelper.FindElementsInHostCoordinates() but I'm either not using it properly or I'm not understanding its purpose because I can't get it to return results.
Here is some example code that I've tried:
private void ListView_OnDrop(object sender, DragEventArgs e)
{
var targetListView = (ListView)sender;
var positionRelativeToTarget = e.GetPosition(targetListView);
var rect = new Rect(positionRelativeToTarget, new Size(10, 15));
var elements = VisualTreeHelper.FindElementsInHostCoordinates(rect, targetListView);
// Trying to get the index in the list where the item was dropped
// 'elements' is always empty
}
For reference, I'm using C#, XAML, and Visual Studio 2013.
Thanks!
Upvotes: 2
Views: 1463
Reputation: 1
I'm doing this to get the target listviewItem on the drag over event. It should work for both list and grid views
private YOURITEMCLASS _dragTarget;
private void ItemListView_OnDragOver(object sender, DragEventArgs e)
{
var pos = e.GetPosition(this);
// Offset position by left and top borders if in split view control
var elements = VisualTreeHelper.FindElementsInHostCoordinates(pos, this);
foreach (var element in elements)
{
var cellItem = element as ContentControl;
var item = cellItem?.Content as YOURITEMCLASS;
if (item == null) continue;
_dragTarget = item;
break;
}
}
Upvotes: 0
Reputation: 1411
If you use a DataContext
as part of your list population process, then you can simply do the following:
private void x_Drop(object sender, DragEventArgs e)
{
MyDataModel model = (sender as FrameworkElement).DataContext as MyDataModel;
// ...
Upvotes: 0
Reputation: 341
I found a solution. I developed an Info Class to recuperate the index of where I dropped the new item.
public class Info
{
public int index { get; set; }
public string color { get; set; }
}
I then defined my observable:
ObservableCollection<Info> c = new ObservableCollection<Info>();
c.Add(new Info { color = "#d9202b", index = 0 }); c.Add(new Info { color = "#ffffff", index = 1 });
c.Add(new Info { color = "#15c23c", index = 2 }); c.Add(new Info { color = "#c29b8f", index = 3 });
c.Add(new Info { color = "#0000ff", index = 4 }); c.Add(new Info { color = "#deba83", index = 5 });
I also defined another collection(c2) the same way. For this senario I will drag an item from second collection(c2) and i will drop it in the first one(c) So for the dragstarted
I used this:
private void x2_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
{
strin = e.Items.FirstOrDefault() as Info;
e.Data.Properties.Add("item", strin);
e.Data.Properties.Add("gridSource", sender);
}
To recuperate information about the place where I dropped my item, it should be dropped over an item in the first list, so I used this:
private void x_Drop(object sender, DragEventArgs e)
{
object gridSource;
e.Data.Properties.TryGetValue("gridSource", out gridSource);
if (gridSource == sender)
return;
object sourceItem;
e.Data.Properties.TryGetValue("item", out sourceItem);
//recuperate Info about place of dropped item
Info p = ((FrameworkElement)e.OriginalSource).DataContext as Info;
if(p==null)
{
//its not dropped over an item, lets add it in the end of the collection
c2.Remove(sourceItem as Info);
c.Add(sourceItem as Info);
}
else
{
//here we have information that we need
c2.Remove(sourceItem as Info);
c.Insert(p.index, sourceItem as Info);
//c.Add(strin);
}
Reorder();
}
Then we should set index for new items in Reorder method:
private void Reorder()
{
for (int i = 0; i < c.Count; i++)
c[i].index = i;
for (int i = 0; i < c2.Count; i++)
c2[i].index = i;
}
Upvotes: 5
Reputation: 141
I found a solution that is good enough for my purposes. Essentially what I ended up doing was handling the drop event on both the ListView and the list item because it's easy to figure out the index if the drop happens on a list item. I still had to handle the drop on the ListView too though for when an item is dropped in between.
Here is the code I ended up with:
MyView.xaml
<UserControl.Resources>
<DataTemplate x:Key="MyItemTemplate">
<local:MyControl AllowDrop="True" Drop="ListItem_OnDrop" />
</DataTemplate>
</UserControl.Resources>
<Grid>
<ListView ItemTemplate="{StaticResource MyItemTemplate}"
CanDragItems="True" AllowDrop="True"
DragItemsStarting="ListView_OnDragItemsStarting"
DragOver="ListView_OnDragOver"
DragLeave="ListView_OnDragLeave"
Drop="ListView_OnDrop" />
</Grid>
MyView.xaml.cs
public sealed partial class MyView
{
private readonly SolidColorBrush listViewDragOverBackgroundBrush = new SolidColorBrush(Color.FromArgb(255, 247, 247, 247));
public MyView()
{
InitializeComponent();
}
private IMyViewModel ViewModel
{
get { return DataContext as IMyViewModel; }
}
private void ListView_OnDragItemsStarting(object sender, DragItemsStartingEventArgs e)
{
e.Data.Properties.Add("dataItem", e.Items[0] as IMyItemViewModel);
}
private void ListView_OnDragOver(object sender, DragEventArgs e)
{
var dropTarget = sender as ListView;
if (dropTarget == null)
{
return;
}
dropTarget.Background = listViewDragOverBackgroundBrush;
}
private void ListView_OnDragLeave(object sender, DragEventArgs e)
{
var dropTarget = sender as ListView;
if (dropTarget == null)
{
return;
}
dropTarget.Background = null;
}
private void ListView_OnDrop(object sender, DragEventArgs e)
{
var draggedItem = e.Data.Properties["dataItem"] as IMyItemViewModel;
var targetListView = sender as ListView;
if (targetListView == null || draggedItem == null)
{
return;
}
targetListView.Background = null;
var droppedPosition = e.GetPosition(targetListView);
var itemsSource = targetListView.ItemsSource as IList;
const double extraHeightThatImNotSureWhereItCameFrom = 8d;
var highWaterMark = 3d; // This list starts with 3px of padding
var dropIndex = 0;
var foundDropLocation = false;
for (int i = 0; i < itemsSource.Count && !foundDropLocation; i++)
{
var itemContainer = (ListViewItem)targetListView.ContainerFromIndex(i);
highWaterMark = highWaterMark + itemContainer.ActualHeight - extraHeightThatImNotSureWhereItCameFrom;
if (droppedPosition.Y <= highWaterMark)
{
dropIndex = i;
foundDropLocation = true;
}
}
if (foundDropLocation)
{
// Handle the drag/drop at a specific location
// DropPosition is an enumeration I made that has Before & After
ViewModel.CompleteDragDrop(draggedItem, DropPosition.Before, dropIndex);
}
else
{
// Add to the end of the list. Also works for an empty list.
ViewModel.CompleteEvidenceDragDrop(draggedItem, DropPosition.After, itemsSource.Count - 1);
}
}
private void ListItem_OnDrop(object sender, DragEventArgs e)
{
e.Handled = true;
var draggedItem = e.Data.Properties["dataItem"] as IMyItemViewModel;
var dropTarget = sender as MyControl;
if (dropTarget == null || draggedItem == null)
{
return;
}
var parentList = dropTarget.Closest<ListView>();
var dropPosition = dropTarget.GetDropPosition(e);
parentList.Background = null;
ViewModel.CompleteDragDrop(draggedItem, dropPosition, dropTarget.DataContext as IMyItemViewModel);
}
}
ExtensionMethods
public static class ExtensionMethods
{
public static T Closest<T>(this DependencyObject obj) where T : DependencyObject
{
if (obj == null)
{
return null;
}
while (true)
{
var parent = VisualTreeHelper.GetParent(obj);
if (parent == null)
{
return null;
}
if (parent.GetType() == typeof(T))
{
return (T)parent;
}
obj = parent;
}
}
public static DropPosition GetDropPosition(this FrameworkElement dropTarget, DragEventArgs e)
{
var positionRelativeToTarget = e.GetPosition(dropTarget);
var dropBefore = positionRelativeToTarget.Y < (dropTarget.ActualHeight / 2);
return dropBefore ? DropPosition.Before : DropPosition.After;
}
}
Upvotes: 1