27 KiB
27 KiB
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/ # Аргументы событий
└── Utilities/ # Утилиты и фабрики
├── DragDropUtilities.cs # Синхронные утилиты
└── AsyncDragDropUtilities.cs # Асинхронные утилиты
🚀 Быстрый старт
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 // По центру
🔮 Планы развития
- Интеграция с популярными UI фреймворками (WinUI, Uno Platform, Avalonia)
- Поддержка жестов (тач, мультитач)
- Виртуализация для работы с большими наборами данных
- Продвинутые визуальные эффекты (анимации, превью)
- Source Generators для автоматической генерации кода
- Инструменты разработчика (дебаггер, профилировщик)