# Lattice.Core.DragDrop Профессиональная, асинхронная система перетаскивания для .NET приложений. Полностью потокобезопасная, расширяемая архитектура с поддержкой кросс-платформенности. ## 📋 Особенности - ✅ **Полная асинхронная поддержка** - async/await для всех операций - ✅ **Потокобезопасность** - `ReaderWriterLockSlim` для эффективной синхронизации - ✅ **Производительность** - Оптимизированные алгоритмы, кэширование, минимальные аллокации - ✅ **Расширяемость** - Легкая интеграция с любыми UI фреймворками - ✅ **Надежность** - Таймауты, обработка ошибок, корректное освобождение ресурсов - ✅ **Статистика** - Встроенный мониторинг производительности ## 🏗️ Архитектура ### Основные компоненты ``` Lattice.Core.DragDrop/ ├── Abstractions/ # Интерфейсы │ ├── IDragSource.cs # Источник перетаскивания (синхронный) │ ├── IAsyncDragSource.cs # Асинхронный источник │ ├── IDropTarget.cs # Цель сброса (синхронная) │ └── IAsyncDropTarget.cs # Асинхронная цель ├── Enums/ # Перечисления ├── Exceptions/ # Исключения с кодами ошибок ├── Extensions/ # Расширения для DI ├── Models/ # Модели данных │ ├── DragInfo.cs # Информация о перетаскивании │ └── DropInfo.cs # Информация о сбросе └── Services/ # Сервисы ├── IDragDropService.cs # Основной интерфейс ├── DragDropService.cs # Реализация сервиса └── EventArgs/ # Аргументы событий ``` ## 🚀 Быстрый старт ### 1. Установка ```csharp // Добавьте проект Lattice.Core.DragDrop в ваше решение // или создайте NuGet пакет ``` ### 2. Базовое использование ```csharp using Lattice.Core.DragDrop; using Lattice.Core.DragDrop.Abstractions; using Lattice.Core.DragDrop.Services; using Lattice.Core.Geometry; // Создаем сервис var dragDropService = new DragDropService(); // Создаем простой источник перетаскивания var dragSource = DragDropUtilities.CreateSimpleDragSource( dataProvider: () => "Example Data", canDrag: () => true, onCompleted: (dragInfo, effects) => Console.WriteLine($"Drag completed with effects: {effects}"), onCancelled: dragInfo => Console.WriteLine("Drag cancelled") ); // Создаем простую цель сброса var dropTarget = DragDropUtilities.CreateSimpleDropTarget( canAccept: dropInfo => dropInfo.Data is string, onDragOver: dropInfo => dropInfo.SuggestedEffects = DragDropEffects.Copy, onDrop: dropInfo => { Console.WriteLine($"Dropped: {dropInfo.Data}"); dropInfo.MarkAsHandled(); } ); // Регистрируем цель string targetId = dragDropService.RegisterDropTarget( dropTarget, new Rect(100, 100, 300, 200) ); // Начинаем перетаскивание bool started = dragDropService.StartDrag( dragSource, new Point(50, 50) ); if (started) { // Обновляем позицию dragDropService.UpdateDrag(new Point(150, 150)); // Завершаем var effects = dragDropService.EndDrag(new Point(200, 200)); } ``` ## 📖 Подробное руководство ### Сервис перетаскивания Основной класс системы - `DragDropService`, реализующий `IDragDropService`. ```csharp // Создание с кастомными настройками var service = new DragDropService(options => { options.DragStartThreshold = 5.0; options.EnableAsyncOperations = true; options.AsyncOperationTimeout = 3000; options.EnableAutoCleanup = true; }); // Свойства bool isActive = service.IsDragActive; // Активна ли операция DragInfo? currentDrag = service.CurrentDragInfo; // Текущая информация double threshold = service.DragStartThreshold; // Порог начала // События service.DragStarted += OnDragStarted; service.DragUpdated += OnDragUpdated; service.DragCompleted += OnDragCompleted; service.DragCancelled += OnDragCancelled; service.ErrorOccurred += OnErrorOccurred; // Регистрация целей string id = service.RegisterDropTarget( target, // IDropTarget bounds, // Rect priority: 1, // Приоритет (выше = выше приоритет) group: "main" // Группа для группового удаления ); // Обновление границ service.UpdateDropTargetBounds(id, newBounds); // Удаление service.UnregisterDropTarget(id); service.UnregisterDropTargetsInGroup("main"); ``` ### Асинхронное использование ```csharp // Асинхронные методы bool started = await service.StartDragAsync(source, startPosition); await service.UpdateDragAsync(currentPosition); DragDropEffects effects = await service.EndDragAsync(dropPosition); await service.CancelDragAsync(); // Статистика var stats = service.GetStats(); Console.WriteLine($"Operations: {stats.TotalDragOperations}"); Console.WriteLine($"Success rate: {stats.SuccessfulDrops}/{stats.TotalDragOperations}"); Console.WriteLine($"Avg time: {stats.AverageOperationTime.TotalMilliseconds}ms"); ``` ### Создание кастомных источников и целей #### Синхронная реализация ```csharp public class FileDragSource : IDragSource { private readonly FileInfo _file; public FileDragSource(FileInfo file) => _file = file; public bool CanStartDrag(out DragInfo? dragInfo) { // Проверяем условия if (!_file.Exists || _file.Length > 100 * 1024 * 1024) // 100 MB limit { dragInfo = null; return false; } // Создаем DragInfo dragInfo = new DragInfo( data: _file, allowedEffects: DragDropEffects.Copy | DragDropEffects.Move, startPosition: Point.Zero, source: this ); // Добавляем дополнительные параметры dragInfo.SetParameter("FileSize", _file.Length); dragInfo.SetParameter("MimeType", GetMimeType(_file)); return true; } public bool StartDrag(DragInfo dragInfo) { // Подготовка к перетаскиванию // Можно создать визуальное представление и т.д. Console.WriteLine($"Starting drag of {_file.Name}"); return true; } public void DragCompleted(DragInfo dragInfo, DragDropEffects effects) { Console.WriteLine($"File drag completed with {effects}"); if (effects == DragDropEffects.Move) { // Файл был перемещен - возможно, удалить оригинал // _file.Delete(); } } public void DragCancelled(DragInfo dragInfo) { Console.WriteLine("File drag cancelled"); } } ``` #### Асинхронная реализация ```csharp public class DatabaseItemDragSource : IAsyncDragSource { private readonly DatabaseService _db; private readonly int _itemId; public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync() { try { // Асинхронные проверки var canDrag = await _db.CanDragItemAsync(_itemId); if (!canDrag) return (false, null); // Асинхронная загрузка данных var data = await _db.GetItemForDragAsync(_itemId); if (data == null) return (false, null); var dragInfo = new DragInfo( data: data, allowedEffects: DragDropEffects.Copy | DragDropEffects.Move, startPosition: Point.Zero, source: this ); return (true, dragInfo); } catch (Exception ex) { // Логирование ошибки return (false, null); } } public Task StartDragAsync(DragInfo dragInfo) { // Асинхронная подготовка return Task.FromResult(true); } public async Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects) { // Асинхронная обработка завершения await _db.LogDragOperationAsync(_itemId, effects); if (effects == DragDropEffects.Move) { await _db.MarkItemAsMovedAsync(_itemId); } } public Task DragCancelledAsync(DragInfo dragInfo) { return Task.CompletedTask; } // Синхронные методы для совместимости public bool CanStartDrag(out DragInfo? dragInfo) { var result = Task.Run(() => CanStartDragAsync()).GetAwaiter().GetResult(); dragInfo = result.DragInfo; return result.CanStart; } // ... остальные синхронные методы } ``` ### Работа с моделями данных #### DragInfo ```csharp // Создание var dragInfo = new DragInfo( data: myObject, allowedEffects: DragDropEffects.Copy | DragDropEffects.Move, startPosition: new Point(x, y), source: this ); // Параметры dragInfo.SetParameter("Timestamp", DateTime.UtcNow); dragInfo.SetParameter("UserId", currentUser.Id); // Получение параметров if (dragInfo.TryGetParameter("Category", out var category)) { // Используем категорию } // Клонирование с новой позицией var updatedDragInfo = dragInfo.CloneWithPosition(newPosition); // Очистка ресурсов dragInfo.Dispose(); ``` #### DropInfo ```csharp // Создается сервисом автоматически // Работа с DropInfo в методах цели: public void DragOver(DropInfo dropInfo) { // Проверяем данные if (dropInfo.Data is MyDataType myData) { // Определяем позицию относительно цели dropInfo.DropPosition = CalculateDropPosition(dropInfo.Position); // Предлагаем эффекты if (CanAcceptData(myData)) { dropInfo.SuggestedEffects = DragDropEffects.Move; dropInfo.ShowVisualFeedback = true; dropInfo.VisualFeedbackData = CreatePreview(myData); } else { dropInfo.SuggestedEffects = DragDropEffects.None; } } } public void Drop(DropInfo dropInfo) { if (dropInfo.Data is MyDataType myData) { // Обработка сброса ProcessDrop(myData, dropInfo.DropPosition); // Помечаем как обработанное dropInfo.MarkAsHandled(); } } ``` ### Утилиты и фабрики #### Синхронные утилиты ```csharp // Простые реализации var simpleSource = DragDropUtilities.CreateSimpleDragSource( () => data, () => true, (dragInfo, effects) => Console.WriteLine("Completed"), dragInfo => Console.WriteLine("Cancelled") ); var simpleTarget = DragDropUtilities.CreateSimpleDropTarget( dropInfo => dropInfo.Data != null, dropInfo => dropInfo.SuggestedEffects = DragDropEffects.Copy, dropInfo => Console.WriteLine($"Dropped: {dropInfo.Data}"), () => Console.WriteLine("Drag left") ); // Геометрия double distance = DragDropUtilities.CalculateDistance(p1, p2); bool exceeded = DragDropUtilities.HasExceededDragThreshold(start, current, threshold); DropPosition position = DragDropUtilities.GetDropPosition(point, bounds, edgeThreshold); // Проверка совместимости bool compatible = DragDropUtilities.AreEffectsCompatible(sourceEffects, targetEffects); bool typeMatch = DragDropUtilities.IsDataCompatible(data, new[] { typeof(string), typeof(int) }); ``` #### Асинхронные утилиты ```csharp // Асинхронные реализации var asyncSource = AsyncDragDropUtilities.CreateAsyncDragSource( async () => await LoadDataAsync(), async () => await CanDragAsync(), async (dragInfo, effects) => await OnCompletedAsync(dragInfo, effects), async dragInfo => await OnCancelledAsync(dragInfo) ); var asyncTarget = AsyncDragDropUtilities.CreateAsyncDropTarget( async dropInfo => await CanAcceptAsync(dropInfo.Data), async dropInfo => await OnDragOverAsync(dropInfo), async dropInfo => await OnDropAsync(dropInfo), async () => await OnDragLeaveAsync() ); // Адаптеры для синхронных интерфейсов IAsyncDragSource asyncFromSync = AsyncDragDropUtilities.CreateAsyncAdapter(syncSource); IAsyncDropTarget asyncTargetFromSync = AsyncDragDropUtilities.CreateAsyncAdapter(syncTarget); // Комбинированные реализации (fallback стратегия) var combined = AsyncDragDropUtilities.Combine( syncSource, asyncSource, preferAsync: true // При ошибке в async использует sync ); // Таймауты var result = await AsyncDragDropUtilities.ExecuteWithTimeoutAsync( task: LongOperationAsync(), timeout: TimeSpan.FromSeconds(5), defaultValue: fallbackValue ); ``` ### Обработка ошибок ```csharp // Подписка на ошибки service.ErrorOccurred += (sender, e) => { Console.WriteLine($"Error in {e.Operation}: {e.Exception.Message}"); // Коды ошибок определены в DragDropErrorCodes switch (e.ErrorCode) { case DragDropErrorCodes.Timeout: Console.WriteLine("Operation timed out"); break; case DragDropErrorCodes.SourceCannotDrag: Console.WriteLine("Source cannot drag"); break; case DragDropErrorCodes.TargetCannotAccept: Console.WriteLine("Target cannot accept"); break; } }; // Использование в коде try { await service.StartDragAsync(source, position); } catch (DragDropException ex) { // Обработка специфичных для DragDrop ошибок Console.WriteLine($"DragDrop error {ex.ErrorCode}: {ex.Message}"); } catch (Exception ex) { // Обработка других ошибок Console.WriteLine($"General error: {ex.Message}"); } ``` ## 🔧 Интеграция с UI фреймворками ### Базовый адаптер для WinUI/WPF ```csharp public class UIElementDragSource : IAsyncDragSource { private readonly FrameworkElement _element; private readonly Func _dataProvider; public UIElementDragSource(FrameworkElement element, Func dataProvider) { _element = element; _dataProvider = dataProvider; // Подписка на события _element.PointerPressed += OnPointerPressed; _element.PointerMoved += OnPointerMoved; _element.PointerReleased += OnPointerReleased; } private Point _dragStartPosition; private bool _isDragging; private void OnPointerPressed(object sender, PointerRoutedEventArgs e) { var point = e.GetCurrentPoint(_element); _dragStartPosition = new Point(point.Position.X, point.Position.Y); } private async void OnPointerMoved(object sender, PointerRoutedEventArgs e) { if (_isDragging) return; var point = e.GetCurrentPoint(_element); var current = new Point(point.Position.X, point.Position.Y); var distance = Math.Sqrt( Math.Pow(current.X - _dragStartPosition.X, 2) + Math.Pow(current.Y - _dragStartPosition.Y, 2)); if (distance > 3.0) // Порог { _isDragging = true; // Начинаем перетаскивание через сервис var service = GetDragDropService(); await service.StartDragAsync(this, ConvertToScreen(_dragStartPosition)); } } public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync() { var data = _dataProvider(); if (data == null) return (false, null); var dragInfo = new DragInfo( data, DragDropEffects.Copy | DragDropEffects.Move, Point.Zero, this ); return (true, dragInfo); } // ... остальная реализация } ``` ## 🧪 Тестирование ### Примеры модульных тестов ```csharp [TestClass] public class DragDropServiceTests { private DragDropService _service; private Mock _mockSource; private Mock _mockTarget; [TestInitialize] public void Setup() { _service = new DragDropService(); _mockSource = new Mock(); _mockTarget = new Mock(); } [TestMethod] public async Task StartDrag_ValidSource_ReturnsTrue() { // Arrange var dragInfo = new DragInfo("test", DragDropEffects.Copy, Point.Zero); _mockSource.Setup(s => s.CanStartDragAsync()) .ReturnsAsync((true, dragInfo)); _mockSource.Setup(s => s.StartDragAsync(It.IsAny())) .ReturnsAsync(true); // Act var result = await _service.StartDragAsync(_mockSource.Object, Point.Zero); // Assert Assert.IsTrue(result); Assert.IsTrue(_service.IsDragActive); } [TestMethod] public async Task UpdateDrag_FindsTarget_CallsDragOver() { // Arrange var targetId = _service.RegisterDropTarget( _mockTarget.Object, new Rect(0, 0, 100, 100) ); await StartTestDrag(); _mockTarget.Setup(t => t.CanAcceptDropAsync(It.IsAny())) .ReturnsAsync(true); // Act await _service.UpdateDragAsync(new Point(50, 50)); // Assert _mockTarget.Verify(t => t.DragOverAsync(It.IsAny()), Times.Once); } private async Task StartTestDrag() { var dragInfo = new DragInfo("test", DragDropEffects.Copy, Point.Zero); _mockSource.Setup(s => s.CanStartDragAsync()) .ReturnsAsync((true, dragInfo)); _mockSource.Setup(s => s.StartDragAsync(It.IsAny())) .ReturnsAsync(true); await _service.StartDragAsync(_mockSource.Object, Point.Zero); } } ``` ## 📊 Мониторинг и производительность ### Сбор статистики ```csharp // Получение статистики var stats = service.GetStats(); Console.WriteLine($"Total operations: {stats.TotalDragOperations}"); Console.WriteLine($"Successful: {stats.SuccessfulDrops}"); Console.WriteLine($"Cancelled: {stats.CancelledOperations}"); Console.WriteLine($"Errors: {stats.ErrorCount}"); Console.WriteLine($"Avg time: {stats.AverageOperationTime.TotalMilliseconds}ms"); // Мониторинг в реальном времени private Stopwatch _operationTimer; service.DragStarted += (s, e) => { _operationTimer = Stopwatch.StartNew(); Console.WriteLine($"Drag started from {e.DragInfo.Source}"); }; service.DragCompleted += (s, e) => { _operationTimer.Stop(); Console.WriteLine($"Drag completed in {_operationTimer.ElapsedMilliseconds}ms"); if (service.EnableAsyncOperations) { var stats = service.GetStats(); Console.WriteLine($"Success rate: {(double)stats.SuccessfulDrops / stats.TotalDragOperations:P}"); } }; ``` ### Оптимизация производительности ```csharp // 1. Настройка параметров var service = new DragDropService(options => { options.DragStartThreshold = 4.0; // Увеличить порог для предотвращения случайных перетаскиваний options.AsyncOperationTimeout = 2000; // Уменьшить таймаут для отзывчивости options.EnableAutoCleanup = true; // Автоочистка неиспользуемых целей }); // 2. Группировка целей _service.RegisterDropTarget(target1, bounds1, group: "toolbox"); _service.RegisterDropTarget(target2, bounds2, group: "toolbox"); // Быстрое удаление всех целей группы _service.UnregisterDropTargetsInGroup("toolbox"); // 3. Приоритеты для оптимизации поиска _service.RegisterDropTarget(importantTarget, bounds, priority: 100); // Высокий приоритет _service.RegisterDropTarget(defaultTarget, bounds, priority: 0); // Низкий приоритет // 4. Периодическая очистка service.ClearAllDropTargets(); // При смене контекста ``` ## 🚀 Продвинутые сценарии ### Переупорядочивание элементов ```csharp public class ReorderableListDropTarget : IAsyncDropTarget { private readonly IList _items; public async Task CanAcceptDropAsync(DropInfo dropInfo) { return dropInfo.Data is object && _items.Contains(dropInfo.Data); } public async Task DropAsync(DropInfo dropInfo) { var item = dropInfo.Data; var insertIndex = CalculateInsertIndex(dropInfo); // Удаляем из старой позиции _items.Remove(item); // Вставляем в новую позицию if (insertIndex < _items.Count) _items.Insert(insertIndex, item); else _items.Add(item); dropInfo.MarkAsHandled(); } private int CalculateInsertIndex(DropInfo dropInfo) { // Логика определения позиции вставки на основе dropInfo.Position // и визуального расположения элементов return 0; } } ``` ### Мультиселект и групповое перетаскивание ```csharp public class MultiSelectionDragSource : IAsyncDragSource { private readonly IEnumerable _selectedItems; public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync() { if (!_selectedItems.Any()) return (false, null); // Создаем коллекцию для перетаскивания var dragData = new DragItemCollection(_selectedItems); var dragInfo = new DragInfo( dragData, DragDropEffects.Copy | DragDropEffects.Move, Point.Zero, this ); dragInfo.SetParameter("ItemCount", _selectedItems.Count()); dragInfo.SetParameter("IsMultiSelect", true); return (true, dragInfo); } } ``` ## 📚 API Reference ### Основные интерфейсы #### IDragDropService ```csharp bool IsDragActive { get; } DragInfo? CurrentDragInfo { get; } IDropTarget? CurrentDropTarget { get; } double DragStartThreshold { get; set; } bool EnableAsyncOperations { get; set; } // Регистрация целей string RegisterDropTarget(IDropTarget target, Rect bounds, int priority = 0, string? group = null); bool UpdateDropTargetBounds(string id, Rect bounds); bool UnregisterDropTarget(string id); void UnregisterDropTargetsInGroup(string group); // Асинхронные операции Task StartDragAsync(IDragSource source, Point startPosition); Task UpdateDragAsync(Point position); Task EndDragAsync(Point position); Task CancelDragAsync(); // Синхронные операции bool StartDrag(IDragSource source, Point startPosition); void UpdateDrag(Point position); DragDropEffects EndDrag(Point position); void CancelDrag(); // Утилиты void ClearAllDropTargets(); DragDropStats GetStats(); ``` #### IAsyncDragSource ```csharp Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync(); Task StartDragAsync(DragInfo dragInfo); Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects); Task DragCancelledAsync(DragInfo dragInfo); ``` #### IAsyncDropTarget ```csharp Task CanAcceptDropAsync(DropInfo dropInfo); Task DragOverAsync(DropInfo dropInfo); Task DropAsync(DropInfo dropInfo); Task DragLeaveAsync(); ``` ### Перечисления #### DragDropEffects ```csharp [Flags] None = 0 Copy = 1 << 0 // Копирование данных Move = 1 << 1 // Перемещение данных Link = 1 << 2 // Ссылка на данные CopyOrMove = Copy | Move All = Copy | Move | Link // Методы расширения: bool CanCopy(this DragDropEffects effects) bool CanMove(this DragDropEffects effects) bool CanLink(this DragDropEffects effects) DragDropEffects GetEffectFromKeys(bool controlKey, bool shiftKey, bool altKey) ``` #### DropPosition ```csharp Inside // Внутри элемента Top // Сверху Bottom // Снизу Left // Слева Right // Справа Center // По центру ``` ## 🔮 Планы развития 1. **Интеграция с популярными UI фреймворками** (WinUI, Uno Platform, Avalonia) 2. **Поддержка жестов** (тач, мультитач) 3. **Виртуализация** для работы с большими наборами данных 4. **Продвинутые визуальные эффекты** (анимации, превью) 5. **Source Generators** для автоматической генерации кода 6. **Инструменты разработчика** (дебаггер, профилировщик)