Lattice.Core.DragDrop
Библиотека для реализации drag-and-drop (перетаскивания) в приложениях на .NET.
📋 Обзор
Lattice.Core.DragDrop предоставляет полнофункциональную, асинхронную и потокобезопасную систему для реализации операций перетаскивания в пользовательских интерфейсах. Библиотека построена на принципах разделения ответственности и поддерживает сложные сценарии перетаскивания с минимальными усилиями со стороны разработчика.
✨ Основные возможности
- ✅ Полностью асинхронный API - все операции поддерживают async/await
- ✅ Потокобезопасность - безопасная работа в многопоточных средах
- ✅ Расширяемая архитектура - легко добавлять новые типы источников и целей
- ✅ Подробные события - полный контроль над жизненным циклом операций
- ✅ Статистика и мониторинг - встроенный сбор метрик использования
- ✅ Поддержка CancellationToken - корректная отмена длительных операций
- ✅ Независимость от UI-фреймворков - может использоваться с любым представлением
🏗️ Архитектура
Основные компоненты
1. IDragSource
Интерфейс для объектов, которые могут быть источником данных при перетаскивании. Определяет:
- Возможность начала перетаскивания
- Подготовку данных для передачи
- Реакцию на завершение или отмену операции
2. IDropTarget
Интерфейс для объектов, которые могут принимать сброшенные данные. Определяет:
- Проверку совместимости данных
- Визуальную обратную связь при наведении
- Обработку сброшенных данных
3. DragDropService
Центральный сервис, координирующий все операции. Отвечает за:
- Регистрацию и управление целями сброса
- Оркестрацию жизненного цикла операций
- Распространение событий между компонентами
- Сбор статистики и обработку ошибок
4. Модели данных
- DragInfo - информация о начале перетаскивания
- DropInfo - информация о потенциальном сбросе
- DragDropEffects - перечисление возможных эффектов
🚀 Быстрый старт
1. Установка
// Пример регистрации в DI-контейнере
services.AddSingleton<IDragDropService, DragDropService>();
2. Создание источника перетаскивания
public class ItemDragSource : IDragSource
{
private readonly Item _item;
public ItemDragSource(Item item)
{
_item = item;
}
public async Task<DragInfo?> TryStartDragAsync(Point startPosition, CancellationToken ct)
{
// Проверяем, можно ли начать перетаскивание
if (!_item.CanBeDragged)
return null;
// Создаем информацию о перетаскивании
return new DragInfo(
data: _item,
allowedEffects: DragDropEffects.Copy | DragDropEffects.Move,
startPosition: startPosition,
source: this
);
}
public async Task OnDragCompletedAsync(DragInfo dragInfo, DragDropEffects effects, CancellationToken ct)
{
if (effects == DragDropEffects.Move)
{
// Удаляем элемент при перемещении
await _repository.DeleteAsync(_item.Id, ct);
}
}
public Task OnDragCancelledAsync(DragInfo dragInfo, CancellationToken ct)
{
// Очистка ресурсов при отмене
return Task.CompletedTask;
}
}
3. Создание цели сброса
public class ContainerDropTarget : IDropTarget
{
private readonly ObservableCollection<Item> _items;
public async Task<bool> CanAcceptDropAsync(DropInfo dropInfo, CancellationToken ct)
{
// Проверяем тип данных
if (dropInfo.Data is not Item item)
return false;
// Проверяем бизнес-правила
return await _validator.CanAddItemAsync(item, ct);
}
public async Task OnDragOverAsync(DropInfo dropInfo, CancellationToken ct)
{
// Обновляем визуальную обратную связь
dropInfo.SuggestedEffects = DragDropEffects.Move;
dropInfo.ShowVisualFeedback = true;
}
public async Task OnDropAsync(DropInfo dropInfo, CancellationToken ct)
{
var item = (Item)dropInfo.Data;
await _items.AddAsync(item, ct);
// Помечаем как обработанное
dropInfo.MarkAsHandled();
}
public Task OnDragLeaveAsync(CancellationToken ct)
{
// Очищаем визуальную обратную связь
return Task.CompletedTask;
}
}
4. Регистрация и использование сервиса
public class MainViewModel
{
private readonly IDragDropService _dragDropService;
public MainViewModel(IDragDropService dragDropService)
{
_dragDropService = dragDropService;
// Регистрация цели сброса
_targetId = _dragDropService.RegisterDropTarget(
target: new ContainerDropTarget(_items),
bounds: new Rect(0, 0, 400, 300),
priority: 0,
group: "main-container"
);
// Подписка на события
_dragDropService.DragStarted += OnDragStarted;
_dragDropService.DragCompleted += OnDragCompleted;
_dragDropService.ErrorOccurred += OnError;
}
// Обработка мышиных событий
public async Task OnMouseDown(Point position)
{
var source = new ItemDragSource(selectedItem);
await _dragDropService.StartDragAsync(source, position);
}
public async Task OnMouseMove(Point position)
{
await _dragDropService.UpdateDragAsync(position);
}
public async Task OnMouseUp(Point position)
{
var effects = await _dragDropService.EndDragAsync(position);
// Обработка результатов
}
}
📊 Статистика и мониторинг
// Получение статистики использования
var stats = _dragDropService.GetStats();
Console.WriteLine($"Всего операций: {stats.TotalDragOperations}");
Console.WriteLine($"Успешных сбросов: {stats.SuccessfulDrops}");
Console.WriteLine($"Отменено операций: {stats.CancelledOperations}");
Console.WriteLine($"Среднее время операции: {stats.AverageOperationTime}");
⚙️ Конфигурация
Параметры сервиса
// Настройка через свойства сервиса
_dragDropService.DragStartThreshold = 5.0; // Порог в пикселях
_dragDropService.EnableAsyncOperations = true;
_dragDropService.AsyncOperationTimeout = 3000; // 3 секунды
Константы по умолчанию
Все значения по умолчанию определены в классе DragDropConstants:
DefaultDragThreshold: 3.0 пикселейDefaultAsyncTimeout: 5000 миллисекундTargetLifetimeMinutes: 10 минут
🔧 Расширенные сценарии
Группировка целей сброса
// Регистрация группы целей
_dragDropService.RegisterDropTarget(target1, bounds1, group: "panel");
_dragDropService.RegisterDropTarget(target2, bounds2, group: "panel");
// Массовая отмена регистрации
_dragDropService.UnregisterDropTargetsInGroup("panel");
Обработка ошибок
private void OnError(object sender, DragDropErrorEventArgs e)
{
_logger.LogError(e.Exception,
"Ошибка в операции {Operation}",
e.Operation);
// Уведомление пользователя
_notificationService.ShowError("Ошибка при перетаскивании");
}
Кастомизация эффектов
// Использование расширений для работы с эффектами
if (effects.CanCopy())
{
// Логика для копирования
}
if (effects.CanMove())
{
// Логика для перемещения
}
// Определение эффекта по модификаторам клавиш
var effect = DragDropEffectsExtensions.GetEffectFromKeys(
controlKey: Keyboard.IsControlDown,
shiftKey: Keyboard.IsShiftDown,
altKey: Keyboard.IsAltDown
);
📝 Best Practices
-
Производительность
- Методы
CanAcceptDropAsyncиOnDragOverAsyncвызываются часто, оптимизируйте их - Избегайте синхронных операций в обработчиках
- Используйте кэширование при проверке типов данных
- Методы
-
Безопасность
- Всегда проверяйте тип данных в
CanAcceptDropAsync - Валидируйте бизнес-правила перед обработкой сброса
- Используйте CancellationToken для отмены длительных операций
- Всегда проверяйте тип данных в
-
Пользовательский опыт
- Предоставляйте визуальную обратную связь через
DropInfo.ShowVisualFeedback - Используйте
DropInfo.VisualFeedbackDataдля кастомизации отображения - Обрабатывайте отмену операций для очистки временных данных
- Предоставляйте визуальную обратную связь через
🔄 Миграция
С версии 1.x на 2.0
-
Интерфейсы переименованы:
CanStartDragAsync→TryStartDragAsyncDragCompletedAsync→OnDragCompletedAsyncDragCancelledAsync→OnDragCancelledAsync
-
Классы EventArgs объединены:
- Все события наследуются от
DragEventArgs - Упрощена иерархия классов событий
- Все события наследуются от
-
Удалены устаревшие компоненты:
AsyncDragDropUtilitiesудаленServiceCollectionExtensionsудален
📄 Лицензия
Библиотека распространяется под лицензией MIT. Подробности см. в файле LICENSE.
🤝 Вклад в разработку
Мы приветствуем вклад в развитие библиотеки. Перед отправкой pull request ознакомьтесь с руководством по контрибьютингу.
🐛 Отчеты об ошибках
Для сообщения об ошибках используйте Issues на GitHub. Пожалуйста, указывайте:
- Версию библиотеки
- Шаги для воспроизведения
- Ожидаемое и фактическое поведение
- Пример кода для демонстрации проблемы