Files
Lattice/Lattice.Core.DragDrop/README.md

26 KiB
Raw Blame History

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. Установка

// Добавьте проект Lattice.Core.DragDrop в ваше решение
// или создайте NuGet пакет

2. Базовое использование

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.

// Создание с кастомными настройками
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");

Асинхронное использование

// Асинхронные методы
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");

Создание кастомных источников и целей

Синхронная реализация

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");
    }
}

Асинхронная реализация

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<bool> 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

// Создание
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<string>("Category", out var category))
{
    // Используем категорию
}

// Клонирование с новой позицией
var updatedDragInfo = dragInfo.CloneWithPosition(newPosition);

// Очистка ресурсов
dragInfo.Dispose();

DropInfo

// Создается сервисом автоматически
// Работа с 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();
    }
}

Утилиты и фабрики

Синхронные утилиты

// Простые реализации
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) });

Асинхронные утилиты

// Асинхронные реализации
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
);

Обработка ошибок

// Подписка на ошибки
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

public class UIElementDragSource : IAsyncDragSource
{
    private readonly FrameworkElement _element;
    private readonly Func<object> _dataProvider;
    
    public UIElementDragSource(FrameworkElement element, Func<object> 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);
    }
    
    // ... остальная реализация
}

🧪 Тестирование

Примеры модульных тестов

[TestClass]
public class DragDropServiceTests
{
    private DragDropService _service;
    private Mock<IAsyncDragSource> _mockSource;
    private Mock<IAsyncDropTarget> _mockTarget;
    
    [TestInitialize]
    public void Setup()
    {
        _service = new DragDropService();
        _mockSource = new Mock<IAsyncDragSource>();
        _mockTarget = new Mock<IAsyncDropTarget>();
    }
    
    [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<DragInfo>()))
            .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<DropInfo>()))
            .ReturnsAsync(true);
        
        // Act
        await _service.UpdateDragAsync(new Point(50, 50));
        
        // Assert
        _mockTarget.Verify(t => t.DragOverAsync(It.IsAny<DropInfo>()), 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<DragInfo>()))
            .ReturnsAsync(true);
        
        await _service.StartDragAsync(_mockSource.Object, Point.Zero);
    }
}

📊 Мониторинг и производительность

Сбор статистики

// Получение статистики
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}");
    }
};

Оптимизация производительности

// 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(); // При смене контекста

🚀 Продвинутые сценарии

Переупорядочивание элементов

public class ReorderableListDropTarget : IAsyncDropTarget
{
    private readonly IList<object> _items;
    
    public async Task<bool> 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;
    }
}

Мультиселект и групповое перетаскивание

public class MultiSelectionDragSource : IAsyncDragSource
{
    private readonly IEnumerable<object> _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

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<bool> StartDragAsync(IDragSource source, Point startPosition);
Task UpdateDragAsync(Point position);
Task<DragDropEffects> 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

Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync();
Task<bool> StartDragAsync(DragInfo dragInfo);
Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects);
Task DragCancelledAsync(DragInfo dragInfo);

IAsyncDropTarget

Task<bool> CanAcceptDropAsync(DropInfo dropInfo);
Task DragOverAsync(DropInfo dropInfo);
Task DropAsync(DropInfo dropInfo);
Task DragLeaveAsync();

Перечисления

DragDropEffects

[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

Inside    // Внутри элемента
Top       // Сверху
Bottom    // Снизу
Left      // Слева
Right     // Справа
Center    // По центру

🔮 Планы развития

  1. Интеграция с популярными UI фреймворками (WinUI, Uno Platform, Avalonia)
  2. Поддержка жестов (тач, мультитач)
  3. Виртуализация для работы с большими наборами данных
  4. Продвинутые визуальные эффекты (анимации, превью)
  5. Source Generators для автоматической генерации кода
  6. Инструменты разработчика (дебаггер, профилировщик)