{"id":47221988,"url":"https://github.com/russkyc/sortable-avalonia","last_synced_at":"2026-04-25T23:38:22.455Z","repository":{"id":342953236,"uuid":"1175735025","full_name":"russkyc/sortable-avalonia","owner":"russkyc","description":"MVVM sort, swap, and cross-collection transfer for Avalonia","archived":false,"fork":false,"pushed_at":"2026-03-16T16:36:38.000Z","size":24307,"stargazers_count":22,"open_issues_count":0,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2026-03-16T19:58:18.342Z","etag":null,"topics":["avalonia","avalonia-ui","bento","bento-grid","drag","drop","kanban","sortable"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/russkyc.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"russkyc","patreon":"russkyc","custom":["https://paypal.me/jrcmo"]}},"created_at":"2026-03-08T05:02:58.000Z","updated_at":"2026-03-16T16:36:43.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/russkyc/sortable-avalonia","commit_stats":null,"previous_names":["russkyc/sortable-avalonia"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/russkyc/sortable-avalonia","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/russkyc%2Fsortable-avalonia","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/russkyc%2Fsortable-avalonia/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/russkyc%2Fsortable-avalonia/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/russkyc%2Fsortable-avalonia/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/russkyc","download_url":"https://codeload.github.com/russkyc/sortable-avalonia/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/russkyc%2Fsortable-avalonia/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31068492,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-27T22:49:25.097Z","status":"ssl_error","status_checked_at":"2026-03-27T22:49:22.599Z","response_time":164,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["avalonia","avalonia-ui","bento","bento-grid","drag","drop","kanban","sortable"],"created_at":"2026-03-13T19:00:25.124Z","updated_at":"2026-04-25T23:38:22.446Z","avatar_url":"https://github.com/russkyc.png","language":"C#","funding_links":["https://github.com/sponsors/russkyc","https://patreon.com/russkyc","https://paypal.me/jrcmo"],"categories":["Libraries \u0026 Extensions"],"sub_categories":["Generic"],"readme":"﻿\u003cp align=\"center\"\u003e\n\u003cimg src=\".github/resources/media/icon.png\" style=\"width: 120px;\" /\u003e\n\u003c/p\u003e\n\n\u003ch2 align=\"center\"\u003eSortable.Avalonia - MVVM sort, swap, and cross-collection transfer for Avalonia\u003c/h2\u003e\n\n\u003cp align=\"center\"\u003e\n    \u003cimg src=\"https://img.shields.io/nuget/v/Sortable.Avalonia?color=1f72de\" alt=\"Nuget\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/-12.X-blueviolet?color=1f72de\u0026label=Avalonia\" alt=\"\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/-.NET%208-blueviolet?color=1f72de\u0026label=NET\" alt=\"\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/-.NET%2010-blueviolet?color=1f72de\u0026label=NET\" alt=\"\"\u003e\n    \u003cimg src=\"https://img.shields.io/github/license/russkyc/sortable-avalonia\"\u003e\n    \u003cimg src=\"https://img.shields.io/github/issues/russkyc/sortable-avalonia\"\u003e\n    \u003cimg src=\"https://img.shields.io/nuget/dt/Sortable.Avalonia\"\u003e\n\u003c/p\u003e\n\nMVVM-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.\n\n\u003e [!NOTE]\n\u003e #### Changes in Version 2.2.0\n\u003e - Package now targets NET 8.0 and NET 10.0 and now requires Avalonia 12.0+. Update your project references accordingly.\n\u003e #### Changes in Version 2.0.0\n\u003e - **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`).\n\u003e - **New feature:** Release behavior with `ReleaseCommand` and `SortableReleaseEventArgs` for handling items released outside valid drop targets.\n\n## Overview\n\n- [Demo](#demo)\n- [Features](#features)\n- [Installation](#installation)\n- [Quickstart](#quickstart)\n- [Item Display Layout (ItemsPanelTemplate)](#item-display-layout-itemspaneltemplate)\n- [Core Concepts](#core-concepts)\n- [Properties Reference](#properties-reference)\n- [Event Arguments](#event-arguments)\n- [Mutation Helper Extensions](#mutation-helper-extensions)\n- [Usage Patterns](#usage-patterns)\n- [Transfer Modes](#transfer-modes)\n- [Sortable Modes](#sortable-modes)\n- [Drag Handles](#drag-handles)\n- [Animation Control](#animation-control)\n- [Custom Drag Template](#custom-drag-template)\n- [Groups](#groups)\n- [18 Demo Scenarios](#18-demo-scenarios)\n- [Documentation](#documentation)\n- [Contributing](#contributing)\n- [License](#license)\n\n## Demo\n\n**Stack Panel (Horizontal Stack)**\n\n\u003cimg src=\".github/resources/media/horizontal-stack.gif\" style=\"widt: 100%;\"\u003e\n\n**Kanban (Sort and Cross-collection drop)**\n\n\u003cimg src=\".github/resources/media/kanban.gif\" style=\"widt: 100%;\"\u003e\n\n**Uniform Grid (Sort and Swap)**\n\n\u003cimg src=\".github/resources/media/grid-sort.gif\" style=\"widt: 100%;\"\u003e\n\n\u003cimg src=\".github/resources/media/grid-swap.gif\" style=\"widt: 100%;\"\u003e\n\n## Features\n\n| Capability | Description |\n|---|---|\n| **Same-collection sorting** | Reorder items within one list via `Sortable` property |\n| **Cross-collection transfers** | Move/copy/swap items between lists via `Droppable` property |\n| **Reversible drops** | Accept/reject drops in handler before commit |\n| **Transfer modes** | `Move`, `Copy`, `Swap` |\n| **Sortable modes** | `Sort` (shift), `Swap` (exchange) |\n| **Drag handles** | Restrict drag start to marked controls |\n| **Custom drag template** | Fully customize drag preview with `DraggingTemplate` |\n| **Animation** | Smooth transitions for interactive + programmatic changes |\n| **Groups** | Isolate interactions by group name |\n| **Mouse + Touch** | Unified pointer input on all platforms |\n| **Backward compatible** | Old `TransferCommand` still works |\n\n## Installation\n\nInstall via NuGet (`Sortable.Avalonia`):\n\n**.NET CLI**\n\n```powershell\ndotnet add package Sortable.Avalonia\n```\n\n**Package Manager Console**\n\n```powershell\nInstall-Package Sortable.Avalonia\n```\n\n**XAML namespace:**\n\n```xml\nxmlns:sortable=\"clr-namespace:Sortable.Avalonia;assembly=Sortable.Avalonia\"\n```\n\n## Quickstart\n\nThe control behavior is configured in XAML, while reorder/transfer decisions stay in your ViewModel via commands (`UpdateCommand`, `DropCommand`).\n\n**1. Sortable list (same-collection only, ViewModel-driven updates):**\n\n```xml\n\u003cItemsControl sortable:Sortable.Sortable=\"True\"\n              sortable:Sortable.UpdateCommand=\"{Binding UpdateCmd}\"\n              ItemsSource=\"{Binding Items}\"\u003e\n    \u003cItemsControl.ItemTemplate\u003e\n        \u003cDataTemplate\u003e\n            \u003cBorder sortable:Sortable.IsSortable=\"True\" Cursor=\"Hand\"\u003e\n                \u003cTextBlock Text=\"{Binding Name}\" /\u003e\n            \u003c/Border\u003e\n        \u003c/DataTemplate\u003e\n    \u003c/ItemsControl.ItemTemplate\u003e\n\u003c/ItemsControl\u003e\n```\n\n```csharp\n[RelayCommand]\nvoid Update(SortableUpdateEventArgs e)\n{\n    if (!e.ApplyUpdateMutation())\n    {\n        return;\n    }\n\n    Console.WriteLine($\"Moved from {e.OldIndex} to {e.NewIndex}\");\n}\n```\n\n**2. Droppable targets (cross-collection, ViewModel acceptance):**\n\n```xml\n\u003cItemsControl sortable:Sortable.Group=\"main\"\n              sortable:Sortable.Droppable=\"True\"\n              sortable:Sortable.DropCommand=\"{Binding DropCmd}\"\n              ItemsSource=\"{Binding TargetItems}\"\u003e\n    \u003cItemsControl.ItemTemplate\u003e\n        \u003cDataTemplate\u003e\n            \u003cBorder sortable:Sortable.IsDroppable=\"True\" /\u003e\n        \u003c/DataTemplate\u003e\n    \u003c/ItemsControl.ItemTemplate\u003e\n\u003c/ItemsControl\u003e\n```\n\n```csharp\n[RelayCommand]\nvoid Drop(SortableDropEventArgs e)\n{\n    e.IsAccepted = ValidateItem(e.Item);\n    e.TransferMode = SortableTransferMode.Move;\n\n    var applied = e.ApplyDropMutation();\n    if (!applied)\n    {\n        Debug.WriteLine(\"Drop was rejected or produced no mutation.\");\n    }\n}\n```\n\n**3. Run demo app:**\n\n```powershell\ndotnet run --project .\\Sortable.Avalonia.Demo\\Sortable.Avalonia.Demo.csproj\n```\n\n## Item Display Layout (ItemsPanelTemplate)\n\n`Sortable` works with any `ItemsControl` panel. You can change how items are displayed by overriding `ItemsControl.ItemsPanel` with an `ItemsPanelTemplate`.\n\n**Vertical stack (default list):**\n\n```xml\n\u003cItemsControl sortable:Sortable.Sortable=\"True\"\n              ItemsSource=\"{Binding Items}\"\u003e\n    \u003cItemsControl.ItemsPanel\u003e\n        \u003cItemsPanelTemplate\u003e\n            \u003cStackPanel Orientation=\"Vertical\" /\u003e\n        \u003c/ItemsPanelTemplate\u003e\n    \u003c/ItemsControl.ItemsPanel\u003e\n\n    \u003cItemsControl.ItemTemplate\u003e\n        \u003cDataTemplate\u003e\n            \u003cBorder sortable:Sortable.IsSortable=\"True\" /\u003e\n        \u003c/DataTemplate\u003e\n    \u003c/ItemsControl.ItemTemplate\u003e\n\u003c/ItemsControl\u003e\n```\n\n**Horizontal stack (lane-style):**\n\n```xml\n\u003cItemsControl sortable:Sortable.Sortable=\"True\"\n              ItemsSource=\"{Binding Items}\"\u003e\n    \u003cItemsControl.ItemsPanel\u003e\n        \u003cItemsPanelTemplate\u003e\n            \u003cStackPanel Orientation=\"Horizontal\" /\u003e\n        \u003c/ItemsPanelTemplate\u003e\n    \u003c/ItemsControl.ItemsPanel\u003e\n\u003c/ItemsControl\u003e\n```\n\n**Uniform grid (card board):**\n\n```xml\n\u003cItemsControl sortable:Sortable.Sortable=\"True\"\n              ItemsSource=\"{Binding Cards}\"\u003e\n    \u003cItemsControl.ItemsPanel\u003e\n        \u003cItemsPanelTemplate\u003e\n            \u003cUniformGrid Columns=\"3\" /\u003e\n        \u003c/ItemsPanelTemplate\u003e\n    \u003c/ItemsControl.ItemsPanel\u003e\n\u003c/ItemsControl\u003e\n```\n\n**Wrap panel (responsive flow):**\n\n```xml\n\u003cItemsControl sortable:Sortable.Sortable=\"True\"\n              ItemsSource=\"{Binding Items}\"\u003e\n    \u003cItemsControl.ItemsPanel\u003e\n        \u003cItemsPanelTemplate\u003e\n            \u003cWrapPanel Orientation=\"Horizontal\" /\u003e\n        \u003c/ItemsPanelTemplate\u003e\n    \u003c/ItemsControl.ItemsPanel\u003e\n\u003c/ItemsControl\u003e\n```\n\nTip: 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.\n\n## Properties Reference\n\n### ItemsControl Attached Properties\n\n| Property | Type | Default | Description |\n|---|---|:---:|---|\n| `Sortable` | `bool` | `false` | Enable same-collection reordering |\n| `Droppable` | `bool` | `false` | Enable cross-collection drop target |\n| `Group` | `string?` | `null` | Group name (only same-group collections interact) |\n| `UpdateCommand` | `ICommand?` | `null` | Fires on same-collection reorder |\n| `DropCommand` | `ICommand?` | `null` | Fires on cross-collection drop (reversible) |\n| `ReleaseCommand` | `ICommand?` | `null` | Fires when item is released outside any valid drop target |\n| `TransferCommand` | `ICommand?` | `null` | Legacy cross-collection (auto-accept, deprecated) |\n| `Mode` | `SortableMode` | `Sort` | In-collection behavior: `Sort` or `Swap` |\n| `CrossCollectionTransferMode` | `SortableTransferMode` | `Move` | Default transfer mode: `Move`, `Copy`, or `Swap` |\n| `AnimationDuration` | `TimeSpan` | `0:0:0.250` | Animation duration (as TimeSpan, e.g. `0:0:0.500` for 500ms) |\n\n### Item Attached Properties\n\n| Property | Type | Default | Description |\n|---|---|:---:|---|\n| `IsSortable` | `bool` | `false` | Item can be sorted within collection |\n| `IsDroppable` | `bool` | `false` | Item can participate in drops |\n| `IsDragHandle` | `bool` | `false` | Marks control as drag handle |\n\n## Event Arguments\n\n### SortableUpdateEventArgs\n\n```csharp\npublic class SortableUpdateEventArgs\n{\n    public object? Item { get; set; }       // Item being moved\n    public int OldIndex { get; set; }       // Original index\n    public int NewIndex { get; set; }       // Target index\n    public IList? SourceCollection { get; set; }\n    public SortableMode Mode { get; set; }  // Sort or Swap\n}\n```\n\n**Usage:**\n\n```csharp\n[RelayCommand]\nvoid Update(SortableUpdateEventArgs e)\n{\n    if (e.ApplyUpdateMutation())\n    {\n        Debug.WriteLine($\"{e.Item}: {e.OldIndex} → {e.NewIndex}\");\n    }\n}\n```\n\n### SortableReleaseEventArgs\n\n```csharp\npublic class SortableReleaseEventArgs\n{\n    public object? Item { get; set; }              // Item released\n    public int OldIndex { get; set; }              // Original index\n    public IList? SourceCollection { get; set; }   // Collection item was dragged from\n}\n```\n\n**Usage:**\n\n```csharp\n[RelayCommand]\nvoid Release(SortableReleaseEventArgs e)\n{\n    Debug.WriteLine($\"Item '{e.Item}' released at index {e.OldIndex} from collection {e.SourceCollection}\");\n    // Custom logic for when an item is released outside any valid drop target\n}\n```\n\n### SortableDropEventArgs\n\n```csharp\npublic class SortableDropEventArgs\n{\n    // Read-only context\n    public object? Item { get; set; }\n    public IList? SourceCollection { get; set; }\n    public IList? TargetCollection { get; set; }\n    public int OldIndex { get; set; }\n    public int NewIndex { get; set; }\n\n    // Handler control\n    public bool IsAccepted { get; set; } = true;                    // Accept/reject\n    public SortableTransferMode TransferMode { get; set; } = Move;  // Move/Copy/Swap\n    public object? ModifiedItem { get; set; }                       // Optional clone/modified item\n    \n    public object? GetItemToInsert() =\u003e ModifiedItem ?? Item;\n}\n```\n\n**Usage:**\n\n```csharp\n[RelayCommand]\nvoid Drop(SortableDropEventArgs e)\n{\n    e.IsAccepted = Validate(e.Item);\n    e.TransferMode = SortableTransferMode.Move;\n\n    if (!e.ApplyDropMutation())\n    {\n        Debug.WriteLine(\"Drop mutation was not applied.\");\n    }\n}\n```\n\n### Mutation Helper Extensions\n\nThe library provides extension methods on event args so you can delegate mutation mechanics while keeping full control of business rules and acceptance logic.\n\n**Three approaches:**\n\n#### 1. Helper-driven (simplest)\n\n```csharp\n[RelayCommand]\nvoid Drop(SortableDropEventArgs e)\n{\n    e.IsAccepted = true;\n    e.TransferMode = SortableTransferMode.Move;\n    \n    if (!e.ApplyDropMutation())\n    {\n        Debug.WriteLine(\"Mutation failed or was rejected.\");\n    }\n}\n```\n\n#### 2. Absolute control (manual mutations)\n\n```csharp\n[RelayCommand]\nvoid Drop(SortableDropEventArgs e)\n{\n    if (e.Item is not TaskItem task || !ValidateBusinessRules(task))\n    {\n        e.IsAccepted = false;\n        return;\n    }\n\n    e.IsAccepted = true;\n    e.TransferMode = SortableTransferMode.Move;\n\n    // Manually orchestrate the mutation with your exact logic\n    e.TargetCollection.Insert(e.NewIndex, task);\n    e.SourceCollection.RemoveAt(e.OldIndex);\n    \n    LogCustomTelemetry(task, e.SourceCollection, e.TargetCollection);\n}\n```\n\n#### 3. Hybrid (recommended for most apps)\n\nValidate domain rules yourself, delegate low-level list operations to the helper.\n\n```csharp\n[RelayCommand]\nvoid Drop(SortableDropEventArgs e)\n{\n    if (e.Item is not KanbanCard card)\n    {\n        return;\n    }\n\n    var sourceColumn = FindColumn(e.SourceCollection);\n    var targetColumn = FindColumn(e.TargetCollection);\n\n    if (sourceColumn == null || targetColumn == null)\n    {\n        return;\n    }\n\n    // Domain rule: prevent duplicates\n    if (targetColumn.Items.Contains(card))\n    {\n        e.IsAccepted = false;\n        return;\n    }\n\n    e.IsAccepted = true;\n    e.TransferMode = SortableTransferMode.Move;\n\n    // Let helper handle the mutation\n    if (!e.ApplyDropMutation())\n    {\n        return;\n    }\n\n    // Post-mutation side effects\n    Console.WriteLine($\"Moved '{card.Title}' from {sourceColumn.Name} to {targetColumn.Name}\");\n    NotifyTeam(card, targetColumn);\n}\n```\n\n**Return values:**\n- `e.ApplyUpdateMutation()` → `bool` (true if mutation applied)\n- `e.ApplyDropMutation()` → `bool` (true if mutation applied)\n\nUse the return value to gate logging, telemetry, or conditional side effects.\n\n### SortableTransferEventArgs (Deprecated)\n\n```csharp\npublic class SortableTransferEventArgs\n{\n    public object? Item { get; set; }\n    public IList? SourceCollection { get; set; }\n    public IList? TargetCollection { get; set; }\n    public int OldIndex { get; set; }\n    public int NewIndex { get; set; }\n}\n```\n\nUse `DropCommand` with `SortableDropEventArgs` instead.\n\n## Usage Patterns\n\nFor panel/layout examples (`StackPanel`, `UniformGrid`, `WrapPanel`), see [Item Display Layout (ItemsPanelTemplate)](#item-display-layout-itemspaneltemplate).\n\n### Pattern 1: Sortable-only list\n\n```xml\n\u003cItemsControl sortable:Sortable.Sortable=\"True\"\n              sortable:Sortable.UpdateCommand=\"{Binding UpdateCmd}\"\n              ItemsSource=\"{Binding Items}\"\u003e\n    \u003cItemsControl.ItemTemplate\u003e\n        \u003cDataTemplate\u003e\n            \u003cBorder sortable:Sortable.IsSortable=\"True\" Cursor=\"Hand\"\u003e\n                \u003cTextBlock Text=\"{Binding}\" /\u003e\n            \u003c/Border\u003e\n        \u003c/DataTemplate\u003e\n    \u003c/ItemsControl.ItemTemplate\u003e\n\u003c/ItemsControl\u003e\n```\n\n**Result:** Reorder within list ✓, transfer between lists ✗\n\n### Pattern 2: Droppable-only zones\n\n```xml\n\u003cItemsControl sortable:Sortable.Group=\"zone\"\n              sortable:Sortable.Droppable=\"True\"\n              sortable:Sortable.DropCommand=\"{Binding DropCmd}\"\n              ItemsSource=\"{Binding Items}\"\u003e\n    \u003cItemsControl.ItemTemplate\u003e\n        \u003cDataTemplate\u003e\n            \u003cBorder sortable:Sortable.IsDroppable=\"True\" /\u003e\n        \u003c/DataTemplate\u003e\n    \u003c/ItemsControl.ItemTemplate\u003e\n\u003c/ItemsControl\u003e\n```\n\n**Result:** Transfer between zones ✓, reorder within zone ✗\n\n### Pattern 3: Full dual-mode\n\n```xml\n\u003cItemsControl sortable:Sortable.Sortable=\"True\"\n              sortable:Sortable.Droppable=\"True\"\n              sortable:Sortable.Group=\"shared\"\n              sortable:Sortable.UpdateCommand=\"{Binding UpdateCmd}\"\n              sortable:Sortable.DropCommand=\"{Binding DropCmd}\"\n              ItemsSource=\"{Binding Items}\"\u003e\n    \u003cItemsControl.ItemTemplate\u003e\n        \u003cDataTemplate\u003e\n            \u003cBorder sortable:Sortable.IsSortable=\"True\"\n                    sortable:Sortable.IsDroppable=\"True\" /\u003e\n        \u003c/DataTemplate\u003e\n    \u003c/ItemsControl.ItemTemplate\u003e\n\u003c/ItemsControl\u003e\n```\n\n**Result:** Reorder within list ✓, transfer between lists ✓\n\n### Pattern 4: Conditional acceptance\n\n```csharp\n[RelayCommand]\nvoid Drop(SortableDropEventArgs e)\n{\n    if (e.Item is TaskItem task)\n    {\n        // Business rule validation\n        e.IsAccepted = task.Priority == Priority.Urgent\n                       \u0026\u0026 !TargetContains(task)\n                       \u0026\u0026 UserHasPermission();\n        e.TransferMode = SortableTransferMode.Move;\n        _ = e.ApplyDropMutation();\n    }\n}\n```\n\n### Pattern 5: Copy mode (duplicate on transfer)\n\n```csharp\n[RelayCommand]\nvoid Drop(SortableDropEventArgs e)\n{\n    if (e.Item is TemplateItem original)\n    {\n        e.ModifiedItem = new TemplateItem(original) { Id = Guid.NewGuid() };\n        e.TransferMode = SortableTransferMode.Copy;\n        e.IsAccepted = true;\n        _ = e.ApplyDropMutation();\n    }\n}\n```\n\n### Pattern 6: Prevent duplicates\n\n```csharp\n[RelayCommand]\nvoid Drop(SortableDropEventArgs e)\n{\n    var target = e.TargetCollection as ObservableCollection\u003cMyItem\u003e;\n    var item = e.Item as MyItem;\n\n    e.IsAccepted = target?.Any(x =\u003e x.Id == item?.Id) != true;\n    e.TransferMode = SortableTransferMode.Move;\n    _ = e.ApplyDropMutation();\n}\n```\n\n### Pattern 7: Cross-collection swap\n\n```xml\n\u003cItemsControl sortable:Sortable.Group=\"rotation\"\n              sortable:Sortable.Droppable=\"True\"\n              sortable:Sortable.CrossCollectionTransferMode=\"Swap\"\n              sortable:Sortable.DropCommand=\"{Binding DropCmd}\"\n              ItemsSource=\"{Binding TeamA}\" /\u003e\n\n\u003cItemsControl sortable:Sortable.Group=\"rotation\"\n              sortable:Sortable.Droppable=\"True\"\n              sortable:Sortable.CrossCollectionTransferMode=\"Swap\"\n              sortable:Sortable.DropCommand=\"{Binding DropCmd}\"\n              ItemsSource=\"{Binding TeamB}\" /\u003e\n```\n\n```csharp\n[RelayCommand]\nvoid Drop(SortableDropEventArgs e)\n{\n    e.TransferMode = SortableTransferMode.Swap;  // Exchange items\n    e.IsAccepted = true;\n    _ = e.ApplyDropMutation();\n}\n```\n\n### Pattern 8: Drag handles\n\n```xml\n\u003cItemsControl sortable:Sortable.Sortable=\"True\" ItemsSource=\"{Binding Items}\"\u003e\n    \u003cItemsControl.ItemTemplate\u003e\n        \u003cDataTemplate\u003e\n            \u003cBorder sortable:Sortable.IsSortable=\"True\"\u003e\n                \u003cGrid ColumnDefinitions=\"Auto,*,Auto\"\u003e\n                    \u003c!-- Drag handle --\u003e\n                    \u003cPathIcon Grid.Column=\"0\"\n                              sortable:Sortable.IsDragHandle=\"True\"\n                              Data=\"M8 2v20M16 2v20\"\n                              Cursor=\"Hand\" /\u003e\n                    \u003c!-- Interactive content --\u003e\n                    \u003cTextBlock Grid.Column=\"1\" Text=\"{Binding Name}\" /\u003e\n                    \u003cButton Grid.Column=\"2\" Content=\"Edit\" /\u003e\n                \u003c/Grid\u003e\n            \u003c/Border\u003e\n        \u003c/DataTemplate\u003e\n    \u003c/ItemsControl.ItemTemplate\u003e\n\u003c/ItemsControl\u003e\n```\n\n**Result:** Only drag via handle icon. Text and button remain clickable.\n\n### Pattern 9: Same-collection swap mode\n\n```xml\n\u003cItemsControl sortable:Sortable.Sortable=\"True\"\n              sortable:Sortable.Mode=\"Swap\"\n              sortable:Sortable.UpdateCommand=\"{Binding UpdateCmd}\"\n              ItemsSource=\"{Binding Items}\"\u003e\n    \u003cItemsControl.ItemTemplate\u003e\n        \u003cDataTemplate\u003e\n            \u003cBorder sortable:Sortable.IsSortable=\"True\" /\u003e\n        \u003c/DataTemplate\u003e\n    \u003c/ItemsControl.ItemTemplate\u003e\n\u003c/ItemsControl\u003e\n```\n\n**Result:** Items swap positions instead of shifting.\n\n### Pattern 10: Custom animation speed\n\n```xml\n\u003cItemsControl sortable:Sortable.Sortable=\"True\"\n              sortable:Sortable.AnimationDuration=\"0:0:0.500\"\n              ItemsSource=\"{Binding Items}\"\u003e\n    \u003c!-- Slower 500ms animations --\u003e\n\u003c/ItemsControl\u003e\n\n\u003cItemsControl sortable:Sortable.Sortable=\"True\"\n              sortable:Sortable.AnimationDuration=\"0:0:0.100\"\n              ItemsSource=\"{Binding Items}\"\u003e\n    \u003c!-- Faster 100ms animations --\u003e\n\u003c/ItemsControl\u003e\n```\n\n### Pattern 11: Custom drag template\n\nYou 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.\n\n```xml\n\u003cUserControl.Resources\u003e\n    \u003cDataTemplate x:Key=\"CustomDragTemplate\" x:DataType=\"models:SortableItem\"\u003e\n        \u003cBorder Padding=\"10\" Background=\"{DynamicResource CardBackgroundFillColorDefaultBrush}\" BorderBrush=\"{DynamicResource AccentFillColorDefaultBrush}\" BorderThickness=\"2\" CornerRadius=\"10\" Effect=\"{DynamicResource ShadowEffect}\" Opacity=\"0.98\"\u003e\n            \u003cGrid ColumnDefinitions=\"32,*,Auto\" ColumnSpacing=\"12\"\u003e\n                \u003cTextBlock Grid.Column=\"0\" VerticalAlignment=\"Center\" FontSize=\"28\" Text=\"{Binding Tag}\" /\u003e\n                \u003cStackPanel Grid.Column=\"1\" Spacing=\"2\"\u003e\n                    \u003cTextBlock FontSize=\"18\" FontWeight=\"Bold\" Text=\"{Binding Name}\" /\u003e\n                    \u003cTextBlock FontSize=\"13\" Foreground=\"{DynamicResource TextFillColorSecondaryBrush}\" Text=\"{Binding Note}\" /\u003e\n                \u003c/StackPanel\u003e\n                \u003cBorder Grid.Column=\"2\" Padding=\"6,2\" VerticalAlignment=\"Center\" Background=\"{DynamicResource AccentFillColorDefaultBrush}\" CornerRadius=\"6\"\u003e\n                    \u003cTextBlock FontSize=\"13\" FontWeight=\"SemiBold\" Foreground=\"White\" Text=\"{Binding Tag}\" /\u003e\n                \u003c/Border\u003e\n            \u003c/Grid\u003e\n        \u003c/Border\u003e\n    \u003c/DataTemplate\u003e\n\u003c/UserControl.Resources\u003e\n\u003cItemsControl\n    sortable:Sortable.DraggingTemplate=\"{StaticResource CustomDragTemplate}\"\n    sortable:Sortable.Sortable=\"True\"\n    ItemsSource=\"{Binding Intake}\"\u003e\n    \u003c!-- ...item template... --\u003e\n\u003c/ItemsControl\u003e\n```\n\n**Result:** Custom drag preview for each item, supporting rich layouts and dynamic content.\n\n**Property Reference:** `DraggingTemplate` (attached property)\n\n**Demo:** See \"Custom Drag Template\" scenario in the demo app.\n\n## Transfer Modes\n\n| Mode | Behavior | Source | Target | Example Use Case |\n|---|---|:---:|:---:|---|\n| `Move` | Remove from source, add to target | Item removed | Item added | Task workflow, file organization |\n| `Copy` | Keep in source, add to target | Item stays | Copy added | Template duplication, reference sharing |\n| `Swap` | Exchange items | Gets target item | Gets source item | Role rotation, seat assignment |\n\n**Set via:**\n\n```csharp\ne.TransferMode = SortableTransferMode.Move;    // or .Copy or .Swap\n```\n\n**Or set default for preview animations:**\n\n```xml\nsortable:Sortable.CrossCollectionTransferMode=\"Swap\"\n```\n\n## Sortable Modes\n\n| Mode | Behavior | Use Case |\n|---|---|---|\n| `Sort` | Shift items, insert at drop position | Priority queues, task ordering |\n| `Swap` | Exchange positions with drop target | Role swaps, seat assignments |\n\n**Set via:**\n\n```xml\nsortable:Sortable.Mode=\"Sort\"    \u003c!-- Default --\u003e\nsortable:Sortable.Mode=\"Swap\"\n```\n\n## Drag Handles\n\nRestrict drag start to specific controls:\n\n```xml\n\u003cItemsControl sortable:Sortable.Sortable=\"True\" ItemsSource=\"{Binding Items}\"\u003e\n    \u003cItemsControl.ItemTemplate\u003e\n        \u003cDataTemplate\u003e\n            \u003cBorder sortable:Sortable.IsSortable=\"True\"\u003e\n                \u003cGrid ColumnDefinitions=\"Auto,*\"\u003e\n                    \u003cPathIcon sortable:Sortable.IsDragHandle=\"True\"\n                              Data=\"M8 2v20M16 2v20\"\n                              Cursor=\"Hand\" /\u003e\n                    \u003cTextBlock Grid.Column=\"1\" Text=\"{Binding}\" /\u003e\n                \u003c/Grid\u003e\n            \u003c/Border\u003e\n        \u003c/DataTemplate\u003e\n    \u003c/ItemsControl.ItemTemplate\u003e\n\u003c/ItemsControl\u003e\n```\n\n**Behavior:** Drag only starts from `IsDragHandle=\"True\"` controls. Text, buttons, etc. remain clickable.\n\n## Animation Control\n\n**Set duration (TimeSpan):**\n\n```xml\nsortable:Sortable.AnimationDuration=\"0:0:0.500\"    \u003c!-- Default: 0:0:0.250 --\u003e\n```\n\n**Applies to:**\n- Interactive drag previews (items shifting during drag)\n- Programmatic collection changes\n- Cross-collection travel animations\n\n## Groups\n\n**Isolate interactions:**\n\n```xml\n\u003c!-- HR group --\u003e\n\u003cItemsControl sortable:Sortable.Group=\"hr\" sortable:Sortable.Droppable=\"True\" /\u003e\n\u003cItemsControl sortable:Sortable.Group=\"hr\" sortable:Sortable.Droppable=\"True\" /\u003e\n\n\u003c!-- Engineering group (separate) --\u003e\n\u003cItemsControl sortable:Sortable.Group=\"eng\" sortable:Sortable.Droppable=\"True\" /\u003e\n```\n\n**Rule:** Items only transfer between collections with matching `Group` values.\n\n## 18 Demo Scenarios\n\nRun the app to explore each demo in a dedicated tab:\n\n| # | Demo | Description |\n|:---:|---|---|\n| 1 | **Kanban Board** | Task cards moving through triage → engineering → release columns |\n| 2 | **Vertical List** | Simple sortable task list with reordering |\n| 3 | **Drag Handle** | Items with icon handles, leaving text/buttons clickable |\n| 4 | **Grid Layout** | Card-based layout with drag-and-drop in grid arrangement |\n| 5 | **Horizontal Stack** | Horizontal lane with left-to-right item ordering |\n| 6 | **Multiple Groups** | Separate HR and Engineering groups (isolated interactions) |\n| 7 | **Disabled Items** | Some items locked (non-draggable) via conditional `IsSortable` |\n| 8 | **Sortable Only** | Lists allow reordering but reject cross-list transfers |\n| 9 | **Droppable Only** | Drop zones accept items but prevent internal reordering |\n| 10 | **Cross Drag (Instant)** | Instant cross-collection transfers (no animation delay) |\n| 11 | **Cross Programmatic Animation** | Programmatic `ObservableCollection` changes with smooth travel animation |\n| 12 | **UniformGrid Interaction** | Interactive drag in `UniformGrid` panel |\n| 13 | **UniformGrid Programmatic** | Programmatic changes in `UniformGrid` with animation |\n| 14 | **Copy Mode** | Template items duplicated (not moved) on transfer |\n| 15 | **Conditional Acceptance** | Drop validation rules (e.g., only URGENT tasks accepted) |\n| 16 | **Sort Mode** | Default shift-based reordering within same list |\n| 17 | **Swap Mode** | Exchange positions (no shifting) within same list |\n| 18 | **Cross Swap** | Exchange items between two collections in one gesture |\n\n**Run:**\n\n```powershell\ndotnet run --project .\\Sortable.Avalonia.Demo\\Sortable.Avalonia.Demo.csproj\n```\n\n## Core Concepts\n\n### MVVM-First Workflow\n\n- **View (XAML):** Declare `Sortable` attached properties, `ItemsSource`, and command bindings.\n- **ViewModel:** Validate rules, choose transfer/sort behavior, and apply mutations via event-arg helpers.\n- **Model:** Remains plain data; no drag/drop UI logic required.\n\n### Sortable vs Droppable\n\n| Property | Purpose | Drag Within List | Drag Between Lists |\n|---|---|:---:|:---:|\n| `Sortable=\"True\"` | Enable reordering | ✓ | ✗ |\n| `Droppable=\"True\"` | Enable drop target | ✗ | ✓ |\n| Both `=\"True\"` | Enable both | ✓ | ✓ |\n\n### Item-Level Control\n\n| Scenario | IsSortable | IsDroppable |\n|---|:---:|:---:|\n| Can reorder and transfer | ✓ | ✓ |\n| Can reorder only | ✓ | ✗ |\n| Can transfer only | ✗ | ✓ |\n| Locked (no drag) | ✗ | ✗ |\n\n### Commands\n\n | Command | Fires When | Event Args | Purpose |\n |---|---|---|---|\n| `UpdateCommand` | Same-collection reorder | `SortableUpdateEventArgs` | ViewModel handles reorder rules and mutation |\n| `DropCommand` | Cross-collection drop | `SortableDropEventArgs` | ViewModel accepts/rejects and selects transfer mode |\n| `ReleaseCommand` | Release outside drop target | `SortableReleaseEventArgs` | ViewModel handles item release outside valid drop zone |\n| `TransferCommand` | Cross-collection transfer | `SortableTransferEventArgs` | Legacy fallback (auto-accept), prefer `DropCommand` |\n\n## Contributing\n\nContributions are welcome.\n\n- Open an issue for bug reports or feature proposals.\n- Share repro steps and expected behavior for drag/drop issues.\n- Keep PRs focused and include before/after behavior notes.\n\n## License\n\nThis project is licensed under the MIT License. See `LICENSE` for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frusskyc%2Fsortable-avalonia","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frusskyc%2Fsortable-avalonia","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frusskyc%2Fsortable-avalonia/lists"}