https://github.com/russkyc/sortable-avalonia
MVVM sort, swap, and cross-collection transfer for Avalonia
https://github.com/russkyc/sortable-avalonia
avalonia avalonia-ui bento bento-grid drag drop kanban sortable
Last synced: 25 days ago
JSON representation
MVVM sort, swap, and cross-collection transfer for Avalonia
- Host: GitHub
- URL: https://github.com/russkyc/sortable-avalonia
- Owner: russkyc
- License: mit
- Created: 2026-03-08T05:02:58.000Z (2 months ago)
- Default Branch: master
- Last Pushed: 2026-03-16T16:36:38.000Z (2 months ago)
- Last Synced: 2026-03-16T19:58:18.342Z (2 months ago)
- Topics: avalonia, avalonia-ui, bento, bento-grid, drag, drop, kanban, sortable
- Language: C#
- Homepage:
- Size: 23.2 MB
- Stars: 22
- Watchers: 2
- Forks: 2
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
- awesome-avalonia - Sortable.Avalonia - Animated drag-drop and sort-swap behavior attachments for Avalonia inspired by SortableJS (Libraries & Extensions / Generic)
README
Sortable.Avalonia - MVVM sort, swap, and cross-collection transfer for Avalonia
MVVM-first attached-behavior library for Avalonia `ItemsControl` enabling drag-and-drop reordering, cross-collection transfers, reversible drop operations, drag handles, sort/swap modes, and animated programmatic updates.
> [!NOTE]
> #### Changes in Version 2.2.0
> - Package now targets NET 8.0 and NET 10.0 and now requires Avalonia 12.0+. Update your project references accordingly.
> #### Changes in Version 2.0.0
> - **Breaking change:** `AnimationDuration` now uses `TimeSpan` instead of `int` (milliseconds). Update your XAML and code to use TimeSpan format (e.g., `0:0:0.500`).
> - **New feature:** Release behavior with `ReleaseCommand` and `SortableReleaseEventArgs` for handling items released outside valid drop targets.
## Overview
- [Demo](#demo)
- [Features](#features)
- [Installation](#installation)
- [Quickstart](#quickstart)
- [Item Display Layout (ItemsPanelTemplate)](#item-display-layout-itemspaneltemplate)
- [Core Concepts](#core-concepts)
- [Properties Reference](#properties-reference)
- [Event Arguments](#event-arguments)
- [Mutation Helper Extensions](#mutation-helper-extensions)
- [Usage Patterns](#usage-patterns)
- [Transfer Modes](#transfer-modes)
- [Sortable Modes](#sortable-modes)
- [Drag Handles](#drag-handles)
- [Animation Control](#animation-control)
- [Custom Drag Template](#custom-drag-template)
- [Groups](#groups)
- [18 Demo Scenarios](#18-demo-scenarios)
- [Documentation](#documentation)
- [Contributing](#contributing)
- [License](#license)
## Demo
**Stack Panel (Horizontal Stack)**

**Kanban (Sort and Cross-collection drop)**

**Uniform Grid (Sort and Swap)**


## Features
| Capability | Description |
|---|---|
| **Same-collection sorting** | Reorder items within one list via `Sortable` property |
| **Cross-collection transfers** | Move/copy/swap items between lists via `Droppable` property |
| **Reversible drops** | Accept/reject drops in handler before commit |
| **Transfer modes** | `Move`, `Copy`, `Swap` |
| **Sortable modes** | `Sort` (shift), `Swap` (exchange) |
| **Drag handles** | Restrict drag start to marked controls |
| **Custom drag template** | Fully customize drag preview with `DraggingTemplate` |
| **Animation** | Smooth transitions for interactive + programmatic changes |
| **Groups** | Isolate interactions by group name |
| **Mouse + Touch** | Unified pointer input on all platforms |
| **Backward compatible** | Old `TransferCommand` still works |
## Installation
Install via NuGet (`Sortable.Avalonia`):
**.NET CLI**
```powershell
dotnet add package Sortable.Avalonia
```
**Package Manager Console**
```powershell
Install-Package Sortable.Avalonia
```
**XAML namespace:**
```xml
xmlns:sortable="clr-namespace:Sortable.Avalonia;assembly=Sortable.Avalonia"
```
## Quickstart
The control behavior is configured in XAML, while reorder/transfer decisions stay in your ViewModel via commands (`UpdateCommand`, `DropCommand`).
**1. Sortable list (same-collection only, ViewModel-driven updates):**
```xml
```
```csharp
[RelayCommand]
void Update(SortableUpdateEventArgs e)
{
if (!e.ApplyUpdateMutation())
{
return;
}
Console.WriteLine($"Moved from {e.OldIndex} to {e.NewIndex}");
}
```
**2. Droppable targets (cross-collection, ViewModel acceptance):**
```xml
```
```csharp
[RelayCommand]
void Drop(SortableDropEventArgs e)
{
e.IsAccepted = ValidateItem(e.Item);
e.TransferMode = SortableTransferMode.Move;
var applied = e.ApplyDropMutation();
if (!applied)
{
Debug.WriteLine("Drop was rejected or produced no mutation.");
}
}
```
**3. Run demo app:**
```powershell
dotnet run --project .\Sortable.Avalonia.Demo\Sortable.Avalonia.Demo.csproj
```
## Item Display Layout (ItemsPanelTemplate)
`Sortable` works with any `ItemsControl` panel. You can change how items are displayed by overriding `ItemsControl.ItemsPanel` with an `ItemsPanelTemplate`.
**Vertical stack (default list):**
```xml
```
**Horizontal stack (lane-style):**
```xml
```
**Uniform grid (card board):**
```xml
```
**Wrap panel (responsive flow):**
```xml
```
Tip: panel choice affects visual arrangement only; drag/drop behavior still depends on `Sortable`, `Droppable`, and item-level flags (`IsSortable`, `IsDroppable`), so layout stays in XAML while interaction rules stay in the ViewModel.
## Properties Reference
### ItemsControl Attached Properties
| Property | Type | Default | Description |
|---|---|:---:|---|
| `Sortable` | `bool` | `false` | Enable same-collection reordering |
| `Droppable` | `bool` | `false` | Enable cross-collection drop target |
| `Group` | `string?` | `null` | Group name (only same-group collections interact) |
| `UpdateCommand` | `ICommand?` | `null` | Fires on same-collection reorder |
| `DropCommand` | `ICommand?` | `null` | Fires on cross-collection drop (reversible) |
| `ReleaseCommand` | `ICommand?` | `null` | Fires when item is released outside any valid drop target |
| `TransferCommand` | `ICommand?` | `null` | Legacy cross-collection (auto-accept, deprecated) |
| `Mode` | `SortableMode` | `Sort` | In-collection behavior: `Sort` or `Swap` |
| `CrossCollectionTransferMode` | `SortableTransferMode` | `Move` | Default transfer mode: `Move`, `Copy`, or `Swap` |
| `AnimationDuration` | `TimeSpan` | `0:0:0.250` | Animation duration (as TimeSpan, e.g. `0:0:0.500` for 500ms) |
### Item Attached Properties
| Property | Type | Default | Description |
|---|---|:---:|---|
| `IsSortable` | `bool` | `false` | Item can be sorted within collection |
| `IsDroppable` | `bool` | `false` | Item can participate in drops |
| `IsDragHandle` | `bool` | `false` | Marks control as drag handle |
## Event Arguments
### SortableUpdateEventArgs
```csharp
public class SortableUpdateEventArgs
{
public object? Item { get; set; } // Item being moved
public int OldIndex { get; set; } // Original index
public int NewIndex { get; set; } // Target index
public IList? SourceCollection { get; set; }
public SortableMode Mode { get; set; } // Sort or Swap
}
```
**Usage:**
```csharp
[RelayCommand]
void Update(SortableUpdateEventArgs e)
{
if (e.ApplyUpdateMutation())
{
Debug.WriteLine($"{e.Item}: {e.OldIndex} → {e.NewIndex}");
}
}
```
### SortableReleaseEventArgs
```csharp
public class SortableReleaseEventArgs
{
public object? Item { get; set; } // Item released
public int OldIndex { get; set; } // Original index
public IList? SourceCollection { get; set; } // Collection item was dragged from
}
```
**Usage:**
```csharp
[RelayCommand]
void Release(SortableReleaseEventArgs e)
{
Debug.WriteLine($"Item '{e.Item}' released at index {e.OldIndex} from collection {e.SourceCollection}");
// Custom logic for when an item is released outside any valid drop target
}
```
### SortableDropEventArgs
```csharp
public class SortableDropEventArgs
{
// Read-only context
public object? Item { get; set; }
public IList? SourceCollection { get; set; }
public IList? TargetCollection { get; set; }
public int OldIndex { get; set; }
public int NewIndex { get; set; }
// Handler control
public bool IsAccepted { get; set; } = true; // Accept/reject
public SortableTransferMode TransferMode { get; set; } = Move; // Move/Copy/Swap
public object? ModifiedItem { get; set; } // Optional clone/modified item
public object? GetItemToInsert() => ModifiedItem ?? Item;
}
```
**Usage:**
```csharp
[RelayCommand]
void Drop(SortableDropEventArgs e)
{
e.IsAccepted = Validate(e.Item);
e.TransferMode = SortableTransferMode.Move;
if (!e.ApplyDropMutation())
{
Debug.WriteLine("Drop mutation was not applied.");
}
}
```
### Mutation Helper Extensions
The library provides extension methods on event args so you can delegate mutation mechanics while keeping full control of business rules and acceptance logic.
**Three approaches:**
#### 1. Helper-driven (simplest)
```csharp
[RelayCommand]
void Drop(SortableDropEventArgs e)
{
e.IsAccepted = true;
e.TransferMode = SortableTransferMode.Move;
if (!e.ApplyDropMutation())
{
Debug.WriteLine("Mutation failed or was rejected.");
}
}
```
#### 2. Absolute control (manual mutations)
```csharp
[RelayCommand]
void Drop(SortableDropEventArgs e)
{
if (e.Item is not TaskItem task || !ValidateBusinessRules(task))
{
e.IsAccepted = false;
return;
}
e.IsAccepted = true;
e.TransferMode = SortableTransferMode.Move;
// Manually orchestrate the mutation with your exact logic
e.TargetCollection.Insert(e.NewIndex, task);
e.SourceCollection.RemoveAt(e.OldIndex);
LogCustomTelemetry(task, e.SourceCollection, e.TargetCollection);
}
```
#### 3. Hybrid (recommended for most apps)
Validate domain rules yourself, delegate low-level list operations to the helper.
```csharp
[RelayCommand]
void Drop(SortableDropEventArgs e)
{
if (e.Item is not KanbanCard card)
{
return;
}
var sourceColumn = FindColumn(e.SourceCollection);
var targetColumn = FindColumn(e.TargetCollection);
if (sourceColumn == null || targetColumn == null)
{
return;
}
// Domain rule: prevent duplicates
if (targetColumn.Items.Contains(card))
{
e.IsAccepted = false;
return;
}
e.IsAccepted = true;
e.TransferMode = SortableTransferMode.Move;
// Let helper handle the mutation
if (!e.ApplyDropMutation())
{
return;
}
// Post-mutation side effects
Console.WriteLine($"Moved '{card.Title}' from {sourceColumn.Name} to {targetColumn.Name}");
NotifyTeam(card, targetColumn);
}
```
**Return values:**
- `e.ApplyUpdateMutation()` → `bool` (true if mutation applied)
- `e.ApplyDropMutation()` → `bool` (true if mutation applied)
Use the return value to gate logging, telemetry, or conditional side effects.
### SortableTransferEventArgs (Deprecated)
```csharp
public class SortableTransferEventArgs
{
public object? Item { get; set; }
public IList? SourceCollection { get; set; }
public IList? TargetCollection { get; set; }
public int OldIndex { get; set; }
public int NewIndex { get; set; }
}
```
Use `DropCommand` with `SortableDropEventArgs` instead.
## Usage Patterns
For panel/layout examples (`StackPanel`, `UniformGrid`, `WrapPanel`), see [Item Display Layout (ItemsPanelTemplate)](#item-display-layout-itemspaneltemplate).
### Pattern 1: Sortable-only list
```xml
```
**Result:** Reorder within list ✓, transfer between lists ✗
### Pattern 2: Droppable-only zones
```xml
```
**Result:** Transfer between zones ✓, reorder within zone ✗
### Pattern 3: Full dual-mode
```xml
```
**Result:** Reorder within list ✓, transfer between lists ✓
### Pattern 4: Conditional acceptance
```csharp
[RelayCommand]
void Drop(SortableDropEventArgs e)
{
if (e.Item is TaskItem task)
{
// Business rule validation
e.IsAccepted = task.Priority == Priority.Urgent
&& !TargetContains(task)
&& UserHasPermission();
e.TransferMode = SortableTransferMode.Move;
_ = e.ApplyDropMutation();
}
}
```
### Pattern 5: Copy mode (duplicate on transfer)
```csharp
[RelayCommand]
void Drop(SortableDropEventArgs e)
{
if (e.Item is TemplateItem original)
{
e.ModifiedItem = new TemplateItem(original) { Id = Guid.NewGuid() };
e.TransferMode = SortableTransferMode.Copy;
e.IsAccepted = true;
_ = e.ApplyDropMutation();
}
}
```
### Pattern 6: Prevent duplicates
```csharp
[RelayCommand]
void Drop(SortableDropEventArgs e)
{
var target = e.TargetCollection as ObservableCollection;
var item = e.Item as MyItem;
e.IsAccepted = target?.Any(x => x.Id == item?.Id) != true;
e.TransferMode = SortableTransferMode.Move;
_ = e.ApplyDropMutation();
}
```
### Pattern 7: Cross-collection swap
```xml
```
```csharp
[RelayCommand]
void Drop(SortableDropEventArgs e)
{
e.TransferMode = SortableTransferMode.Swap; // Exchange items
e.IsAccepted = true;
_ = e.ApplyDropMutation();
}
```
### Pattern 8: Drag handles
```xml
```
**Result:** Only drag via handle icon. Text and button remain clickable.
### Pattern 9: Same-collection swap mode
```xml
```
**Result:** Items swap positions instead of shifting.
### Pattern 10: Custom animation speed
```xml
```
### Pattern 11: Custom drag template
You can fully customize the drag preview for any ItemsControl using the `DraggingTemplate` attached property. This lets you define a `DataTemplate` for the drag visual, supporting rich layouts, icons, and dynamic content.
```xml
```
**Result:** Custom drag preview for each item, supporting rich layouts and dynamic content.
**Property Reference:** `DraggingTemplate` (attached property)
**Demo:** See "Custom Drag Template" scenario in the demo app.
## Transfer Modes
| Mode | Behavior | Source | Target | Example Use Case |
|---|---|:---:|:---:|---|
| `Move` | Remove from source, add to target | Item removed | Item added | Task workflow, file organization |
| `Copy` | Keep in source, add to target | Item stays | Copy added | Template duplication, reference sharing |
| `Swap` | Exchange items | Gets target item | Gets source item | Role rotation, seat assignment |
**Set via:**
```csharp
e.TransferMode = SortableTransferMode.Move; // or .Copy or .Swap
```
**Or set default for preview animations:**
```xml
sortable:Sortable.CrossCollectionTransferMode="Swap"
```
## Sortable Modes
| Mode | Behavior | Use Case |
|---|---|---|
| `Sort` | Shift items, insert at drop position | Priority queues, task ordering |
| `Swap` | Exchange positions with drop target | Role swaps, seat assignments |
**Set via:**
```xml
sortable:Sortable.Mode="Sort"
sortable:Sortable.Mode="Swap"
```
## Drag Handles
Restrict drag start to specific controls:
```xml
```
**Behavior:** Drag only starts from `IsDragHandle="True"` controls. Text, buttons, etc. remain clickable.
## Animation Control
**Set duration (TimeSpan):**
```xml
sortable:Sortable.AnimationDuration="0:0:0.500"
```
**Applies to:**
- Interactive drag previews (items shifting during drag)
- Programmatic collection changes
- Cross-collection travel animations
## Groups
**Isolate interactions:**
```xml
```
**Rule:** Items only transfer between collections with matching `Group` values.
## 18 Demo Scenarios
Run the app to explore each demo in a dedicated tab:
| # | Demo | Description |
|:---:|---|---|
| 1 | **Kanban Board** | Task cards moving through triage → engineering → release columns |
| 2 | **Vertical List** | Simple sortable task list with reordering |
| 3 | **Drag Handle** | Items with icon handles, leaving text/buttons clickable |
| 4 | **Grid Layout** | Card-based layout with drag-and-drop in grid arrangement |
| 5 | **Horizontal Stack** | Horizontal lane with left-to-right item ordering |
| 6 | **Multiple Groups** | Separate HR and Engineering groups (isolated interactions) |
| 7 | **Disabled Items** | Some items locked (non-draggable) via conditional `IsSortable` |
| 8 | **Sortable Only** | Lists allow reordering but reject cross-list transfers |
| 9 | **Droppable Only** | Drop zones accept items but prevent internal reordering |
| 10 | **Cross Drag (Instant)** | Instant cross-collection transfers (no animation delay) |
| 11 | **Cross Programmatic Animation** | Programmatic `ObservableCollection` changes with smooth travel animation |
| 12 | **UniformGrid Interaction** | Interactive drag in `UniformGrid` panel |
| 13 | **UniformGrid Programmatic** | Programmatic changes in `UniformGrid` with animation |
| 14 | **Copy Mode** | Template items duplicated (not moved) on transfer |
| 15 | **Conditional Acceptance** | Drop validation rules (e.g., only URGENT tasks accepted) |
| 16 | **Sort Mode** | Default shift-based reordering within same list |
| 17 | **Swap Mode** | Exchange positions (no shifting) within same list |
| 18 | **Cross Swap** | Exchange items between two collections in one gesture |
**Run:**
```powershell
dotnet run --project .\Sortable.Avalonia.Demo\Sortable.Avalonia.Demo.csproj
```
## Core Concepts
### MVVM-First Workflow
- **View (XAML):** Declare `Sortable` attached properties, `ItemsSource`, and command bindings.
- **ViewModel:** Validate rules, choose transfer/sort behavior, and apply mutations via event-arg helpers.
- **Model:** Remains plain data; no drag/drop UI logic required.
### Sortable vs Droppable
| Property | Purpose | Drag Within List | Drag Between Lists |
|---|---|:---:|:---:|
| `Sortable="True"` | Enable reordering | ✓ | ✗ |
| `Droppable="True"` | Enable drop target | ✗ | ✓ |
| Both `="True"` | Enable both | ✓ | ✓ |
### Item-Level Control
| Scenario | IsSortable | IsDroppable |
|---|:---:|:---:|
| Can reorder and transfer | ✓ | ✓ |
| Can reorder only | ✓ | ✗ |
| Can transfer only | ✗ | ✓ |
| Locked (no drag) | ✗ | ✗ |
### Commands
| Command | Fires When | Event Args | Purpose |
|---|---|---|---|
| `UpdateCommand` | Same-collection reorder | `SortableUpdateEventArgs` | ViewModel handles reorder rules and mutation |
| `DropCommand` | Cross-collection drop | `SortableDropEventArgs` | ViewModel accepts/rejects and selects transfer mode |
| `ReleaseCommand` | Release outside drop target | `SortableReleaseEventArgs` | ViewModel handles item release outside valid drop zone |
| `TransferCommand` | Cross-collection transfer | `SortableTransferEventArgs` | Legacy fallback (auto-accept), prefer `DropCommand` |
## Contributing
Contributions are welcome.
- Open an issue for bug reports or feature proposals.
- Share repro steps and expected behavior for drag/drop issues.
- Keep PRs focused and include before/after behavior notes.
## License
This project is licensed under the MIT License. See `LICENSE` for details.