Drag & Drop
VirtualScroll supports drag and drop operations, allowing users to reorder items by dragging them to new positions. This feature is available on both Android and iOS platforms and works seamlessly with observable collections.
Overview
Drag and drop functionality in VirtualScroll is enabled through the DragHandler property. All built-in adapter implementations support drag and drop operations out of the box, making it easy to add reordering capabilities to your lists.
Note: Section headers/footers and global headers/footers cannot be dragged. Only data items can be reordered.
Enabling Drag & Drop
To enable drag and drop, simply bind the DragHandler property to your adapter:
<vs:VirtualScroll ItemsSource="{Binding Adapter}"
DragHandler="{Binding Adapter}">
<!-- Templates -->
</vs:VirtualScroll>
Since all built-in adapters implement IReorderableVirtualScrollAdapter (which combines IVirtualScrollAdapter and IVirtualScrollDragHandler), you can bind the same adapter instance to both ItemsSource and DragHandler.
Basic Usage with Observable Collections
The simplest way to enable drag and drop is with an ObservableCollection<T>:
XAML:
<vs:VirtualScroll ItemsSource="{Binding Adapter}"
DragHandler="{Binding Adapter}">
<vs:VirtualScroll.ItemTemplate>
<DataTemplate x:DataType="models:MyItem">
<nalu:ViewBox>
<Border StrokeShape="RoundRectangle 8" Margin="8" Padding="16">
<Label Text="{Binding Name}" />
</Border>
</nalu:ViewBox>
</DataTemplate>
</vs:VirtualScroll.ItemTemplate>
</vs:VirtualScroll>
PageModel:
public partial class MyPageModel : ObservableObject
{
public ObservableCollection<MyItem> Items { get; }
public IReorderableVirtualScrollAdapter Adapter { get; }
public MyPageModel()
{
Items = new ObservableCollection<MyItem>(
Enumerable.Range(1, 20).Select(i => new MyItem($"Item {i}"))
);
// Create adapter - it automatically supports drag & drop
Adapter = VirtualScroll.CreateObservableCollectionAdapter(Items);
}
}
When you bind the adapter to DragHandler, users can long-press and drag items to reorder them. The underlying ObservableCollection is automatically updated, and change notifications are properly handled.
Grouped Collections
Drag and drop also works with grouped collections, allowing items to be moved both within sections and between sections:
XAML:
<vs:VirtualScroll ItemsSource="{Binding Adapter}"
DragHandler="{Binding Adapter}">
<vs:VirtualScroll.SectionHeaderTemplate>
<DataTemplate x:DataType="models:Group">
<Label Text="{Binding Title}" FontSize="18" FontAttributes="Bold"
BackgroundColor="LightGray" Padding="16,8" />
</DataTemplate>
</vs:VirtualScroll.SectionHeaderTemplate>
<vs:VirtualScroll.ItemTemplate>
<DataTemplate x:DataType="models:Item">
<nalu:ViewBox>
<Border StrokeShape="RoundRectangle 8" Margin="8" Padding="16">
<Label Text="{Binding Name}" />
</Border>
</nalu:ViewBox>
</DataTemplate>
</vs:VirtualScroll.ItemTemplate>
</vs:VirtualScroll>
PageModel:
public partial class GroupedPageModel : ObservableObject
{
public ObservableCollection<Group> Groups { get; }
public IReorderableVirtualScrollAdapter Adapter { get; }
public GroupedPageModel()
{
Groups = new ObservableCollection<Group>(
Enumerable.Range(1, 5).Select(i => new Group($"Group {i}"))
);
// Create grouped adapter - supports cross-section drag & drop
Adapter = VirtualScroll.CreateObservableCollectionAdapter(
Groups,
group => group.Items
);
}
}
public class Group : ObservableObject
{
public string Title { get; }
public ObservableCollection<Item> Items { get; }
public Group(string title)
{
Title = title;
Items = new ObservableCollection<Item>(
Enumerable.Range(1, 5).Select(i => new Item($"{title} - Item {i}"))
);
}
}
When dragging items in a grouped collection:
- Items can be moved within the same section
- Items can be moved between different sections
- The source and destination collections are automatically updated
- Change notifications are properly handled for both collections
Custom Drag Behavior
You can customize drag behavior by creating a custom adapter that implements IReorderableVirtualScrollAdapter or by overriding methods in the built-in adapters:
Controlling Which Items Can Be Dragged
Override CanDragItem to conditionally allow dragging:
public class CustomAdapter : VirtualScrollObservableCollectionAdapter<MyItem>
{
public CustomAdapter(ObservableCollection<MyItem> collection)
: base(collection)
{
}
public override bool CanDragItem(VirtualScrollDragInfo dragInfo)
{
// Only allow dragging if the item is not locked
if (dragInfo.Item is MyItem item)
{
return !item.IsLocked;
}
return false;
}
}
Controlling Drop Locations
Override CanDropItemAt to restrict where items can be dropped:
public override bool CanDropItemAt(VirtualScrollDragDropInfo dragDropInfo)
{
// Prevent dropping items at the beginning of the list
if (dragDropInfo.DestinationItemIndex == 0)
{
return false;
}
// Prevent moving items between certain sections
if (dragDropInfo.OriginalSectionIndex != dragDropInfo.DestinationSectionIndex)
{
// Only allow cross-section moves if both sections allow it
var sourceSection = GetSection(dragDropInfo.OriginalSectionIndex);
var destSection = GetSection(dragDropInfo.DestinationSectionIndex);
return sourceSection?.AllowExport == true &&
destSection?.AllowImport == true;
}
return true;
}
Custom Move Logic
Override MoveItem to implement custom move behavior:
public override void MoveItem(VirtualScrollDragMoveInfo dragMoveInfo)
{
// Perform custom validation or side effects before moving
if (dragMoveInfo.Item is MyItem item)
{
item.MovedAt = DateTime.Now;
}
// Call base implementation to perform the actual move
base.MoveItem(dragMoveInfo);
// Perform any post-move operations
OnItemMoved(item);
}
Lifecycle Hooks
The drag handler interface provides several lifecycle hooks that you can use to respond to drag operations:
OnDragInitiating
Called when a drag operation is about to start (before it actually begins):
public override void OnDragInitiating(VirtualScrollDragInfo dragInfo)
{
// Prepare for drag - e.g., show visual feedback
if (dragInfo.Item is MyItem item)
{
item.IsDragging = true;
}
}
OnDragStarted
Called when the drag operation has started:
public override void OnDragStarted(VirtualScrollDragInfo dragInfo)
{
// Track drag start - e.g., log analytics
Analytics.TrackEvent("ItemDragStarted", new Dictionary<string, string>
{
{ "ItemId", dragInfo.Item?.ToString() ?? "Unknown" }
});
}
OnDragEnded
Called when the drag operation has completed (whether successful or cancelled):
public override void OnDragEnded(VirtualScrollDragInfo dragInfo)
{
// Clean up drag state
if (dragInfo.Item is MyItem item)
{
item.IsDragging = false;
}
// Save changes if needed
SaveChanges();
}
Complete Example
Here's a complete example demonstrating drag and drop with custom behavior:
XAML:
<vs:VirtualScroll ItemsSource="{Binding Adapter}"
DragHandler="{Binding Adapter}"
FadingEdgeLength="32">
<vs:VirtualScroll.ItemTemplate>
<DataTemplate x:DataType="pageModels:TaskItem">
<nalu:ViewBox>
<Border StrokeShape="RoundRectangle 8"
Margin="8"
Padding="16"
BackgroundColor="{Binding BackgroundColor}">
<Grid ColumnDefinitions="*,Auto">
<Label Grid.Column="0"
Text="{Binding Title}"
FontSize="16" />
<Image Grid.Column="1"
Source="drag_handle.png"
WidthRequest="24"
HeightRequest="24"
Opacity="0.3" />
</Grid>
</Border>
</nalu:ViewBox>
</DataTemplate>
</vs:VirtualScroll.ItemTemplate>
</vs:VirtualScroll>
PageModel:
public partial class TaskListPageModel : ObservableObject
{
public ObservableCollection<TaskItem> Tasks { get; }
public IReorderableVirtualScrollAdapter Adapter { get; }
public TaskListPageModel()
{
Tasks = new ObservableCollection<TaskItem>(
Enumerable.Range(1, 10).Select(i => new TaskItem($"Task {i}"))
);
Adapter = VirtualScroll.CreateObservableCollectionAdapter(Tasks);
}
}
public partial class TaskItem : ObservableObject
{
[ObservableProperty]
private bool _isDragging;
[ObservableProperty]
private Color _backgroundColor = Colors.White;
public string Title { get; }
public TaskItem(string title)
{
Title = title;
}
}
Custom Adapter:
public class TaskListAdapter : VirtualScrollObservableCollectionAdapter<TaskItem>
{
public TaskListAdapter(ObservableCollection<TaskItem> collection)
: base(collection)
{
}
public override bool CanDragItem(VirtualScrollDragInfo dragInfo)
{
// Only allow dragging completed tasks
return dragInfo.Item is TaskItem task && task.IsCompleted;
}
public override void OnDragStarted(VirtualScrollDragInfo dragInfo)
{
if (dragInfo.Item is TaskItem task)
{
task.IsDragging = true;
task.BackgroundColor = Colors.LightBlue;
}
}
public override void OnDragEnded(VirtualScrollDragInfo dragInfo)
{
if (dragInfo.Item is TaskItem task)
{
task.IsDragging = false;
task.BackgroundColor = Colors.White;
}
}
}
Limitations
Headers and Footers: Section headers/footers and global headers/footers cannot be dragged. Only data items support drag and drop.
Read-Only Collections: Drag and drop requires collections that support modification. Fixed-size or read-only collections will throw an exception when drag operations are attempted.
Static Collections: Static collection adapters (created with
CreateStaticCollectionAdapter) do support drag and drop.Change Notifications: During drag swap operations, change notifications from the underlying collection are temporarily suppressed to prevent conflicts. Notifications resume after the drag completes.
Best Practices
Visual Feedback: Provide visual feedback during drag operations by updating item appearance in
OnDragStartedandOnDragEnded.Validation: Use
CanDragItemandCanDropItemAtto validate drag operations before they occur, providing a better user experience.Custom Drag Handler: While implementing a custom drag handler, make sure that
MoveItemdoes NOT trigger a notification.